diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /dom/system | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/system')
59 files changed, 10201 insertions, 0 deletions
diff --git a/dom/system/IOUtils.cpp b/dom/system/IOUtils.cpp new file mode 100644 index 0000000000..7fce321582 --- /dev/null +++ b/dom/system/IOUtils.cpp @@ -0,0 +1,1678 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "IOUtils.h" + +#include <cstdint> + +#include "ErrorList.h" +#include "js/ArrayBuffer.h" +#include "js/JSON.h" +#include "js/Utility.h" +#include "js/experimental/TypedData.h" +#include "jsfriendapi.h" +#include "mozilla/Compression.h" +#include "mozilla/Encoding.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/Maybe.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/Services.h" +#include "mozilla/Span.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/TextUtils.h" +#include "mozilla/Unused.h" +#include "mozilla/Utf8.h" +#include "mozilla/dom/IOUtilsBinding.h" +#include "mozilla/dom/Promise.h" +#include "nsCOMPtr.h" +#include "nsError.h" +#include "nsFileStreams.h" +#include "nsIDirectoryEnumerator.h" +#include "nsIFile.h" +#include "nsIGlobalObject.h" +#include "nsLocalFile.h" +#include "nsPrintfCString.h" +#include "nsReadableUtils.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsTArray.h" +#include "nsThreadManager.h" +#include "nsXULAppAPI.h" +#include "prerror.h" +#include "prio.h" +#include "prtime.h" +#include "prtypes.h" + +#define REJECT_IF_SHUTTING_DOWN(aJSPromise) \ + do { \ + if (sShutdownStarted) { \ + (aJSPromise) \ + ->MaybeRejectWithNotAllowedError( \ + "Shutting down and refusing additional I/O tasks"); \ + return (aJSPromise).forget(); \ + } \ + } while (false) + +#define REJECT_IF_INIT_PATH_FAILED(_file, _path, _promise) \ + do { \ + if (nsresult _rv = (_file)->InitWithPath((_path)); NS_FAILED(_rv)) { \ + (_promise)->MaybeRejectWithOperationError( \ + FormatErrorMessage(_rv, "Could not parse path (%s)", \ + NS_ConvertUTF16toUTF8(_path).get())); \ + return (_promise).forget(); \ + } \ + } while (0) + +namespace mozilla::dom { + +// static helper functions + +/** + * Platform-specific (e.g. Windows, Unix) implementations of XPCOM APIs may + * report I/O errors inconsistently. For convenience, this function will attempt + * to match a |nsresult| against known results which imply a file cannot be + * found. + * + * @see nsLocalFileWin.cpp + * @see nsLocalFileUnix.cpp + */ +static bool IsFileNotFound(nsresult aResult) { + return aResult == NS_ERROR_FILE_NOT_FOUND || + aResult == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST; +} +/** + * Like |IsFileNotFound|, but checks for known results that suggest a file + * is not a directory. + */ +static bool IsNotDirectory(nsresult aResult) { + return aResult == NS_ERROR_FILE_DESTINATION_NOT_DIR || + aResult == NS_ERROR_FILE_NOT_DIRECTORY; +} + +/** + * Formats an error message and appends the error name to the end. + */ +template <typename... Args> +static nsCString FormatErrorMessage(nsresult aError, const char* const aMessage, + Args... aArgs) { + nsPrintfCString msg(aMessage, aArgs...); + + if (const char* errName = GetStaticErrorName(aError)) { + msg.AppendPrintf(": %s", errName); + } else { + // In the exceptional case where there is no error name, print the literal + // integer value of the nsresult as an upper case hex value so it can be + // located easily in searchfox. + msg.AppendPrintf(": 0x%" PRIX32, static_cast<uint32_t>(aError)); + } + + return std::move(msg); +} + +static nsCString FormatErrorMessage(nsresult aError, + const char* const aMessage) { + const char* errName = GetStaticErrorName(aError); + if (errName) { + return nsPrintfCString("%s: %s", aMessage, errName); + } + // In the exceptional case where there is no error name, print the literal + // integer value of the nsresult as an upper case hex value so it can be + // located easily in searchfox. + return nsPrintfCString("%s: 0x%" PRIX32, aMessage, + static_cast<uint32_t>(aError)); +} + +MOZ_MUST_USE inline bool ToJSValue( + JSContext* aCx, const IOUtils::InternalFileInfo& aInternalFileInfo, + JS::MutableHandle<JS::Value> aValue) { + FileInfo info; + info.mPath.Construct(aInternalFileInfo.mPath); + info.mType.Construct(aInternalFileInfo.mType); + info.mSize.Construct(aInternalFileInfo.mSize); + info.mLastModified.Construct(aInternalFileInfo.mLastModified); + + if (aInternalFileInfo.mCreationTime.isSome()) { + info.mCreationTime.Construct(aInternalFileInfo.mCreationTime.ref()); + } + + info.mPermissions.Construct(aInternalFileInfo.mPermissions); + + return ToJSValue(aCx, info, aValue); +} + +// IOUtils implementation + +/* static */ +StaticDataMutex<StaticRefPtr<nsISerialEventTarget>> + IOUtils::sBackgroundEventTarget("sBackgroundEventTarget"); +/* static */ +StaticRefPtr<nsIAsyncShutdownClient> IOUtils::sBarrier; +/* static */ +Atomic<bool> IOUtils::sShutdownStarted = Atomic<bool>(false); + +/* static */ +template <typename OkT, typename Fn> +RefPtr<IOUtils::IOPromise<OkT>> IOUtils::RunOnBackgroundThread(Fn aFunc) { + nsCOMPtr<nsISerialEventTarget> bg = GetBackgroundEventTarget(); + if (!bg) { + return IOPromise<OkT>::CreateAndReject( + IOError(NS_ERROR_ABORT) + .WithMessage("Could not dispatch task to background thread"), + __func__); + } + + return InvokeAsync(bg, __func__, [func = std::move(aFunc)]() { + Result<OkT, IOError> result = func(); + if (result.isErr()) { + return IOPromise<OkT>::CreateAndReject(result.unwrapErr(), __func__); + } + return IOPromise<OkT>::CreateAndResolve(result.unwrap(), __func__); + }); +} + +/* static */ +template <typename OkT, typename Fn> +void IOUtils::RunOnBackgroundThreadAndResolve(Promise* aPromise, Fn aFunc) { + RunOnBackgroundThread<OkT, Fn>(std::move(aFunc)) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [promise = RefPtr(aPromise)](OkT&& ok) { + ResolveJSPromise(promise, std::forward<OkT>(ok)); + }, + [promise = RefPtr(aPromise)](const IOError& err) { + RejectJSPromise(promise, err); + }); +} + +/* static */ +already_AddRefed<Promise> IOUtils::Read(GlobalObject& aGlobal, + const nsAString& aPath, + const ReadOptions& aOptions) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr<Promise> promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + REJECT_IF_SHUTTING_DOWN(promise); + + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + Maybe<uint32_t> toRead = Nothing(); + if (!aOptions.mMaxBytes.IsNull()) { + if (aOptions.mMaxBytes.Value() == 0) { + // Resolve with an empty buffer. + nsTArray<uint8_t> arr(0); + promise->MaybeResolve(TypedArrayCreator<Uint8Array>(arr)); + return promise.forget(); + } + toRead.emplace(aOptions.mMaxBytes.Value()); + } + + RunOnBackgroundThreadAndResolve<JsBuffer>( + promise, + [file = std::move(file), toRead, decompress = aOptions.mDecompress]() { + return ReadSync(file, toRead, decompress, BufferKind::Uint8Array); + }); + return promise.forget(); +} + +/* static */ +already_AddRefed<Promise> IOUtils::ReadUTF8(GlobalObject& aGlobal, + const nsAString& aPath, + const ReadUTF8Options& aOptions) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr<Promise> promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + RunOnBackgroundThreadAndResolve<JsBuffer>( + promise, [file = std::move(file), decompress = aOptions.mDecompress]() { + return ReadUTF8Sync(file, decompress); + }); + + return promise.forget(); +} + +/* static */ +already_AddRefed<Promise> IOUtils::ReadJSON(GlobalObject& aGlobal, + const nsAString& aPath, + const ReadUTF8Options& aOptions) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr<Promise> promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + RunOnBackgroundThread<JsBuffer>([file, decompress = aOptions.mDecompress]() { + return ReadUTF8Sync(file, decompress); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [promise, file](JsBuffer&& aBuffer) { + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(promise->GetGlobalObject()))) { + promise->MaybeRejectWithUnknownError( + "Could not initialize JS API"); + return; + } + JSContext* cx = jsapi.cx(); + + JS::Rooted<JSString*> jsonStr( + cx, IOUtils::JsBuffer::IntoString(cx, std::move(aBuffer))); + if (!jsonStr) { + RejectJSPromise(promise, IOError(NS_ERROR_OUT_OF_MEMORY)); + return; + } + + JS::Rooted<JS::Value> val(cx); + if (!JS_ParseJSON(cx, jsonStr, &val)) { + JS::Rooted<JS::Value> exn(cx); + if (JS_GetPendingException(cx, &exn)) { + JS_ClearPendingException(cx); + promise->MaybeReject(exn); + } else { + RejectJSPromise( + promise, + IOError(NS_ERROR_DOM_UNKNOWN_ERR) + .WithMessage("ParseJSON threw an uncatchable exception " + "while parsing file(%s)", + file->HumanReadablePath().get())); + } + + return; + } + + promise->MaybeResolve(val); + }, + [promise](const IOError& aErr) { RejectJSPromise(promise, aErr); }); + + return promise.forget(); +} + +/* static */ +already_AddRefed<Promise> IOUtils::Write(GlobalObject& aGlobal, + const nsAString& aPath, + const Uint8Array& aData, + const WriteOptions& aOptions) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr<Promise> promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + REJECT_IF_SHUTTING_DOWN(promise); + + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + aData.ComputeState(); + auto buf = Buffer<uint8_t>::CopyFrom(Span(aData.Data(), aData.Length())); + if (buf.isNothing()) { + promise->MaybeRejectWithOperationError( + "Out of memory: Could not allocate buffer while writing to file"); + return promise.forget(); + } + + auto opts = InternalWriteOpts::FromBinding(aOptions); + if (opts.isErr()) { + RejectJSPromise(promise, opts.unwrapErr()); + return promise.forget(); + } + + RunOnBackgroundThreadAndResolve<uint32_t>( + promise, [file = std::move(file), buf = std::move(*buf), + opts = opts.unwrap()]() { return WriteSync(file, buf, opts); }); + + return promise.forget(); +} + +/* static */ +already_AddRefed<Promise> IOUtils::WriteUTF8(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aString, + const WriteOptions& aOptions) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr<Promise> promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + REJECT_IF_SHUTTING_DOWN(promise); + + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + auto opts = InternalWriteOpts::FromBinding(aOptions); + if (opts.isErr()) { + RejectJSPromise(promise, opts.unwrapErr()); + return promise.forget(); + } + + RunOnBackgroundThreadAndResolve<uint32_t>( + promise, [file = std::move(file), str = nsCString(aString), + opts = opts.unwrap()]() { + return WriteSync(file, AsBytes(Span(str)), opts); + }); + + return promise.forget(); +} + +static bool AppendJsonAsUtf8(const char16_t* aData, uint32_t aLen, void* aStr) { + nsCString* str = static_cast<nsCString*>(aStr); + return AppendUTF16toUTF8(Span<const char16_t>(aData, aLen), *str, fallible); +} + +/* static */ +already_AddRefed<Promise> IOUtils::WriteJSON(GlobalObject& aGlobal, + const nsAString& aPath, + JS::Handle<JS::Value> aValue, + const WriteOptions& aOptions) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr<Promise> promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + REJECT_IF_SHUTTING_DOWN(promise); + + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + auto opts = InternalWriteOpts::FromBinding(aOptions); + if (opts.isErr()) { + RejectJSPromise(promise, opts.unwrapErr()); + return promise.forget(); + } + + JSContext* cx = aGlobal.Context(); + JS::Rooted<JS::Value> rootedValue(cx, aValue); + nsCString utf8Str; + + if (!JS_Stringify(cx, &rootedValue, nullptr, JS::NullHandleValue, + AppendJsonAsUtf8, &utf8Str)) { + JS::Rooted<JS::Value> exn(cx, JS::UndefinedValue()); + if (JS_GetPendingException(cx, &exn)) { + JS_ClearPendingException(cx); + promise->MaybeReject(exn); + } else { + RejectJSPromise(promise, + IOError(NS_ERROR_DOM_UNKNOWN_ERR) + .WithMessage("Could not serialize object to JSON")); + } + return promise.forget(); + } + + RunOnBackgroundThreadAndResolve<uint32_t>( + promise, [file = std::move(file), utf8Str = std::move(utf8Str), + opts = opts.unwrap()]() { + return WriteSync(file, AsBytes(Span(utf8Str)), opts); + }); + + return promise.forget(); +} + +/* static */ +already_AddRefed<Promise> IOUtils::Move(GlobalObject& aGlobal, + const nsAString& aSourcePath, + const nsAString& aDestPath, + const MoveOptions& aOptions) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr<Promise> promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + REJECT_IF_SHUTTING_DOWN(promise); + + nsCOMPtr<nsIFile> sourceFile = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(sourceFile, aSourcePath, promise); + + nsCOMPtr<nsIFile> destFile = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(destFile, aDestPath, promise); + + RunOnBackgroundThreadAndResolve<Ok>( + promise, + [sourceFile = std::move(sourceFile), destFile = std::move(destFile), + noOverwrite = aOptions.mNoOverwrite]() { + return MoveSync(sourceFile, destFile, noOverwrite); + }); + + return promise.forget(); +} + +/* static */ +already_AddRefed<Promise> IOUtils::Remove(GlobalObject& aGlobal, + const nsAString& aPath, + const RemoveOptions& aOptions) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr<Promise> promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + REJECT_IF_SHUTTING_DOWN(promise); + + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + RunOnBackgroundThreadAndResolve<Ok>( + promise, [file = std::move(file), ignoreAbsent = aOptions.mIgnoreAbsent, + recursive = aOptions.mRecursive]() { + return RemoveSync(file, ignoreAbsent, recursive); + }); + + return promise.forget(); +} + +/* static */ +already_AddRefed<Promise> IOUtils::MakeDirectory( + GlobalObject& aGlobal, const nsAString& aPath, + const MakeDirectoryOptions& aOptions) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr<Promise> promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + REJECT_IF_SHUTTING_DOWN(promise); + + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + RunOnBackgroundThreadAndResolve<Ok>( + promise, + [file = std::move(file), createAncestors = aOptions.mCreateAncestors, + ignoreExisting = aOptions.mIgnoreExisting, + permissions = aOptions.mPermissions]() { + return MakeDirectorySync(file, createAncestors, ignoreExisting, + permissions); + }); + + return promise.forget(); +} + +already_AddRefed<Promise> IOUtils::Stat(GlobalObject& aGlobal, + const nsAString& aPath) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr<Promise> promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + REJECT_IF_SHUTTING_DOWN(promise); + + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + RunOnBackgroundThreadAndResolve<InternalFileInfo>( + promise, [file = std::move(file)]() { return StatSync(file); }); + + return promise.forget(); +} + +/* static */ +already_AddRefed<Promise> IOUtils::Copy(GlobalObject& aGlobal, + const nsAString& aSourcePath, + const nsAString& aDestPath, + const CopyOptions& aOptions) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr<Promise> promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + REJECT_IF_SHUTTING_DOWN(promise); + + nsCOMPtr<nsIFile> sourceFile = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(sourceFile, aSourcePath, promise); + + nsCOMPtr<nsIFile> destFile = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(destFile, aDestPath, promise); + + RunOnBackgroundThreadAndResolve<Ok>( + promise, + [sourceFile = std::move(sourceFile), destFile = std::move(destFile), + noOverwrite = aOptions.mNoOverwrite, recursive = aOptions.mRecursive]() { + return CopySync(sourceFile, destFile, noOverwrite, recursive); + }); + + return promise.forget(); +} + +/* static */ +already_AddRefed<Promise> IOUtils::Touch( + GlobalObject& aGlobal, const nsAString& aPath, + const Optional<int64_t>& aModification) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr<Promise> promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + Maybe<int64_t> newTime = Nothing(); + if (aModification.WasPassed()) { + newTime = Some(aModification.Value()); + } + + RunOnBackgroundThreadAndResolve<int64_t>( + promise, + [file = std::move(file), newTime]() { return TouchSync(file, newTime); }); + + return promise.forget(); +} + +/* static */ +already_AddRefed<Promise> IOUtils::GetChildren(GlobalObject& aGlobal, + const nsAString& aPath) { + MOZ_ASSERT(XRE_IsParentProcess()); + RefPtr<Promise> promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + RunOnBackgroundThreadAndResolve<nsTArray<nsString>>( + promise, [file = std::move(file)]() { return GetChildrenSync(file); }); + + return promise.forget(); +} + +/* static */ +already_AddRefed<Promise> IOUtils::SetPermissions(GlobalObject& aGlobal, + const nsAString& aPath, + const uint32_t aPermissions) { + MOZ_ASSERT(XRE_IsParentProcess()); + RefPtr<Promise> promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + RunOnBackgroundThreadAndResolve<Ok>( + promise, [file = std::move(file), permissions = aPermissions]() { + return SetPermissionsSync(file, permissions); + }); + + return promise.forget(); +} + +/* static */ +already_AddRefed<Promise> IOUtils::Exists(GlobalObject& aGlobal, + const nsAString& aPath) { + MOZ_ASSERT(XRE_IsParentProcess()); + RefPtr<Promise> promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + RunOnBackgroundThreadAndResolve<bool>( + promise, [file = std::move(file)]() { return ExistsSync(file); }); + + return promise.forget(); +} + +/* static */ +already_AddRefed<nsISerialEventTarget> IOUtils::GetBackgroundEventTarget() { + if (sShutdownStarted) { + return nullptr; + } + + auto lockedBackgroundEventTarget = sBackgroundEventTarget.Lock(); + if (!lockedBackgroundEventTarget.ref()) { + nsCOMPtr<nsISerialEventTarget> et; + MOZ_ALWAYS_SUCCEEDS(NS_CreateBackgroundTaskQueue( + "IOUtils::BackgroundIOThread", getter_AddRefs(et))); + MOZ_ASSERT(et); + *lockedBackgroundEventTarget = et; + + if (NS_IsMainThread()) { + IOUtils::SetShutdownHooks(); + } else { + nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction( + __func__, []() { IOUtils::SetShutdownHooks(); }); + NS_DispatchToMainThread(runnable.forget()); + } + } + return do_AddRef(*lockedBackgroundEventTarget); +} + +/* static */ +already_AddRefed<nsIAsyncShutdownClient> IOUtils::GetShutdownBarrier() { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + + if (!sBarrier) { + nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdownService(); + MOZ_ASSERT(svc); + + nsCOMPtr<nsIAsyncShutdownClient> barrier; + nsresult rv = svc->GetProfileBeforeChange(getter_AddRefs(barrier)); + NS_ENSURE_SUCCESS(rv, nullptr); + sBarrier = barrier; + } + return do_AddRef(sBarrier); +} + +/* static */ +void IOUtils::SetShutdownHooks() { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier(); + nsCOMPtr<nsIAsyncShutdownBlocker> blocker = new IOUtilsShutdownBlocker(); + + nsresult rv = barrier->AddBlocker( + blocker, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, + u"IOUtils: waiting for pending I/O to finish"_ns); + // Adding a new shutdown blocker should only fail if the current shutdown + // phase has completed. Ensure that we have set our shutdown flag to stop + // accepting new I/O tasks in this case. + if (NS_FAILED(rv)) { + sShutdownStarted = true; + } + NS_ENSURE_SUCCESS_VOID(rv); +} + +/* static */ +already_AddRefed<Promise> IOUtils::CreateJSPromise(GlobalObject& aGlobal) { + ErrorResult er; + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<Promise> promise = Promise::Create(global, er); + if (er.Failed()) { + return nullptr; + } + MOZ_ASSERT(promise); + return do_AddRef(promise); +} + +/* static */ +template <typename T> +void IOUtils::ResolveJSPromise(Promise* aPromise, T&& aValue) { + if constexpr (std::is_same_v<T, Ok>) { + aPromise->MaybeResolveWithUndefined(); + } else { + aPromise->MaybeResolve(std::forward<T>(aValue)); + } +} + +/* static */ +void IOUtils::RejectJSPromise(Promise* aPromise, const IOError& aError) { + const auto& errMsg = aError.Message(); + + switch (aError.Code()) { + case NS_ERROR_FILE_TARGET_DOES_NOT_EXIST: + case NS_ERROR_FILE_NOT_FOUND: + aPromise->MaybeRejectWithNotFoundError(errMsg.refOr("File not found"_ns)); + break; + case NS_ERROR_FILE_ACCESS_DENIED: + aPromise->MaybeRejectWithNotAllowedError( + errMsg.refOr("Access was denied to the target file"_ns)); + break; + case NS_ERROR_FILE_TOO_BIG: + aPromise->MaybeRejectWithNotReadableError( + errMsg.refOr("Target file is too big"_ns)); + break; + case NS_ERROR_FILE_ALREADY_EXISTS: + aPromise->MaybeRejectWithNoModificationAllowedError( + errMsg.refOr("Target file already exists"_ns)); + break; + case NS_ERROR_FILE_COPY_OR_MOVE_FAILED: + aPromise->MaybeRejectWithOperationError( + errMsg.refOr("Failed to copy or move the target file"_ns)); + break; + case NS_ERROR_FILE_READ_ONLY: + aPromise->MaybeRejectWithReadOnlyError( + errMsg.refOr("Target file is read only"_ns)); + break; + case NS_ERROR_FILE_NOT_DIRECTORY: + case NS_ERROR_FILE_DESTINATION_NOT_DIR: + aPromise->MaybeRejectWithInvalidAccessError( + errMsg.refOr("Target file is not a directory"_ns)); + break; + case NS_ERROR_FILE_UNRECOGNIZED_PATH: + aPromise->MaybeRejectWithOperationError( + errMsg.refOr("Target file path is not recognized"_ns)); + break; + case NS_ERROR_FILE_DIR_NOT_EMPTY: + aPromise->MaybeRejectWithOperationError( + errMsg.refOr("Target directory is not empty"_ns)); + break; + case NS_ERROR_FILE_CORRUPTED: + aPromise->MaybeRejectWithNotReadableError( + errMsg.refOr("Target file could not be read and may be corrupt"_ns)); + break; + case NS_ERROR_ILLEGAL_INPUT: + case NS_ERROR_ILLEGAL_VALUE: + aPromise->MaybeRejectWithDataError( + errMsg.refOr("Argument is not allowed"_ns)); + break; + case NS_ERROR_ABORT: + aPromise->MaybeRejectWithAbortError(errMsg.refOr("Operation aborted"_ns)); + break; + default: + aPromise->MaybeRejectWithUnknownError( + errMsg.refOr(FormatErrorMessage(aError.Code(), "Unexpected error"))); + } +} + +/* static */ +Result<IOUtils::JsBuffer, IOUtils::IOError> IOUtils::ReadSync( + nsIFile* aFile, const Maybe<uint32_t>& aMaxBytes, const bool aDecompress, + IOUtils::BufferKind aBufferKind) { + MOZ_ASSERT(!NS_IsMainThread()); + + if (aMaxBytes.isSome() && aDecompress) { + return Err( + IOError(NS_ERROR_ILLEGAL_INPUT) + .WithMessage( + "The `maxBytes` and `decompress` options are not compatible")); + } + + RefPtr<nsFileStream> stream = new nsFileStream(); + if (nsresult rv = + stream->Init(aFile, PR_RDONLY | nsIFile::OS_READAHEAD, 0666, 0); + NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage("Could not open the file at %s", + aFile->HumanReadablePath().get())); + } + int64_t bufSize = 0; + + if (aMaxBytes.isNothing()) { + // Limitation: We cannot read files that are larger than the max size of a + // TypedArray (UINT32_MAX bytes). Reject if the file is too + // big to be read. + + int64_t streamSize = -1; + if (nsresult rv = stream->GetSize(&streamSize); NS_FAILED(rv)) { + return Err(IOError(NS_ERROR_FILE_ACCESS_DENIED) + .WithMessage("Could not get info for the file at %s", + aFile->HumanReadablePath().get())); + } + MOZ_RELEASE_ASSERT(streamSize >= 0); + + if (streamSize > static_cast<int64_t>(UINT32_MAX)) { + return Err( + IOError(NS_ERROR_FILE_TOO_BIG) + .WithMessage("Could not read the file at %s because it is too " + "large(size=%" PRId64 " bytes)", + aFile->HumanReadablePath().get(), streamSize)); + } + bufSize = static_cast<uint32_t>(streamSize); + } else { + bufSize = aMaxBytes.value(); + } + + JsBuffer buffer = JsBuffer::CreateEmpty(aBufferKind); + + if (bufSize > 0) { + auto result = JsBuffer::Create(aBufferKind, bufSize); + if (result.isErr()) { + return result.propagateErr(); + } + buffer = result.unwrap(); + Span<char> toRead = buffer.BeginWriting(); + + // Read the file from disk. + uint32_t totalRead = 0; + while (totalRead != bufSize) { + uint32_t bytesRead = 0; + if (nsresult rv = + stream->Read(toRead.Elements(), bufSize - totalRead, &bytesRead); + NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Encountered an unexpected error while reading file(%s)", + aFile->HumanReadablePath().get())); + } + if (bytesRead == 0) { + break; + } + totalRead += bytesRead; + toRead = toRead.From(bytesRead); + } + + buffer.SetLength(totalRead); + } + + // Decompress the file contents, if required. + if (aDecompress) { + return MozLZ4::Decompress(AsBytes(buffer.BeginReading()), aBufferKind); + } + + return std::move(buffer); +} + +/* static */ +Result<IOUtils::JsBuffer, IOUtils::IOError> IOUtils::ReadUTF8Sync( + nsIFile* aFile, bool aDecompress) { + auto result = ReadSync(aFile, Nothing{}, aDecompress, BufferKind::String); + if (result.isErr()) { + return result.propagateErr(); + } + + JsBuffer buffer = result.unwrap(); + if (!IsUtf8(buffer.BeginReading())) { + return Err( + IOError(NS_ERROR_FILE_CORRUPTED) + .WithMessage( + "Could not read file(%s) because it is not UTF-8 encoded", + aFile->HumanReadablePath().get())); + } + + return buffer; +} + +/* static */ +Result<uint32_t, IOUtils::IOError> IOUtils::WriteSync( + nsIFile* aFile, const Span<const uint8_t>& aByteArray, + const IOUtils::InternalWriteOpts& aOptions) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsIFile* backupFile = aOptions.mBackupFile; + nsIFile* tempFile = aOptions.mTmpFile; + + bool exists = false; + MOZ_TRY(aFile->Exists(&exists)); + + if (aOptions.mNoOverwrite && exists) { + return Err(IOError(NS_ERROR_DOM_TYPE_MISMATCH_ERR) + .WithMessage("Refusing to overwrite the file at %s\n" + "Specify `noOverwrite: false` to allow " + "overwriting the destination", + aFile->HumanReadablePath().get())); + } + + // If backupFile was specified, perform the backup as a move. + if (exists && backupFile) { + // We copy `destFile` here to a new `nsIFile` because + // `nsIFile::MoveToFollowingLinks` will update the path of the file. If we + // did not do this, we would end up having `destFile` point to the same + // location as `backupFile`. Then, when we went to write to `destFile`, we + // would end up overwriting `backupFile` and never actually write to the + // file we were supposed to. + nsCOMPtr<nsIFile> toMove; + MOZ_ALWAYS_SUCCEEDS(aFile->Clone(getter_AddRefs(toMove))); + + if (MoveSync(toMove, backupFile, aOptions.mNoOverwrite).isErr()) { + return Err(IOError(NS_ERROR_FILE_COPY_OR_MOVE_FAILED) + .WithMessage("Failed to backup the source file(%s) to %s", + aFile->HumanReadablePath().get(), + backupFile->HumanReadablePath().get())); + } + } + + // If tempFile was specified, we will write to there first, then perform a + // move to ensure the file ends up at the final requested destination. + nsIFile* writeFile; + + if (tempFile) { + writeFile = tempFile; + } else { + writeFile = aFile; + } + + int32_t flags = PR_WRONLY | PR_TRUNCATE | PR_CREATE_FILE; + if (aOptions.mFlush) { + flags |= PR_SYNC; + } + + // Try to perform the write and ensure that the file is closed before + // continuing. + uint32_t totalWritten = 0; + { + // Compress the byte array if required. + nsTArray<uint8_t> compressed; + Span<const char> bytes; + if (aOptions.mCompress) { + auto rv = MozLZ4::Compress(aByteArray); + if (rv.isErr()) { + return rv.propagateErr(); + } + compressed = rv.unwrap(); + bytes = Span(reinterpret_cast<const char*>(compressed.Elements()), + compressed.Length()); + } else { + bytes = Span(reinterpret_cast<const char*>(aByteArray.Elements()), + aByteArray.Length()); + } + + RefPtr<nsFileOutputStream> stream = new nsFileOutputStream(); + if (nsresult rv = stream->Init(writeFile, flags, 0666, 0); NS_FAILED(rv)) { + return Err( + IOError(rv).WithMessage("Could not open the file at %s for writing", + writeFile->HumanReadablePath().get())); + } + + // nsFileStream::Write uses PR_Write under the hood, which accepts a + // *int32_t* for the chunk size. + uint32_t chunkSize = INT32_MAX; + Span<const char> pendingBytes = bytes; + + while (pendingBytes.Length() > 0) { + if (pendingBytes.Length() < chunkSize) { + chunkSize = pendingBytes.Length(); + } + + uint32_t bytesWritten = 0; + if (nsresult rv = + stream->Write(pendingBytes.Elements(), chunkSize, &bytesWritten); + NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Could not write chunk (size = %" PRIu32 + ") to file %s. The file may be corrupt.", + chunkSize, writeFile->HumanReadablePath().get())); + } + pendingBytes = pendingBytes.From(bytesWritten); + totalWritten += bytesWritten; + } + } + + // If tempFile was passed, check destFile against writeFile and, if they + // differ, the operation is finished by performing a move. + if (tempFile) { + nsAutoStringN<256> destPath; + nsAutoStringN<256> writePath; + + MOZ_ALWAYS_SUCCEEDS(aFile->GetPath(destPath)); + MOZ_ALWAYS_SUCCEEDS(writeFile->GetPath(writePath)); + + // nsIFile::MoveToFollowingLinks will only update the path of the file if + // the move succeeds. + if (destPath != writePath && MoveSync(writeFile, aFile, false).isErr()) { + return Err(IOError(NS_ERROR_FILE_COPY_OR_MOVE_FAILED) + .WithMessage( + "Could not move temporary file(%s) to destination(%s)", + writeFile->HumanReadablePath().get(), + aFile->HumanReadablePath().get())); + } + } + return totalWritten; +} + +/* static */ +Result<Ok, IOUtils::IOError> IOUtils::MoveSync(nsIFile* aSourceFile, + nsIFile* aDestFile, + bool aNoOverwrite) { + MOZ_ASSERT(!NS_IsMainThread()); + + // Ensure the source file exists before continuing. If it doesn't exist, + // subsequent operations can fail in different ways on different platforms. + bool srcExists = false; + MOZ_TRY(aSourceFile->Exists(&srcExists)); + if (!srcExists) { + return Err( + IOError(NS_ERROR_FILE_NOT_FOUND) + .WithMessage( + "Could not move source file(%s) because it does not exist", + aSourceFile->HumanReadablePath().get())); + } + + return CopyOrMoveSync(&nsIFile::MoveToFollowingLinks, "move", aSourceFile, + aDestFile, aNoOverwrite); +} + +/* static */ +Result<Ok, IOUtils::IOError> IOUtils::CopySync(nsIFile* aSourceFile, + nsIFile* aDestFile, + bool aNoOverwrite, + bool aRecursive) { + MOZ_ASSERT(!NS_IsMainThread()); + + // Ensure the source file exists before continuing. If it doesn't exist, + // subsequent operations can fail in different ways on different platforms. + bool srcExists; + MOZ_TRY(aSourceFile->Exists(&srcExists)); + if (!srcExists) { + return Err( + IOError(NS_ERROR_FILE_NOT_FOUND) + .WithMessage( + "Could not copy source file(%s) because it does not exist", + aSourceFile->HumanReadablePath().get())); + } + + // If source is a directory, fail immediately unless the recursive option is + // true. + bool srcIsDir = false; + MOZ_TRY(aSourceFile->IsDirectory(&srcIsDir)); + if (srcIsDir && !aRecursive) { + return Err( + IOError(NS_ERROR_FILE_COPY_OR_MOVE_FAILED) + .WithMessage( + "Refused to copy source directory(%s) to the destination(%s)\n" + "Specify the `recursive: true` option to allow copying " + "directories", + aSourceFile->HumanReadablePath().get(), + aDestFile->HumanReadablePath().get())); + } + + return CopyOrMoveSync(&nsIFile::CopyToFollowingLinks, "copy", aSourceFile, + aDestFile, aNoOverwrite); +} + +/* static */ +template <typename CopyOrMoveFn> +Result<Ok, IOUtils::IOError> IOUtils::CopyOrMoveSync(CopyOrMoveFn aMethod, + const char* aMethodName, + nsIFile* aSource, + nsIFile* aDest, + bool aNoOverwrite) { + MOZ_ASSERT(!NS_IsMainThread()); + + // Case 1: Destination is an existing directory. Copy/move source into dest. + bool destIsDir = false; + bool destExists = true; + + nsresult rv = aDest->IsDirectory(&destIsDir); + if (NS_SUCCEEDED(rv) && destIsDir) { + rv = (aSource->*aMethod)(aDest, u""_ns); + if (NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Could not %s source file(%s) to destination directory(%s)", + aMethodName, aSource->HumanReadablePath().get(), + aDest->HumanReadablePath().get())); + } + return Ok(); + } + + if (NS_FAILED(rv)) { + if (!IsFileNotFound(rv)) { + // It's ok if the dest file doesn't exist. Case 2 handles this below. + // Bail out early for any other kind of error though. + return Err(IOError(rv)); + } + destExists = false; + } + + // Case 2: Destination is a file which may or may not exist. + // Try to copy or rename the source to the destination. + // If the destination exists and the source is not a regular file, + // then this may fail. + if (aNoOverwrite && destExists) { + return Err( + IOError(NS_ERROR_FILE_ALREADY_EXISTS) + .WithMessage( + "Could not %s source file(%s) to destination(%s) because the " + "destination already exists and overwrites are not allowed\n" + "Specify the `noOverwrite: false` option to mitigate this " + "error", + aMethodName, aSource->HumanReadablePath().get(), + aDest->HumanReadablePath().get())); + } + if (destExists && !destIsDir) { + // If the source file is a directory, but the target is a file, abort early. + // Different implementations of |CopyTo| and |MoveTo| seem to handle this + // error case differently (or not at all), so we explicitly handle it here. + bool srcIsDir = false; + MOZ_TRY(aSource->IsDirectory(&srcIsDir)); + if (srcIsDir) { + return Err(IOError(NS_ERROR_FILE_DESTINATION_NOT_DIR) + .WithMessage("Could not %s the source directory(%s) to " + "the destination(%s) because the destination " + "is not a directory", + aMethodName, + aSource->HumanReadablePath().get(), + aDest->HumanReadablePath().get())); + } + } + + nsCOMPtr<nsIFile> destDir; + nsAutoString destName; + MOZ_TRY(aDest->GetLeafName(destName)); + MOZ_TRY(aDest->GetParent(getter_AddRefs(destDir))); + + // NB: if destDir doesn't exist, then |CopyToFollowingLinks| or + // |MoveToFollowingLinks| will create it. + rv = (aSource->*aMethod)(destDir, destName); + if (NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Could not %s the source file(%s) to the destination(%s)", aMethodName, + aSource->HumanReadablePath().get(), aDest->HumanReadablePath().get())); + } + return Ok(); +} + +/* static */ +Result<Ok, IOUtils::IOError> IOUtils::RemoveSync(nsIFile* aFile, + bool aIgnoreAbsent, + bool aRecursive) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsresult rv = aFile->Remove(aRecursive); + if (aIgnoreAbsent && IsFileNotFound(rv)) { + return Ok(); + } + if (NS_FAILED(rv)) { + IOError err(rv); + if (IsFileNotFound(rv)) { + return Err(err.WithMessage( + "Could not remove the file at %s because it does not exist.\n" + "Specify the `ignoreAbsent: true` option to mitigate this error", + aFile->HumanReadablePath().get())); + } + if (rv == NS_ERROR_FILE_DIR_NOT_EMPTY) { + return Err(err.WithMessage( + "Could not remove the non-empty directory at %s.\n" + "Specify the `recursive: true` option to mitigate this error", + aFile->HumanReadablePath().get())); + } + return Err(err.WithMessage("Could not remove the file at %s", + aFile->HumanReadablePath().get())); + } + return Ok(); +} + +/* static */ +Result<Ok, IOUtils::IOError> IOUtils::MakeDirectorySync(nsIFile* aFile, + bool aCreateAncestors, + bool aIgnoreExisting, + int32_t aMode) { + MOZ_ASSERT(!NS_IsMainThread()); + + // nsIFile::Create will create ancestor directories by default. + // If the caller does not want this behaviour, then check and possibly + // return an error. + if (!aCreateAncestors) { + nsCOMPtr<nsIFile> parent; + MOZ_TRY(aFile->GetParent(getter_AddRefs(parent))); + bool parentExists = false; + MOZ_TRY(parent->Exists(&parentExists)); + if (!parentExists) { + return Err(IOError(NS_ERROR_FILE_NOT_FOUND) + .WithMessage("Could not create directory at %s because " + "the path has missing " + "ancestor components", + aFile->HumanReadablePath().get())); + } + } + + nsresult rv = aFile->Create(nsIFile::DIRECTORY_TYPE, aMode); + if (NS_FAILED(rv)) { + if (rv == NS_ERROR_FILE_ALREADY_EXISTS) { + // NB: We may report a success only if the target is an existing + // directory. We don't want to silence errors that occur if the target is + // an existing file, since trying to create a directory where a regular + // file exists may be indicative of a logic error. + bool isDirectory; + MOZ_TRY(aFile->IsDirectory(&isDirectory)); + if (!isDirectory) { + return Err(IOError(NS_ERROR_FILE_NOT_DIRECTORY) + .WithMessage("Could not create directory because the " + "target file(%s) exists " + "and is not a directory", + aFile->HumanReadablePath().get())); + } + // The directory exists. + // The caller may suppress this error. + if (aIgnoreExisting) { + return Ok(); + } + // Otherwise, forward it. + return Err(IOError(rv).WithMessage( + "Could not create directory because it already exists at %s\n" + "Specify the `ignoreExisting: true` option to mitigate this " + "error", + aFile->HumanReadablePath().get())); + } + return Err(IOError(rv).WithMessage("Could not create directory at %s", + aFile->HumanReadablePath().get())); + } + return Ok(); +} + +Result<IOUtils::InternalFileInfo, IOUtils::IOError> IOUtils::StatSync( + nsIFile* aFile) { + MOZ_ASSERT(!NS_IsMainThread()); + + InternalFileInfo info; + MOZ_ALWAYS_SUCCEEDS(aFile->GetPath(info.mPath)); + + bool isRegular = false; + // IsFile will stat and cache info in the file object. If the file doesn't + // exist, or there is an access error, we'll discover it here. + // Any subsequent errors are unexpected and will just be forwarded. + nsresult rv = aFile->IsFile(&isRegular); + if (NS_FAILED(rv)) { + IOError err(rv); + if (IsFileNotFound(rv)) { + return Err( + err.WithMessage("Could not stat file(%s) because it does not exist", + aFile->HumanReadablePath().get())); + } + return Err(err); + } + + // Now we can populate the info object by querying the file. + info.mType = FileType::Regular; + if (!isRegular) { + bool isDir = false; + MOZ_TRY(aFile->IsDirectory(&isDir)); + info.mType = isDir ? FileType::Directory : FileType::Other; + } + + int64_t size = -1; + if (info.mType == FileType::Regular) { + MOZ_TRY(aFile->GetFileSize(&size)); + } + info.mSize = size; + PRTime lastModified = 0; + MOZ_TRY(aFile->GetLastModifiedTime(&lastModified)); + info.mLastModified = static_cast<int64_t>(lastModified); + + PRTime creationTime = 0; + if (nsresult rv = aFile->GetCreationTime(&creationTime); NS_SUCCEEDED(rv)) { + info.mCreationTime.emplace(static_cast<int64_t>(creationTime)); + } else if (NS_FAILED(rv) && rv != NS_ERROR_NOT_IMPLEMENTED) { + // This field is only supported on some platforms. + return Err(IOError(rv)); + } + + MOZ_TRY(aFile->GetPermissions(&info.mPermissions)); + + return info; +} + +/* static */ +Result<int64_t, IOUtils::IOError> IOUtils::TouchSync( + nsIFile* aFile, const Maybe<int64_t>& aNewModTime) { + MOZ_ASSERT(!NS_IsMainThread()); + + int64_t now = aNewModTime.valueOrFrom([]() { + // NB: PR_Now reports time in microseconds since the Unix epoch + // (1970-01-01T00:00:00Z). Both nsLocalFile's lastModifiedTime and + // JavaScript's Date primitive values are to be expressed in + // milliseconds since Epoch. + int64_t nowMicros = PR_Now(); + int64_t nowMillis = nowMicros / PR_USEC_PER_MSEC; + return nowMillis; + }); + + // nsIFile::SetLastModifiedTime will *not* do what is expected when passed 0 + // as an argument. Rather than setting the time to 0, it will recalculate the + // system time and set it to that value instead. We explicit forbid this, + // because this side effect is surprising. + // + // If it ever becomes possible to set a file time to 0, this check should be + // removed, though this use case seems rare. + if (now == 0) { + return Err( + IOError(NS_ERROR_ILLEGAL_VALUE) + .WithMessage( + "Refusing to set the modification time of file(%s) to 0.\n" + "To use the current system time, call `touch` with no " + "arguments", + aFile->HumanReadablePath().get())); + } + + nsresult rv = aFile->SetLastModifiedTime(now); + + if (NS_FAILED(rv)) { + IOError err(rv); + if (IsFileNotFound(rv)) { + return Err( + err.WithMessage("Could not touch file(%s) because it does not exist", + aFile->HumanReadablePath().get())); + } + return Err(err); + } + return now; +} + +/* static */ +Result<nsTArray<nsString>, IOUtils::IOError> IOUtils::GetChildrenSync( + nsIFile* aFile) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<nsIDirectoryEnumerator> iter; + nsresult rv = aFile->GetDirectoryEntries(getter_AddRefs(iter)); + if (NS_FAILED(rv)) { + IOError err(rv); + if (IsFileNotFound(rv)) { + return Err(err.WithMessage( + "Could not get children of file(%s) because it does not exist", + aFile->HumanReadablePath().get())); + } + if (IsNotDirectory(rv)) { + return Err(err.WithMessage( + "Could not get children of file(%s) because it is not a directory", + aFile->HumanReadablePath().get())); + } + return Err(err); + } + nsTArray<nsString> children; + + bool hasMoreElements = false; + MOZ_TRY(iter->HasMoreElements(&hasMoreElements)); + while (hasMoreElements) { + nsCOMPtr<nsIFile> child; + MOZ_TRY(iter->GetNextFile(getter_AddRefs(child))); + if (child) { + nsString path; + MOZ_TRY(child->GetPath(path)); + children.AppendElement(path); + } + MOZ_TRY(iter->HasMoreElements(&hasMoreElements)); + } + + return children; +} + +/* static */ +Result<Ok, IOUtils::IOError> IOUtils::SetPermissionsSync( + nsIFile* aFile, const uint32_t aPermissions) { + MOZ_ASSERT(!NS_IsMainThread()); + + MOZ_TRY(aFile->SetPermissions(aPermissions)); + return Ok{}; +} + +/* static */ +Result<bool, IOUtils::IOError> IOUtils::ExistsSync(nsIFile* aFile) { + MOZ_ASSERT(!NS_IsMainThread()); + + bool exists = false; + MOZ_TRY(aFile->Exists(&exists)); + + return exists; +} + +/* static */ +Result<nsTArray<uint8_t>, IOUtils::IOError> IOUtils::MozLZ4::Compress( + Span<const uint8_t> aUncompressed) { + nsTArray<uint8_t> result; + size_t worstCaseSize = + Compression::LZ4::maxCompressedSize(aUncompressed.Length()) + HEADER_SIZE; + if (!result.SetCapacity(worstCaseSize, fallible)) { + return Err(IOError(NS_ERROR_OUT_OF_MEMORY) + .WithMessage("Could not allocate buffer to compress data")); + } + result.AppendElements(Span(MAGIC_NUMBER.data(), MAGIC_NUMBER.size())); + std::array<uint8_t, sizeof(uint32_t)> contentSizeBytes{}; + LittleEndian::writeUint32(contentSizeBytes.data(), aUncompressed.Length()); + result.AppendElements(Span(contentSizeBytes.data(), contentSizeBytes.size())); + + if (aUncompressed.Length() == 0) { + // Don't try to compress an empty buffer. + // Just return the correctly formed header. + result.SetLength(HEADER_SIZE); + return result; + } + + size_t compressed = Compression::LZ4::compress( + reinterpret_cast<const char*>(aUncompressed.Elements()), + aUncompressed.Length(), + reinterpret_cast<char*>(result.Elements()) + HEADER_SIZE); + if (!compressed) { + return Err( + IOError(NS_ERROR_UNEXPECTED).WithMessage("Could not compress data")); + } + result.SetLength(HEADER_SIZE + compressed); + return result; +} + +/* static */ +Result<IOUtils::JsBuffer, IOUtils::IOError> IOUtils::MozLZ4::Decompress( + Span<const uint8_t> aFileContents, IOUtils::BufferKind aBufferKind) { + if (aFileContents.LengthBytes() < HEADER_SIZE) { + return Err( + IOError(NS_ERROR_FILE_CORRUPTED) + .WithMessage( + "Could not decompress file because the buffer is too short")); + } + auto header = aFileContents.To(HEADER_SIZE); + if (!std::equal(std::begin(MAGIC_NUMBER), std::end(MAGIC_NUMBER), + std::begin(header))) { + nsCString magicStr; + uint32_t i = 0; + for (; i < header.Length() - 1; ++i) { + magicStr.AppendPrintf("%02X ", header.at(i)); + } + magicStr.AppendPrintf("%02X", header.at(i)); + + return Err(IOError(NS_ERROR_FILE_CORRUPTED) + .WithMessage("Could not decompress file because it has an " + "invalid LZ4 header (wrong magic number: '%s')", + magicStr.get())); + } + size_t numBytes = sizeof(uint32_t); + Span<const uint8_t> sizeBytes = header.Last(numBytes); + uint32_t expectedDecompressedSize = + LittleEndian::readUint32(sizeBytes.data()); + if (expectedDecompressedSize == 0) { + return JsBuffer::CreateEmpty(aBufferKind); + } + auto contents = aFileContents.From(HEADER_SIZE); + auto result = JsBuffer::Create(aBufferKind, expectedDecompressedSize); + if (result.isErr()) { + return result.propagateErr(); + } + + JsBuffer decompressed = result.unwrap(); + size_t actualSize = 0; + if (!Compression::LZ4::decompress( + reinterpret_cast<const char*>(contents.Elements()), contents.Length(), + reinterpret_cast<char*>(decompressed.Elements()), + expectedDecompressedSize, &actualSize)) { + return Err( + IOError(NS_ERROR_FILE_CORRUPTED) + .WithMessage( + "Could not decompress file contents, the file may be corrupt")); + } + decompressed.SetLength(actualSize); + return decompressed; +} + +NS_IMPL_ISUPPORTS(IOUtilsShutdownBlocker, nsIAsyncShutdownBlocker); + +NS_IMETHODIMP IOUtilsShutdownBlocker::GetName(nsAString& aName) { + aName = u"IOUtils Blocker"_ns; + return NS_OK; +} + +NS_IMETHODIMP IOUtilsShutdownBlocker::BlockShutdown( + nsIAsyncShutdownClient* aBarrierClient) { + nsCOMPtr<nsISerialEventTarget> et = IOUtils::GetBackgroundEventTarget(); + + IOUtils::sShutdownStarted = true; + + if (!IOUtils::sBarrier) { + return NS_ERROR_NULL_POINTER; + } + + nsCOMPtr<nsIRunnable> backgroundRunnable = + NS_NewRunnableFunction(__func__, [self = RefPtr(this)]() { + nsCOMPtr<nsIRunnable> mainThreadRunnable = + NS_NewRunnableFunction(__func__, [self = RefPtr(self)]() { + IOUtils::sBarrier->RemoveBlocker(self); + + auto lockedBackgroundET = IOUtils::sBackgroundEventTarget.Lock(); + *lockedBackgroundET = nullptr; + IOUtils::sBarrier = nullptr; + }); + nsresult rv = NS_DispatchToMainThread(mainThreadRunnable.forget()); + NS_ENSURE_SUCCESS_VOID(rv); + }); + + return et->Dispatch(backgroundRunnable.forget(), + nsIEventTarget::DISPATCH_NORMAL); +} + +NS_IMETHODIMP IOUtilsShutdownBlocker::GetState(nsIPropertyBag** aState) { + return NS_OK; +} + +Result<IOUtils::InternalWriteOpts, IOUtils::IOError> +IOUtils::InternalWriteOpts::FromBinding(const WriteOptions& aOptions) { + InternalWriteOpts opts; + opts.mFlush = aOptions.mFlush; + opts.mNoOverwrite = aOptions.mNoOverwrite; + + if (aOptions.mBackupFile.WasPassed()) { + opts.mBackupFile = new nsLocalFile(); + if (nsresult rv = + opts.mBackupFile->InitWithPath(aOptions.mBackupFile.Value()); + NS_FAILED(rv)) { + return Err(IOUtils::IOError(rv).WithMessage( + "Could not parse path of backupFile (%s)", + NS_ConvertUTF16toUTF8(aOptions.mBackupFile.Value()).get())); + } + } + + if (aOptions.mTmpPath.WasPassed()) { + opts.mTmpFile = new nsLocalFile(); + if (nsresult rv = opts.mTmpFile->InitWithPath(aOptions.mTmpPath.Value()); + NS_FAILED(rv)) { + return Err(IOUtils::IOError(rv).WithMessage( + "Could not parse path of temp file (%s)", + NS_ConvertUTF16toUTF8(aOptions.mTmpPath.Value()).get())); + } + } + + opts.mCompress = aOptions.mCompress; + return opts; +} + +/* static */ +Result<IOUtils::JsBuffer, IOUtils::IOError> IOUtils::JsBuffer::Create( + IOUtils::BufferKind aBufferKind, size_t aCapacity) { + JsBuffer buffer(aBufferKind, aCapacity); + if (aCapacity != 0 && !buffer.mBuffer) { + return Err(IOError(NS_ERROR_OUT_OF_MEMORY) + .WithMessage("Could not allocate buffer")); + } + return buffer; +} + +/* static */ +IOUtils::JsBuffer IOUtils::JsBuffer::CreateEmpty( + IOUtils::BufferKind aBufferKind) { + JsBuffer buffer(aBufferKind, 0); + MOZ_RELEASE_ASSERT(buffer.mBuffer == nullptr); + return buffer; +} + +IOUtils::JsBuffer::JsBuffer(IOUtils::BufferKind aBufferKind, size_t aCapacity) + : mBufferKind(aBufferKind), mCapacity(aCapacity), mLength(0) { + if (mCapacity) { + if (aBufferKind == BufferKind::String) { + mBuffer = JS::UniqueChars( + js_pod_arena_malloc<char>(js::StringBufferArena, mCapacity)); + } else { + MOZ_RELEASE_ASSERT(aBufferKind == BufferKind::Uint8Array); + mBuffer = JS::UniqueChars( + js_pod_arena_malloc<char>(js::ArrayBufferContentsArena, mCapacity)); + } + } +} + +IOUtils::JsBuffer::JsBuffer(IOUtils::JsBuffer&& aOther) noexcept + : mBufferKind(aOther.mBufferKind), + mCapacity(aOther.mCapacity), + mLength(aOther.mLength), + mBuffer(std::move(aOther.mBuffer)) { + aOther.mCapacity = 0; + aOther.mLength = 0; +} + +IOUtils::JsBuffer& IOUtils::JsBuffer::operator=( + IOUtils::JsBuffer&& aOther) noexcept { + mBufferKind = aOther.mBufferKind; + mCapacity = aOther.mCapacity; + mLength = aOther.mLength; + mBuffer = std::move(aOther.mBuffer); + + // Invalidate aOther. + aOther.mCapacity = 0; + aOther.mLength = 0; + + return *this; +} + +/* static */ +JSString* IOUtils::JsBuffer::IntoString(JSContext* aCx, JsBuffer aBuffer) { + MOZ_RELEASE_ASSERT(aBuffer.mBufferKind == IOUtils::BufferKind::String); + + if (!aBuffer.mCapacity) { + return JS_GetEmptyString(aCx); + } + + if (IsAscii(aBuffer.BeginReading())) { + // If the string is just plain ASCII, then we can hand the buffer off to + // JavaScript as a Latin1 string (since ASCII is a subset of Latin1). + JS::UniqueLatin1Chars asLatin1( + reinterpret_cast<JS::Latin1Char*>(aBuffer.mBuffer.release())); + return JS_NewLatin1String(aCx, std::move(asLatin1), aBuffer.mLength); + } + + // If the string is encodable as Latin1, we need to deflate the string to a + // Latin1 string to accoutn for UTF-8 characters that are encoded as more than + // a single byte. + // + // Otherwise, the string contains characters outside Latin1 so we have to + // inflate to UTF-16. + return JS_NewStringCopyUTF8N( + aCx, JS::UTF8Chars(aBuffer.mBuffer.get(), aBuffer.mLength)); +} + +/* static */ +JSObject* IOUtils::JsBuffer::IntoUint8Array(JSContext* aCx, JsBuffer aBuffer) { + MOZ_RELEASE_ASSERT(aBuffer.mBufferKind == IOUtils::BufferKind::Uint8Array); + + if (!aBuffer.mCapacity) { + return JS_NewUint8Array(aCx, 0); + } + + char* rawBuffer = aBuffer.mBuffer.release(); + MOZ_RELEASE_ASSERT(rawBuffer); + JS::Rooted<JSObject*> arrayBuffer( + aCx, JS::NewArrayBufferWithContents(aCx, aBuffer.mLength, + reinterpret_cast<void*>(rawBuffer))); + + if (!arrayBuffer) { + // The array buffer does not take ownership of the data pointer unless + // creation succeeds. We are still on the hook to free it. + // + // aBuffer will be destructed at end of scope, but its destructor does not + // take into account |mCapacity| or |mLength|, so it is OK for them to be + // non-zero here with a null |mBuffer|. + js_free(rawBuffer); + return nullptr; + } + + return JS_NewUint8ArrayWithBuffer(aCx, arrayBuffer, 0, aBuffer.mLength); +} + +MOZ_MUST_USE bool ToJSValue(JSContext* aCx, IOUtils::JsBuffer&& aBuffer, + JS::MutableHandle<JS::Value> aValue) { + if (aBuffer.mBufferKind == IOUtils::BufferKind::String) { + JSString* str = IOUtils::JsBuffer::IntoString(aCx, std::move(aBuffer)); + if (!str) { + return false; + } + + aValue.setString(str); + return true; + } + + JSObject* array = IOUtils::JsBuffer::IntoUint8Array(aCx, std::move(aBuffer)); + if (!array) { + return false; + } + + aValue.setObject(*array); + return true; +} + +} // namespace mozilla::dom + +#undef REJECT_IF_SHUTTING_DOWN +#undef REJECT_IF_INIT_PATH_FAILED diff --git a/dom/system/IOUtils.h b/dom/system/IOUtils.h new file mode 100644 index 0000000000..177de0a461 --- /dev/null +++ b/dom/system/IOUtils.h @@ -0,0 +1,600 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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_dom_IOUtils__ +#define mozilla_dom_IOUtils__ + +#include "js/Utility.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Attributes.h" +#include "mozilla/Buffer.h" +#include "mozilla/DataMutex.h" +#include "mozilla/MozPromise.h" +#include "mozilla/Result.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/IOUtilsBinding.h" +#include "mozilla/dom/TypedArray.h" +#include "nsIAsyncShutdown.h" +#include "nsISerialEventTarget.h" +#include "nsPrintfCString.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsTArray.h" +#include "prio.h" + +namespace mozilla { + +/** + * Utility class to be used with |UniquePtr| to automatically close NSPR file + * descriptors when they go out of scope. + * + * Example: + * + * UniquePtr<PRFileDesc, PR_CloseDelete> fd = PR_Open(path, flags, mode); + */ +class PR_CloseDelete { + public: + constexpr PR_CloseDelete() = default; + PR_CloseDelete(const PR_CloseDelete& aOther) = default; + PR_CloseDelete(PR_CloseDelete&& aOther) = default; + PR_CloseDelete& operator=(const PR_CloseDelete& aOther) = default; + PR_CloseDelete& operator=(PR_CloseDelete&& aOther) = default; + + void operator()(PRFileDesc* aPtr) const { PR_Close(aPtr); } +}; + +namespace dom { + +/** + * Implementation for the Web IDL interface at dom/chrome-webidl/IOUtils.webidl. + * Methods of this class must only be called from the parent process. + */ +class IOUtils final { + public: + class IOError; + + static already_AddRefed<Promise> Read(GlobalObject& aGlobal, + const nsAString& aPath, + const ReadOptions& aOptions); + + static already_AddRefed<Promise> ReadUTF8(GlobalObject& aGlobal, + const nsAString& aPath, + const ReadUTF8Options& aOptions); + + static already_AddRefed<Promise> ReadJSON(GlobalObject& aGlobal, + const nsAString& aPath, + const ReadUTF8Options& aOptions); + + static already_AddRefed<Promise> Write(GlobalObject& aGlobal, + const nsAString& aPath, + const Uint8Array& aData, + const WriteOptions& aOptions); + + static already_AddRefed<Promise> WriteUTF8(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aString, + const WriteOptions& aOptions); + + static already_AddRefed<Promise> WriteJSON(GlobalObject& aGlobal, + const nsAString& aPath, + JS::Handle<JS::Value> aValue, + const WriteOptions& aOptions); + + static already_AddRefed<Promise> Move(GlobalObject& aGlobal, + const nsAString& aSourcePath, + const nsAString& aDestPath, + const MoveOptions& aOptions); + + static already_AddRefed<Promise> Remove(GlobalObject& aGlobal, + const nsAString& aPath, + const RemoveOptions& aOptions); + + static already_AddRefed<Promise> MakeDirectory( + GlobalObject& aGlobal, const nsAString& aPath, + const MakeDirectoryOptions& aOptions); + + static already_AddRefed<Promise> Stat(GlobalObject& aGlobal, + const nsAString& aPath); + + static already_AddRefed<Promise> Copy(GlobalObject& aGlobal, + const nsAString& aSourcePath, + const nsAString& aDestPath, + const CopyOptions& aOptions); + + static already_AddRefed<Promise> Touch( + GlobalObject& aGlobal, const nsAString& aPath, + const Optional<int64_t>& aModification); + + static already_AddRefed<Promise> GetChildren(GlobalObject& aGlobal, + const nsAString& aPath); + + static already_AddRefed<Promise> SetPermissions(GlobalObject& aGlobal, + const nsAString& aPath, + const uint32_t aPermissions); + + static already_AddRefed<Promise> Exists(GlobalObject& aGlobal, + const nsAString& aPath); + + class JsBuffer; + + /** + * The kind of buffer to allocate. + * + * This controls what kind of JS object (a JSString or a Uint8Array) is + * returned by |ToJSValue()|. + */ + enum class BufferKind { + String, + Uint8Array, + }; + + private: + ~IOUtils() = default; + + template <typename T> + using IOPromise = MozPromise<T, IOError, true>; + + friend class IOUtilsShutdownBlocker; + struct InternalFileInfo; + struct InternalWriteOpts; + class MozLZ4; + + static StaticDataMutex<StaticRefPtr<nsISerialEventTarget>> + sBackgroundEventTarget; + static StaticRefPtr<nsIAsyncShutdownClient> sBarrier; + static Atomic<bool> sShutdownStarted; + + template <typename OkT, typename Fn, typename... Args> + static RefPtr<IOUtils::IOPromise<OkT>> InvokeToIOPromise(Fn aFunc, + Args... aArgs); + + static already_AddRefed<nsIAsyncShutdownClient> GetShutdownBarrier(); + + static already_AddRefed<nsISerialEventTarget> GetBackgroundEventTarget(); + + static void SetShutdownHooks(); + + template <typename OkT, typename Fn> + static RefPtr<IOPromise<OkT>> RunOnBackgroundThread(Fn aFunc); + + template <typename OkT, typename Fn> + static void RunOnBackgroundThreadAndResolve(Promise* aPromise, Fn aFunc); + + /** + * Creates a new JS Promise. + * + * @return The new promise, or |nullptr| on failure. + */ + static already_AddRefed<Promise> CreateJSPromise(GlobalObject& aGlobal); + + // Allow conversion of |InternalFileInfo| with |ToJSValue|. + friend MOZ_MUST_USE bool ToJSValue(JSContext* aCx, + const InternalFileInfo& aInternalFileInfo, + JS::MutableHandle<JS::Value> aValue); + + /** + * Resolves |aPromise| with an appropriate JS value for |aValue|. + */ + template <typename T> + static void ResolveJSPromise(Promise* aPromise, T&& aValue); + /** + * Rejects |aPromise| with an appropriate |DOMException| describing |aError|. + */ + static void RejectJSPromise(Promise* aPromise, const IOError& aError); + + /** + * Attempts to read the entire file at |aPath| into a buffer. + * + * @param aFile The location of the file. + * @param aMaxBytes If |Some|, then only read up this this number of bytes, + * otherwise attempt to read the whole file. + * @param aDecompress If true, decompress the bytes read from disk before + * returning the result to the caller. + * @param aBufferKind The kind of buffer to allocate. + * + * @return A buffer containing the entire (decompressed) file contents, or an + * error. + */ + static Result<JsBuffer, IOError> ReadSync(nsIFile* aFile, + const Maybe<uint32_t>& aMaxBytes, + const bool aDecompress, + BufferKind aBufferKind); + + /* + * Attempts to read the entire file at |aPath| as a UTF-8 string. + * + * @param aFile The location of the file. + * @param aDecompress If true, decompress the bytes read from disk before + * returning the result to the caller. + * + * @return The (decompressed) contents of the file re-encoded as a UTF-16 + * string. + */ + static Result<JsBuffer, IOError> ReadUTF8Sync(nsIFile* aFile, + const bool aDecompress); + + /** + * Attempt to write the entirety of |aByteArray| to the file at |aPath|. + * This may occur by writing to an intermediate destination and performing a + * move, depending on |aOptions|. + * + * @param aFile The location of the file. + * @param aByteArray The data to write to the file. + * @param aOptions Options to modify the way the write is completed. + * + * @return The number of bytes written to the file, or an error if the write + * failed or was incomplete. + */ + static Result<uint32_t, IOError> WriteSync( + nsIFile* aFile, const Span<const uint8_t>& aByteArray, + const InternalWriteOpts& aOptions); + + /** + * Attempts to move the file located at |aSourceFile| to |aDestFile|. + * + * @param aSourceFile The location of the file to move. + * @param aDestFile The destination for the file. + * @param noOverWrite If true, abort with an error if a file already exists at + * |aDestFile|. Otherwise, the file will be overwritten by + * the move. + * + * @return Ok if the file was moved successfully, or an error. + */ + static Result<Ok, IOError> MoveSync(nsIFile* aSourceFile, nsIFile* aDestFile, + bool aNoOverwrite); + + /** + * Attempts to copy the file at |aSourceFile| to |aDestFile|. + * + * @param aSourceFile The location of the file to copy. + * @param aDestFile The destination that the file will be copied to. + * + * @return Ok if the operation was successful, or an error. + */ + static Result<Ok, IOError> CopySync(nsIFile* aSourceFile, nsIFile* aDestFile, + bool aNoOverWrite, bool aRecursive); + + /** + * Provides the implementation for |CopySync| and |MoveSync|. + * + * @param aMethod A pointer to one of |nsIFile::MoveTo| or |CopyTo| + * instance methods. + * @param aMethodName The name of the method to the performed. Either "move" + * or "copy". + * @param aSource The source file to be copied or moved. + * @param aDest The destination file. + * @param aNoOverwrite If true, allow overwriting |aDest| during the copy or + * move. Otherwise, abort with an error if the file would + * be overwritten. + * + * @return Ok if the operation was successful, or an error. + */ + template <typename CopyOrMoveFn> + static Result<Ok, IOError> CopyOrMoveSync(CopyOrMoveFn aMethod, + const char* aMethodName, + nsIFile* aSource, nsIFile* aDest, + bool aNoOverwrite); + + /** + * Attempts to remove the file located at |aFile|. + * + * @param aFile The location of the file. + * @param aIgnoreAbsent If true, suppress errors due to an absent target file. + * @param aRecursive If true, attempt to recursively remove descendant + * files. This option is safe to use even if the target + * is not a directory. + * + * @return Ok if the file was removed successfully, or an error. + */ + static Result<Ok, IOError> RemoveSync(nsIFile* aFile, bool aIgnoreAbsent, + bool aRecursive); + + /** + * Attempts to create a new directory at |aFile|. + * + * @param aFile The location of the directory to create. + * @param aCreateAncestors If true, create missing ancestor directories as + * needed. Otherwise, report an error if the target + * has non-existing ancestor directories. + * @param aIgnoreExisting If true, suppress errors that occur if the target + * directory already exists. Otherwise, propagate the + * error if it occurs. + * @param aMode Optional file mode. Defaults to 0777 to allow the + * system umask to compute the best mode for the new + * directory. + * + * @return Ok if the directory was created successfully, or an error. + */ + static Result<Ok, IOError> MakeDirectorySync(nsIFile* aFile, + bool aCreateAncestors, + bool aIgnoreExisting, + int32_t aMode = 0777); + + /** + * Attempts to stat a file at |aFile|. + * + * @param aFile The location of the file. + * + * @return An |InternalFileInfo| struct if successful, or an error. + */ + static Result<IOUtils::InternalFileInfo, IOError> StatSync(nsIFile* aFile); + + /** + * Attempts to update the last modification time of the file at |aFile|. + * + * @param aFile The location of the file. + * @param aNewModTime Some value in milliseconds since Epoch. For the current + * system time, use |Nothing|. + * + * @return Timestamp of the file if the operation was successful, or an error. + */ + static Result<int64_t, IOError> TouchSync(nsIFile* aFile, + const Maybe<int64_t>& aNewModTime); + + /** + * Returns the immediate children of the directory at |aFile|, if any. + * + * @param aFile The location of the directory. + * + * @return An array of absolute paths identifying the children of |aFile|. + * If there are no children, an empty array. Otherwise, an error. + */ + static Result<nsTArray<nsString>, IOError> GetChildrenSync(nsIFile* aFile); + + /** + * Set the permissions of the given file. + * + * Windows does not make a distinction between user, group, and other + * permissions like UNICES do. If a permission flag is set for any of user, + * group, or other has a permission, then all users will have that + * permission. + * + * @param aFile The location of the file. + * @param aPermissions The permissions to set, as a UNIX file mode. + * + * @return |Ok| if the permissions were successfully set, or an error. + */ + static Result<Ok, IOError> SetPermissionsSync(nsIFile* aFile, + const uint32_t aPermissions); + + /** + * Return whether or not the file exists. + * + * @param aFile The location of the file. + * + * @return Whether or not the file exists. + */ + static Result<bool, IOError> ExistsSync(nsIFile* aFile); +}; + +/** + * An error class used with the |Result| type returned by most private |IOUtils| + * methods. + */ +class IOUtils::IOError { + public: + MOZ_IMPLICIT IOError(nsresult aCode) : mCode(aCode), mMessage(Nothing()) {} + + /** + * Replaces the message associated with this error. + */ + template <typename... Args> + IOError WithMessage(const char* const aMessage, Args... aArgs) { + mMessage.emplace(nsPrintfCString(aMessage, aArgs...)); + return *this; + } + IOError WithMessage(const char* const aMessage) { + mMessage.emplace(nsCString(aMessage)); + return *this; + } + IOError WithMessage(const nsCString& aMessage) { + mMessage.emplace(aMessage); + return *this; + } + + /** + * Returns the |nsresult| associated with this error. + */ + nsresult Code() const { return mCode; } + + /** + * Maybe returns a message associated with this error. + */ + const Maybe<nsCString>& Message() const { return mMessage; } + + private: + nsresult mCode; + Maybe<nsCString> mMessage; +}; + +/** + * This is an easier to work with representation of a |mozilla::dom::FileInfo| + * for private use in the IOUtils implementation. + * + * Because web IDL dictionaries are not easily copy/moveable, this class is + * used instead, until converted to the proper |mozilla::dom::FileInfo| before + * returning any results to JavaScript. + */ +struct IOUtils::InternalFileInfo { + nsString mPath; + FileType mType = FileType::Other; + uint64_t mSize = 0; + uint64_t mLastModified = 0; + Maybe<uint64_t> mCreationTime; + uint32_t mPermissions = 0; +}; + +/** + * This is an easier to work with representation of a + * |mozilla::dom::WriteOptions| for private use in the |IOUtils| + * implementation. + * + * Because web IDL dictionaries are not easily copy/moveable, this class is + * used instead. + */ +struct IOUtils::InternalWriteOpts { + RefPtr<nsIFile> mBackupFile; + RefPtr<nsIFile> mTmpFile; + bool mFlush = false; + bool mNoOverwrite = false; + bool mCompress = false; + + static Result<InternalWriteOpts, IOUtils::IOError> FromBinding( + const WriteOptions& aOptions); +}; + +/** + * Re-implements the file compression and decompression utilities found + * in toolkit/components/lz4/lz4.js + * + * This implementation uses the non-standard data layout: + * + * - MAGIC_NUMBER (8 bytes) + * - content size (uint32_t, little endian) + * - content, as obtained from mozilla::Compression::LZ4::compress + * + * See bug 1209390 for more info. + */ +class IOUtils::MozLZ4 { + public: + static constexpr std::array<uint8_t, 8> MAGIC_NUMBER{ + {'m', 'o', 'z', 'L', 'z', '4', '0', '\0'}}; + + static const uint32_t HEADER_SIZE = 8 + sizeof(uint32_t); + + /** + * Compresses |aUncompressed| byte array, and returns a byte array with the + * correct format whose contents may be written to disk. + */ + static Result<nsTArray<uint8_t>, IOError> Compress( + Span<const uint8_t> aUncompressed); + + /** + * Checks |aFileContents| for the correct file header, and returns the + * decompressed content. + */ + static Result<IOUtils::JsBuffer, IOError> Decompress( + Span<const uint8_t> aFileContents, IOUtils::BufferKind); +}; + +class IOUtilsShutdownBlocker : public nsIAsyncShutdownBlocker { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIASYNCSHUTDOWNBLOCKER + + private: + virtual ~IOUtilsShutdownBlocker() = default; +}; + +/** + * A buffer that is allocated inside one of JS heaps so that it can be converted + * to a JSString or Uint8Array object with at most one copy in the worst case. + */ +class IOUtils::JsBuffer final { + public: + /** + * Create a new buffer of the given kind with the requested capacity. + * + * @param aBufferKind The kind of buffer to create (either a string or an + * array). + * @param aCapacity The capacity of the buffer. + * + * @return Either a successfully created buffer or an error if it could not be + * allocated. + */ + static Result<JsBuffer, IOUtils::IOError> Create( + IOUtils::BufferKind aBufferKind, size_t aCapacity); + + /** + * Create a new, empty buffer. + * + * This operation cannot fail. + * + * @param aBufferKind The kind of buffer to create (either a string or an + * array). + * + * @return An empty JsBuffer. + */ + static JsBuffer CreateEmpty(IOUtils::BufferKind aBufferKind); + + JsBuffer(const JsBuffer&) = delete; + JsBuffer(JsBuffer&& aOther) noexcept; + JsBuffer& operator=(const JsBuffer&) = delete; + JsBuffer& operator=(JsBuffer&& aOther) noexcept; + + size_t Length() { return mLength; } + char* Elements() { return mBuffer.get(); } + void SetLength(size_t aNewLength) { + MOZ_RELEASE_ASSERT(aNewLength <= mCapacity); + mLength = aNewLength; + } + + /** + * Return a span for writing to the buffer. + * + * |SetLength| should be called after the buffer has been written to. + * + * @returns A span for writing to. The size of the span is the entire + * allocated capacity. + */ + Span<char> BeginWriting() { + MOZ_RELEASE_ASSERT(mBuffer.get()); + return Span(mBuffer.get(), mCapacity); + } + + /** + * Return a span for reading from. + * + * @returns A span for reading form. The size of the span is the set length + * of the buffer. + */ + Span<const char> BeginReading() const { + MOZ_RELEASE_ASSERT(mBuffer.get() || mLength == 0); + return Span(mBuffer.get(), mLength); + } + + /** + * Consume the JsBuffer and convert it into a JSString. + * + * NOTE: This method asserts the buffer was allocated as a string buffer. + * + * @param aBuffer The buffer to convert to a string. After this call, the + * buffer will be invaldated and |IntoString| cannot be called + * again. + * + * @returns A JSString with the contents of |aBuffer|. + */ + static JSString* IntoString(JSContext* aCx, JsBuffer aBuffer); + + /** + * Consume the JsBuffer and convert it into a Uint8Array. + * + * NOTE: This method asserts the buffer was allocated as an array buffer. + * + * @param aBuffer The buffer to convert to an array. After this call, the + * buffer will be invalidated and |IntoUint8Array| cannot be + * called again. + * + * @returns A JSBuffer + */ + static JSObject* IntoUint8Array(JSContext* aCx, JsBuffer aBuffer); + + friend MOZ_MUST_USE bool ToJSValue(JSContext* aCx, JsBuffer&& aBuffer, + JS::MutableHandle<JS::Value> aValue); + + private: + IOUtils::BufferKind mBufferKind; + size_t mCapacity; + size_t mLength; + JS::UniqueChars mBuffer; + + JsBuffer(BufferKind aBufferKind, size_t aCapacity); +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/system/NetworkGeolocationProvider.jsm b/dom/system/NetworkGeolocationProvider.jsm new file mode 100644 index 0000000000..9c8630c8a7 --- /dev/null +++ b/dom/system/NetworkGeolocationProvider.jsm @@ -0,0 +1,527 @@ +/* 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"; + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetters(this, { + clearTimeout: "resource://gre/modules/Timer.jsm", + LocationHelper: "resource://gre/modules/LocationHelper.jsm", + setTimeout: "resource://gre/modules/Timer.jsm", +}); + +XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]); + +// GeolocationPositionError has no interface object, so we can't use that here. +const POSITION_UNAVAILABLE = 2; +const TELEMETRY_KEY = "REGION_LOCATION_SERVICES_DIFFERENCE"; + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "gLoggingEnabled", + "geo.provider.network.logging.enabled", + false +); + +function LOG(aMsg) { + if (gLoggingEnabled) { + dump("*** WIFI GEO: " + aMsg + "\n"); + } +} + +function CachedRequest(loc, cellInfo, wifiList) { + this.location = loc; + + let wifis = new Set(); + if (wifiList) { + for (let i = 0; i < wifiList.length; i++) { + wifis.add(wifiList[i].macAddress); + } + } + + // Use only these values for equality + // (the JSON will contain additional values in future) + function makeCellKey(cell) { + return ( + "" + + cell.radio + + ":" + + cell.mobileCountryCode + + ":" + + cell.mobileNetworkCode + + ":" + + cell.locationAreaCode + + ":" + + cell.cellId + ); + } + + let cells = new Set(); + if (cellInfo) { + for (let i = 0; i < cellInfo.length; i++) { + cells.add(makeCellKey(cellInfo[i])); + } + } + + this.hasCells = () => cells.size > 0; + + this.hasWifis = () => wifis.size > 0; + + // if fields match + this.isCellEqual = function(cellInfo) { + if (!this.hasCells()) { + return false; + } + + let len1 = cells.size; + let len2 = cellInfo.length; + + if (len1 != len2) { + LOG("cells not equal len"); + return false; + } + + for (let i = 0; i < len2; i++) { + if (!cells.has(makeCellKey(cellInfo[i]))) { + return false; + } + } + return true; + }; + + // if 50% of the SSIDS match + this.isWifiApproxEqual = function(wifiList) { + if (!this.hasWifis()) { + return false; + } + + // if either list is a 50% subset of the other, they are equal + let common = 0; + for (let i = 0; i < wifiList.length; i++) { + if (wifis.has(wifiList[i].macAddress)) { + common++; + } + } + let kPercentMatch = 0.5; + return common >= Math.max(wifis.size, wifiList.length) * kPercentMatch; + }; + + this.isGeoip = function() { + return !this.hasCells() && !this.hasWifis(); + }; + + this.isCellAndWifi = function() { + return this.hasCells() && this.hasWifis(); + }; + + this.isCellOnly = function() { + return this.hasCells() && !this.hasWifis(); + }; + + this.isWifiOnly = function() { + return this.hasWifis() && !this.hasCells(); + }; +} + +var gCachedRequest = null; +var gDebugCacheReasoning = ""; // for logging the caching logic + +// This function serves two purposes: +// 1) do we have a cached request +// 2) is the cached request better than what newCell and newWifiList will obtain +// If the cached request exists, and we know it to have greater accuracy +// by the nature of its origin (wifi/cell/geoip), use its cached location. +// +// If there is more source info than the cached request had, return false +// In other cases, MLS is known to produce better/worse accuracy based on the +// inputs, so base the decision on that. +function isCachedRequestMoreAccurateThanServerRequest(newCell, newWifiList) { + gDebugCacheReasoning = ""; + let isNetworkRequestCacheEnabled = true; + try { + // Mochitest needs this pref to simulate request failure + isNetworkRequestCacheEnabled = Services.prefs.getBoolPref( + "geo.provider.network.debug.requestCache.enabled" + ); + if (!isNetworkRequestCacheEnabled) { + gCachedRequest = null; + } + } catch (e) {} + + if (!gCachedRequest || !isNetworkRequestCacheEnabled) { + gDebugCacheReasoning = "No cached data"; + return false; + } + + if (!newCell && !newWifiList) { + gDebugCacheReasoning = "New req. is GeoIP."; + return true; + } + + if ( + newCell && + newWifiList && + (gCachedRequest.isCellOnly() || gCachedRequest.isWifiOnly()) + ) { + gDebugCacheReasoning = "New req. is cell+wifi, cache only cell or wifi."; + return false; + } + + if (newCell && gCachedRequest.isWifiOnly()) { + // In order to know if a cell-only request should trump a wifi-only request + // need to know if wifi is low accuracy. >5km would be VERY low accuracy, + // it is worth trying the cell + var isHighAccuracyWifi = gCachedRequest.location.coords.accuracy < 5000; + gDebugCacheReasoning = + "Req. is cell, cache is wifi, isHigh:" + isHighAccuracyWifi; + return isHighAccuracyWifi; + } + + let hasEqualCells = false; + if (newCell) { + hasEqualCells = gCachedRequest.isCellEqual(newCell); + } + + let hasEqualWifis = false; + if (newWifiList) { + hasEqualWifis = gCachedRequest.isWifiApproxEqual(newWifiList); + } + + gDebugCacheReasoning = + "EqualCells:" + hasEqualCells + " EqualWifis:" + hasEqualWifis; + + if (gCachedRequest.isCellOnly()) { + gDebugCacheReasoning += ", Cell only."; + if (hasEqualCells) { + return true; + } + } else if (gCachedRequest.isWifiOnly() && hasEqualWifis) { + gDebugCacheReasoning += ", Wifi only."; + return true; + } else if (gCachedRequest.isCellAndWifi()) { + gDebugCacheReasoning += ", Cache has Cell+Wifi."; + if ( + (hasEqualCells && hasEqualWifis) || + (!newWifiList && hasEqualCells) || + (!newCell && hasEqualWifis) + ) { + return true; + } + } + + return false; +} + +function NetworkGeoCoordsObject(lat, lon, acc) { + this.latitude = lat; + this.longitude = lon; + this.accuracy = acc; + + // Neither GLS nor MLS return the following properties, so set them to NaN + // here. nsGeoPositionCoords will convert NaNs to null for optional properties + // of the JavaScript Coordinates object. + this.altitude = NaN; + this.altitudeAccuracy = NaN; + this.heading = NaN; + this.speed = NaN; +} + +NetworkGeoCoordsObject.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIDOMGeoPositionCoords"]), +}; + +function NetworkGeoPositionObject(lat, lng, acc) { + this.coords = new NetworkGeoCoordsObject(lat, lng, acc); + this.address = null; + this.timestamp = Date.now(); +} + +NetworkGeoPositionObject.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIDOMGeoPosition"]), +}; + +function NetworkGeolocationProvider() { + /* + The _wifiMonitorTimeout controls how long we wait on receiving an update + from the Wifi subsystem. If this timer fires, we believe the Wifi scan has + had a problem and we no longer can use Wifi to position the user this time + around (we will continue to be hopeful that Wifi will recover). + + This timeout value is also used when Wifi scanning is disabled (see + isWifiScanningEnabled). In this case, we use this timer to collect cell/ip + data and xhr it to the location server. + */ + XPCOMUtils.defineLazyPreferenceGetter( + this, + "_wifiMonitorTimeout", + "geo.provider.network.timeToWaitBeforeSending", + 5000 + ); + + XPCOMUtils.defineLazyPreferenceGetter( + this, + "_wifiScanningEnabled", + "geo.provider.network.scan", + true + ); + + XPCOMUtils.defineLazyPreferenceGetter( + this, + "_wifiCompareURL", + "geo.provider.network.compare.url", + null + ); + + this.wifiService = null; + this.timer = null; + this.started = false; +} + +NetworkGeolocationProvider.prototype = { + classID: Components.ID("{77DA64D3-7458-4920-9491-86CC9914F904}"), + QueryInterface: ChromeUtils.generateQI([ + "nsIGeolocationProvider", + "nsIWifiListener", + "nsITimerCallback", + "nsIObserver", + ]), + listener: null, + + get isWifiScanningEnabled() { + return Cc["@mozilla.org/wifi/monitor;1"] && this._wifiScanningEnabled; + }, + + resetTimer() { + if (this.timer) { + this.timer.cancel(); + this.timer = null; + } + // Wifi thread triggers NetworkGeolocationProvider to proceed. With no wifi, + // do manual timeout. + this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this.timer.initWithCallback( + this, + this._wifiMonitorTimeout, + this.timer.TYPE_REPEATING_SLACK + ); + }, + + startup() { + if (this.started) { + return; + } + + this.started = true; + + if (this.isWifiScanningEnabled) { + if (this.wifiService) { + this.wifiService.stopWatching(this); + } + this.wifiService = Cc["@mozilla.org/wifi/monitor;1"].getService( + Ci.nsIWifiMonitor + ); + this.wifiService.startWatching(this); + } + + this.resetTimer(); + LOG("startup called."); + }, + + watch(c) { + this.listener = c; + }, + + shutdown() { + LOG("shutdown called"); + if (!this.started) { + return; + } + + // Without clearing this, we could end up using the cache almost indefinitely + // TODO: add logic for cache lifespan, for now just be safe and clear it + gCachedRequest = null; + + if (this.timer) { + this.timer.cancel(); + this.timer = null; + } + + if (this.wifiService) { + this.wifiService.stopWatching(this); + this.wifiService = null; + } + + this.listener = null; + this.started = false; + }, + + setHighAccuracy(enable) {}, + + onChange(accessPoints) { + // we got some wifi data, rearm the timer. + this.resetTimer(); + + let wifiData = null; + if (accessPoints) { + wifiData = LocationHelper.formatWifiAccessPoints(accessPoints); + } + this.sendLocationRequest(wifiData); + }, + + onError(code) { + LOG("wifi error: " + code); + this.sendLocationRequest(null); + }, + + onStatus(err, statusMessage) { + if (!this.listener) { + return; + } + LOG("onStatus called." + statusMessage); + + if (statusMessage && this.listener.notifyStatus) { + this.listener.notifyStatus(statusMessage); + } + + if (err && this.listener.notifyError) { + this.listener.notifyError(POSITION_UNAVAILABLE, statusMessage); + } + }, + + notify(timer) { + this.onStatus(false, "wifi-timeout"); + this.sendLocationRequest(null); + }, + + /** + * After wifi (and possible cell tower) data has been gathered, this method is + * invoked to perform the request to network geolocation provider. + * The result of each request is sent to all registered listener (@see watch) + * by invoking its respective `update`, `notifyError` or `notifyStatus` + * callbacks. + * `update` is called upon a successful request with its response data; this will be a `NetworkGeoPositionObject` instance. + * `notifyError` is called whenever the request gets an error from the local + * network subsystem, the server or simply times out. + * `notifyStatus` is called for each status change of the request that may be + * of interest to the consumer of this class. Currently the following status + * changes are reported: 'xhr-start', 'xhr-timeout', 'xhr-error' and + * 'xhr-empty'. + * + * @param {Array} wifiData Optional set of publicly available wifi networks + * in the following structure: + * <code> + * [ + * { macAddress: <mac1>, signalStrength: <signal1> }, + * { macAddress: <mac2>, signalStrength: <signal2> } + * ] + * </code> + */ + async sendLocationRequest(wifiData) { + let data = { cellTowers: undefined, wifiAccessPoints: undefined }; + if (wifiData && wifiData.length >= 2) { + data.wifiAccessPoints = wifiData; + } + + let useCached = isCachedRequestMoreAccurateThanServerRequest( + data.cellTowers, + data.wifiAccessPoints + ); + + LOG("Use request cache:" + useCached + " reason:" + gDebugCacheReasoning); + + if (useCached) { + gCachedRequest.location.timestamp = Date.now(); + if (this.listener) { + this.listener.update(gCachedRequest.location); + } + return; + } + + // From here on, do a network geolocation request // + let url = Services.urlFormatter.formatURLPref("geo.provider.network.url"); + LOG("Sending request"); + + let result; + try { + result = await this.makeRequest(url, wifiData); + LOG( + `geo provider reported: ${result.location.lng}:${result.location.lat}` + ); + let newLocation = new NetworkGeoPositionObject( + result.location.lat, + result.location.lng, + result.accuracy + ); + + if (this.listener) { + this.listener.update(newLocation); + } + + gCachedRequest = new CachedRequest( + newLocation, + data.cellTowers, + data.wifiAccessPoints + ); + } catch (err) { + LOG("Location request hit error: " + err.name); + Cu.reportError(err); + if (err.name == "AbortError") { + this.onStatus(true, "xhr-timeout"); + } else { + this.onStatus(true, "xhr-error"); + } + } + + if (!this._wifiCompareURL) { + return; + } + + let compareUrl = Services.urlFormatter.formatURL(this._wifiCompareURL); + let compare = await this.makeRequest(compareUrl, wifiData); + if (!compare.location) { + LOG("Backup location service didnt report location"); + return; + } + let distance = LocationHelper.distance(result.location, compare.location); + LOG( + `compare reported reported: ${compare.location.lng}:${compare.location.lat}` + ); + LOG(`distance between results: ${distance}`); + if (!isNaN(distance)) { + Services.telemetry.getHistogramById(TELEMETRY_KEY).add(distance); + } + }, + + async makeRequest(url, wifiData) { + this.onStatus(false, "xhr-start"); + + let fetchController = new AbortController(); + let fetchOpts = { + method: "POST", + headers: { "Content-Type": "application/json; charset=UTF-8" }, + credentials: "omit", + signal: fetchController.signal, + }; + + if (wifiData) { + fetchOpts.body = JSON.stringify({ wifiAccessPoints: wifiData }); + } + + let timeoutId = setTimeout( + () => fetchController.abort(), + Services.prefs.getIntPref("geo.provider.network.timeout") + ); + + let req = await fetch(url, fetchOpts); + clearTimeout(timeoutId); + let result = req.json(); + return result; + }, +}; + +var EXPORTED_SYMBOLS = ["NetworkGeolocationProvider"]; diff --git a/dom/system/OSFileConstants.cpp b/dom/system/OSFileConstants.cpp new file mode 100644 index 0000000000..42305e4b4c --- /dev/null +++ b/dom/system/OSFileConstants.cpp @@ -0,0 +1,996 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "mozilla/DebugOnly.h" + +#include "fcntl.h" +#include "errno.h" + +#include "prsystem.h" + +// Short macro to get the size of a member of a +// given struct at compile time. +// t is the type of struct, m the name of the +// member: +// DOM_SIZEOF_MEMBER(struct mystruct, myint) +// will give you the size of the type of myint. +#define DOM_SIZEOF_MEMBER(t, m) sizeof(((t*)0)->m) + +#if defined(XP_UNIX) +# include "unistd.h" +# include "dirent.h" +# include "poll.h" +# include "sys/stat.h" +# if defined(XP_LINUX) +# include <sys/vfs.h> +# define statvfs statfs +# define f_frsize f_bsize +# else +# include "sys/statvfs.h" +# endif // defined(XP_LINUX) +# if !defined(ANDROID) +# include "sys/wait.h" +# include <spawn.h> +# endif // !defined(ANDROID) +#endif // defined(XP_UNIX) + +#if defined(XP_LINUX) +# include <linux/fadvise.h> +#endif // defined(XP_LINUX) + +#if defined(XP_MACOSX) +# include "copyfile.h" +#endif // defined(XP_MACOSX) + +#if defined(XP_WIN) +# include <windows.h> +# include <accctrl.h> + +# ifndef PATH_MAX +# define PATH_MAX MAX_PATH +# endif + +#endif // defined(XP_WIN) + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "BindingUtils.h" + +// Used to provide information on the OS + +#include "nsThreadUtils.h" +#include "nsIObserverService.h" +#include "nsIObserver.h" +#include "nsDirectoryServiceUtils.h" +#include "nsIXULRuntime.h" +#include "nsXPCOMCIDInternal.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsSystemInfo.h" +#include "nsDirectoryServiceDefs.h" +#include "nsXULAppAPI.h" +#include "nsAppDirectoryServiceDefs.h" +#include "mozJSComponentLoader.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/UniquePtr.h" + +#include "OSFileConstants.h" +#include "nsZipArchive.h" + +#if defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || \ + defined(__OpenBSD__) +# define __dd_fd dd_fd +#endif + +/** + * This module defines the basic libc constants (error numbers, open modes, + * etc.) used by OS.File and possibly other OS-bound JavaScript libraries. + */ + +namespace mozilla { + +namespace { + +StaticRefPtr<OSFileConstantsService> gInstance; + +} // anonymous namespace + +struct OSFileConstantsService::Paths { + /** + * The name of the directory holding all the libraries (libxpcom, libnss, + * etc.) + */ + nsString libDir; + nsString tmpDir; + nsString profileDir; + nsString localProfileDir; + + Paths() { + libDir.SetIsVoid(true); + tmpDir.SetIsVoid(true); + profileDir.SetIsVoid(true); + localProfileDir.SetIsVoid(true); + } +}; + +/** + * Return the path to one of the special directories. + * + * @param aKey The key to the special directory (e.g. "TmpD", "ProfD", ...) + * @param aOutPath The path to the special directory. In case of error, + * the string is set to void. + */ +nsresult GetPathToSpecialDir(const char* aKey, nsString& aOutPath) { + nsCOMPtr<nsIFile> file; + nsresult rv = NS_GetSpecialDirectory(aKey, getter_AddRefs(file)); + if (NS_FAILED(rv) || !file) { + return rv; + } + + return file->GetPath(aOutPath); +} + +/** + * In some cases, OSFileConstants may be instantiated before the + * profile is setup. In such cases, |OS.Constants.Path.profileDir| and + * |OS.Constants.Path.localProfileDir| are undefined. However, we want + * to ensure that this does not break existing code, so that future + * workers spawned after the profile is setup have these constants. + * + * For this purpose, we register an observer to set |mPaths->profileDir| + * and |mPaths->localProfileDir| once the profile is setup. + */ +NS_IMETHODIMP +OSFileConstantsService::Observe(nsISupports*, const char* aTopic, + const char16_t*) { + if (!mInitialized) { + // Initialization has not taken place, something is wrong, + // don't make things worse. + return NS_OK; + } + + nsresult rv = + GetPathToSpecialDir(NS_APP_USER_PROFILE_50_DIR, mPaths->profileDir); + if (NS_FAILED(rv)) { + return rv; + } + rv = GetPathToSpecialDir(NS_APP_USER_PROFILE_LOCAL_50_DIR, + mPaths->localProfileDir); + if (NS_FAILED(rv)) { + return rv; + } + + return NS_OK; +} + +/** + * Perform the part of initialization that can only be + * executed on the main thread. + */ +nsresult OSFileConstantsService::InitOSFileConstants() { + MOZ_ASSERT(NS_IsMainThread()); + if (mInitialized) { + return NS_OK; + } + + UniquePtr<Paths> paths(new Paths); + + // Initialize paths->libDir + nsCOMPtr<nsIFile> file; + nsresult rv = + NS_GetSpecialDirectory(NS_XPCOM_LIBRARY_FILE, getter_AddRefs(file)); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIFile> libDir; + rv = file->GetParent(getter_AddRefs(libDir)); + if (NS_FAILED(rv)) { + return rv; + } + + rv = libDir->GetPath(paths->libDir); + if (NS_FAILED(rv)) { + return rv; + } + + // Setup profileDir and localProfileDir immediately if possible (we + // assume that NS_APP_USER_PROFILE_50_DIR and + // NS_APP_USER_PROFILE_LOCAL_50_DIR are set simultaneously) + rv = GetPathToSpecialDir(NS_APP_USER_PROFILE_50_DIR, paths->profileDir); + if (NS_SUCCEEDED(rv)) { + rv = GetPathToSpecialDir(NS_APP_USER_PROFILE_LOCAL_50_DIR, + paths->localProfileDir); + } + + // Otherwise, delay setup of profileDir/localProfileDir until they + // become available. + if (NS_FAILED(rv)) { + nsCOMPtr<nsIObserverService> obsService = + do_GetService(NS_OBSERVERSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + return rv; + } + rv = obsService->AddObserver(this, "profile-do-change", false); + if (NS_FAILED(rv)) { + return rv; + } + } + + GetPathToSpecialDir(NS_OS_TEMP_DIR, paths->tmpDir); + + mPaths = std::move(paths); + + // Get the umask from the system-info service. + // The property will always be present, but it will be zero on + // non-Unix systems. + // nsSystemInfo::gUserUmask is initialized by NS_InitXPCOM so we don't need + // to initialize the service. + mUserUmask = nsSystemInfo::gUserUmask; + + mInitialized = true; + return NS_OK; +} + +/** + * Define a simple read-only property holding an integer. + * + * @param name The name of the constant. Used both as the JS name for the + * constant and to access its value. Must be defined. + * + * Produces a |ConstantSpec|. + */ +#define INT_CONSTANT(name) \ + { #name, JS::Int32Value(name) } + +/** + * Define a simple read-only property holding an unsigned integer. + * + * @param name The name of the constant. Used both as the JS name for the + * constant and to access its value. Must be defined. + * + * Produces a |ConstantSpec|. + */ +#define UINT_CONSTANT(name) \ + { #name, JS::NumberValue(name) } + +/** + * End marker for ConstantSpec + */ +#define PROP_END \ + { nullptr, JS::UndefinedValue() } + +// Define missing constants for Android +#if !defined(S_IRGRP) +# define S_IXOTH 0001 +# define S_IWOTH 0002 +# define S_IROTH 0004 +# define S_IRWXO 0007 +# define S_IXGRP 0010 +# define S_IWGRP 0020 +# define S_IRGRP 0040 +# define S_IRWXG 0070 +# define S_IXUSR 0100 +# define S_IWUSR 0200 +# define S_IRUSR 0400 +# define S_IRWXU 0700 +#endif // !defined(S_IRGRP) + +/** + * The properties defined in libc. + * + * If you extend this list of properties, please + * separate categories ("errors", "open", etc.), + * keep properties organized by alphabetical order + * and #ifdef-away properties that are not portable. + */ +static const dom::ConstantSpec gLibcProperties[] = { + // Arguments for open + INT_CONSTANT(O_APPEND), +#if defined(O_CLOEXEC) + INT_CONSTANT(O_CLOEXEC), +#endif // defined(O_CLOEXEC) + INT_CONSTANT(O_CREAT), +#if defined(O_DIRECTORY) + INT_CONSTANT(O_DIRECTORY), +#endif // defined(O_DIRECTORY) +#if defined(O_EVTONLY) + INT_CONSTANT(O_EVTONLY), +#endif // defined(O_EVTONLY) + INT_CONSTANT(O_EXCL), +#if defined(O_EXLOCK) + INT_CONSTANT(O_EXLOCK), +#endif // defined(O_EXLOCK) +#if defined(O_LARGEFILE) + INT_CONSTANT(O_LARGEFILE), +#endif // defined(O_LARGEFILE) +#if defined(O_NOFOLLOW) + INT_CONSTANT(O_NOFOLLOW), +#endif // defined(O_NOFOLLOW) +#if defined(O_NONBLOCK) + INT_CONSTANT(O_NONBLOCK), +#endif // defined(O_NONBLOCK) + INT_CONSTANT(O_RDONLY), + INT_CONSTANT(O_RDWR), +#if defined(O_RSYNC) + INT_CONSTANT(O_RSYNC), +#endif // defined(O_RSYNC) +#if defined(O_SHLOCK) + INT_CONSTANT(O_SHLOCK), +#endif // defined(O_SHLOCK) +#if defined(O_SYMLINK) + INT_CONSTANT(O_SYMLINK), +#endif // defined(O_SYMLINK) +#if defined(O_SYNC) + INT_CONSTANT(O_SYNC), +#endif // defined(O_SYNC) + INT_CONSTANT(O_TRUNC), + INT_CONSTANT(O_WRONLY), + +#if defined(FD_CLOEXEC) + INT_CONSTANT(FD_CLOEXEC), +#endif // defined(FD_CLOEXEC) + +#if defined(AT_EACCESS) + INT_CONSTANT(AT_EACCESS), +#endif // defined(AT_EACCESS) +#if defined(AT_FDCWD) + INT_CONSTANT(AT_FDCWD), +#endif // defined(AT_FDCWD) +#if defined(AT_SYMLINK_NOFOLLOW) + INT_CONSTANT(AT_SYMLINK_NOFOLLOW), +#endif // defined(AT_SYMLINK_NOFOLLOW) + +#if defined(POSIX_FADV_SEQUENTIAL) + INT_CONSTANT(POSIX_FADV_SEQUENTIAL), +#endif // defined(POSIX_FADV_SEQUENTIAL) + +// access +#if defined(F_OK) + INT_CONSTANT(F_OK), + INT_CONSTANT(R_OK), + INT_CONSTANT(W_OK), + INT_CONSTANT(X_OK), +#endif // defined(F_OK) + + // modes + INT_CONSTANT(S_IRGRP), + INT_CONSTANT(S_IROTH), + INT_CONSTANT(S_IRUSR), + INT_CONSTANT(S_IRWXG), + INT_CONSTANT(S_IRWXO), + INT_CONSTANT(S_IRWXU), + INT_CONSTANT(S_IWGRP), + INT_CONSTANT(S_IWOTH), + INT_CONSTANT(S_IWUSR), + INT_CONSTANT(S_IXOTH), + INT_CONSTANT(S_IXGRP), + INT_CONSTANT(S_IXUSR), + + // seek + INT_CONSTANT(SEEK_CUR), + INT_CONSTANT(SEEK_END), + INT_CONSTANT(SEEK_SET), + +#if defined(XP_UNIX) + // poll + INT_CONSTANT(POLLERR), + INT_CONSTANT(POLLHUP), + INT_CONSTANT(POLLIN), + INT_CONSTANT(POLLNVAL), + INT_CONSTANT(POLLOUT), + +// wait +# if defined(WNOHANG) + INT_CONSTANT(WNOHANG), +# endif // defined(WNOHANG) + + // fcntl command values + INT_CONSTANT(F_GETLK), + INT_CONSTANT(F_SETFD), + INT_CONSTANT(F_SETFL), + INT_CONSTANT(F_SETLK), + INT_CONSTANT(F_SETLKW), + + // flock type values + INT_CONSTANT(F_RDLCK), + INT_CONSTANT(F_WRLCK), + INT_CONSTANT(F_UNLCK), + +// splice +# if defined(SPLICE_F_MOVE) + INT_CONSTANT(SPLICE_F_MOVE), +# endif // defined(SPLICE_F_MOVE) +# if defined(SPLICE_F_NONBLOCK) + INT_CONSTANT(SPLICE_F_NONBLOCK), +# endif // defined(SPLICE_F_NONBLOCK) +# if defined(SPLICE_F_MORE) + INT_CONSTANT(SPLICE_F_MORE), +# endif // defined(SPLICE_F_MORE) +# if defined(SPLICE_F_GIFT) + INT_CONSTANT(SPLICE_F_GIFT), +# endif // defined(SPLICE_F_GIFT) +#endif // defined(XP_UNIX) +// copyfile +#if defined(COPYFILE_DATA) + INT_CONSTANT(COPYFILE_DATA), + INT_CONSTANT(COPYFILE_EXCL), + INT_CONSTANT(COPYFILE_XATTR), + INT_CONSTANT(COPYFILE_STAT), + INT_CONSTANT(COPYFILE_ACL), + INT_CONSTANT(COPYFILE_MOVE), +#endif // defined(COPYFILE_DATA) + + // error values + INT_CONSTANT(EACCES), + INT_CONSTANT(EAGAIN), + INT_CONSTANT(EBADF), + INT_CONSTANT(EEXIST), + INT_CONSTANT(EFAULT), + INT_CONSTANT(EFBIG), + INT_CONSTANT(EINVAL), + INT_CONSTANT(EINTR), + INT_CONSTANT(EIO), + INT_CONSTANT(EISDIR), +#if defined(ELOOP) // not defined with VC9 + INT_CONSTANT(ELOOP), +#endif // defined(ELOOP) + INT_CONSTANT(EMFILE), + INT_CONSTANT(ENAMETOOLONG), + INT_CONSTANT(ENFILE), + INT_CONSTANT(ENOENT), + INT_CONSTANT(ENOMEM), + INT_CONSTANT(ENOSPC), + INT_CONSTANT(ENOTDIR), + INT_CONSTANT(ENXIO), +#if defined(EOPNOTSUPP) // not defined with VC 9 + INT_CONSTANT(EOPNOTSUPP), +#endif // defined(EOPNOTSUPP) +#if defined(EOVERFLOW) // not defined with VC 9 + INT_CONSTANT(EOVERFLOW), +#endif // defined(EOVERFLOW) + INT_CONSTANT(EPERM), + INT_CONSTANT(ERANGE), +#if defined(ETIMEDOUT) // not defined with VC 9 + INT_CONSTANT(ETIMEDOUT), +#endif // defined(ETIMEDOUT) +#if defined(EWOULDBLOCK) // not defined with VC 9 + INT_CONSTANT(EWOULDBLOCK), +#endif // defined(EWOULDBLOCK) + INT_CONSTANT(EXDEV), + +#if defined(DT_UNKNOWN) + // Constants for |readdir| + INT_CONSTANT(DT_UNKNOWN), + INT_CONSTANT(DT_FIFO), + INT_CONSTANT(DT_CHR), + INT_CONSTANT(DT_DIR), + INT_CONSTANT(DT_BLK), + INT_CONSTANT(DT_REG), + INT_CONSTANT(DT_LNK), + INT_CONSTANT(DT_SOCK), +#endif // defined(DT_UNKNOWN) + +#if defined(XP_UNIX) + // Constants for |stat| + INT_CONSTANT(S_IFMT), + INT_CONSTANT(S_IFIFO), + INT_CONSTANT(S_IFCHR), + INT_CONSTANT(S_IFDIR), + INT_CONSTANT(S_IFBLK), + INT_CONSTANT(S_IFREG), + INT_CONSTANT(S_IFLNK), // not defined on minGW + INT_CONSTANT(S_IFSOCK), // not defined on minGW +#endif // defined(XP_UNIX) + + INT_CONSTANT(PATH_MAX), + +// Constants used to define data structures +// +// Many data structures have different fields/sizes/etc. on +// various OSes / versions of the same OS / platforms. For these +// data structures, we need to compute and export from C the size +// and, if necessary, the offset of fields, so as to be able to +// define the structure in JS. + +#if defined(XP_UNIX) + // The size of |mode_t|. + {"OSFILE_SIZEOF_MODE_T", JS::Int32Value(sizeof(mode_t))}, + + // The size of |gid_t|. + {"OSFILE_SIZEOF_GID_T", JS::Int32Value(sizeof(gid_t))}, + + // The size of |uid_t|. + {"OSFILE_SIZEOF_UID_T", JS::Int32Value(sizeof(uid_t))}, + + // The size of |time_t|. + {"OSFILE_SIZEOF_TIME_T", JS::Int32Value(sizeof(time_t))}, + + // The size of |fsblkcnt_t|. + {"OSFILE_SIZEOF_FSBLKCNT_T", JS::Int32Value(sizeof(fsblkcnt_t))}, + +# if !defined(ANDROID) + // The size of |posix_spawn_file_actions_t|. + {"OSFILE_SIZEOF_POSIX_SPAWN_FILE_ACTIONS_T", + JS::Int32Value(sizeof(posix_spawn_file_actions_t))}, + + // The size of |posix_spawnattr_t|. + {"OSFILE_SIZEOF_POSIX_SPAWNATTR_T", + JS::Int32Value(sizeof(posix_spawnattr_t))}, +# endif // !defined(ANDROID) + + // Defining |dirent|. + // Size + {"OSFILE_SIZEOF_DIRENT", JS::Int32Value(sizeof(dirent))}, + + // Defining |flock|. + {"OSFILE_SIZEOF_FLOCK", JS::Int32Value(sizeof(struct flock))}, + {"OSFILE_OFFSETOF_FLOCK_L_START", + JS::Int32Value(offsetof(struct flock, l_start))}, + {"OSFILE_OFFSETOF_FLOCK_L_LEN", + JS::Int32Value(offsetof(struct flock, l_len))}, + {"OSFILE_OFFSETOF_FLOCK_L_PID", + JS::Int32Value(offsetof(struct flock, l_pid))}, + {"OSFILE_OFFSETOF_FLOCK_L_TYPE", + JS::Int32Value(offsetof(struct flock, l_type))}, + {"OSFILE_OFFSETOF_FLOCK_L_WHENCE", + JS::Int32Value(offsetof(struct flock, l_whence))}, + + // Offset of field |d_name|. + {"OSFILE_OFFSETOF_DIRENT_D_NAME", + JS::Int32Value(offsetof(struct dirent, d_name))}, + // An upper bound to the length of field |d_name| of struct |dirent|. + // (may not be exact, depending on padding). + {"OSFILE_SIZEOF_DIRENT_D_NAME", + JS::Int32Value(sizeof(struct dirent) - offsetof(struct dirent, d_name))}, + + // Defining |timeval|. + {"OSFILE_SIZEOF_TIMEVAL", JS::Int32Value(sizeof(struct timeval))}, + {"OSFILE_OFFSETOF_TIMEVAL_TV_SEC", + JS::Int32Value(offsetof(struct timeval, tv_sec))}, + {"OSFILE_OFFSETOF_TIMEVAL_TV_USEC", + JS::Int32Value(offsetof(struct timeval, tv_usec))}, + +# if defined(DT_UNKNOWN) + // Position of field |d_type| in |dirent| + // Not strictly posix, but seems defined on all platforms + // except mingw32. + {"OSFILE_OFFSETOF_DIRENT_D_TYPE", + JS::Int32Value(offsetof(struct dirent, d_type))}, +# endif // defined(DT_UNKNOWN) + +// Under MacOS X and BSDs, |dirfd| is a macro rather than a +// function, so we need a little help to get it to work +# if defined(dirfd) + {"OSFILE_SIZEOF_DIR", JS::Int32Value(sizeof(DIR))}, + + {"OSFILE_OFFSETOF_DIR_DD_FD", JS::Int32Value(offsetof(DIR, __dd_fd))}, +# endif + + // Defining |stat| + + {"OSFILE_SIZEOF_STAT", JS::Int32Value(sizeof(struct stat))}, + + {"OSFILE_OFFSETOF_STAT_ST_MODE", + JS::Int32Value(offsetof(struct stat, st_mode))}, + {"OSFILE_OFFSETOF_STAT_ST_UID", + JS::Int32Value(offsetof(struct stat, st_uid))}, + {"OSFILE_OFFSETOF_STAT_ST_GID", + JS::Int32Value(offsetof(struct stat, st_gid))}, + {"OSFILE_OFFSETOF_STAT_ST_SIZE", + JS::Int32Value(offsetof(struct stat, st_size))}, + +# if defined(HAVE_ST_ATIMESPEC) + {"OSFILE_OFFSETOF_STAT_ST_ATIME", + JS::Int32Value(offsetof(struct stat, st_atimespec))}, + {"OSFILE_OFFSETOF_STAT_ST_MTIME", + JS::Int32Value(offsetof(struct stat, st_mtimespec))}, + {"OSFILE_OFFSETOF_STAT_ST_CTIME", + JS::Int32Value(offsetof(struct stat, st_ctimespec))}, +# else + {"OSFILE_OFFSETOF_STAT_ST_ATIME", + JS::Int32Value(offsetof(struct stat, st_atime))}, + {"OSFILE_OFFSETOF_STAT_ST_MTIME", + JS::Int32Value(offsetof(struct stat, st_mtime))}, + {"OSFILE_OFFSETOF_STAT_ST_CTIME", + JS::Int32Value(offsetof(struct stat, st_ctime))}, +# endif // defined(HAVE_ST_ATIME) + +// Several OSes have a birthtime field. For the moment, supporting only Darwin. +# if defined(_DARWIN_FEATURE_64_BIT_INODE) + {"OSFILE_OFFSETOF_STAT_ST_BIRTHTIME", + JS::Int32Value(offsetof(struct stat, st_birthtime))}, +# endif // defined(_DARWIN_FEATURE_64_BIT_INODE) + + // Defining |statvfs| + + {"OSFILE_SIZEOF_STATVFS", JS::Int32Value(sizeof(struct statvfs))}, + + // We have no guarantee how big "f_frsize" is, so we have to calculate that. + {"OSFILE_SIZEOF_STATVFS_F_FRSIZE", + JS::Int32Value(DOM_SIZEOF_MEMBER(struct statvfs, f_frsize))}, + {"OSFILE_OFFSETOF_STATVFS_F_FRSIZE", + JS::Int32Value(offsetof(struct statvfs, f_frsize))}, + {"OSFILE_OFFSETOF_STATVFS_F_BAVAIL", + JS::Int32Value(offsetof(struct statvfs, f_bavail))}, + +#endif // defined(XP_UNIX) + +// System configuration + +// Under MacOSX, to avoid using deprecated functions that do not +// match the constants we define in this object (including +// |sizeof|/|offsetof| stuff, but not only), for a number of +// functions, we need to use functions with a $INODE64 suffix. +// That is true on Intel-based mac when the _DARWIN_FEATURE_64_BIT_INODE +// macro is set. But not on Apple Silicon. +#if defined(_DARWIN_FEATURE_64_BIT_INODE) && !defined(__aarch64__) + {"_DARWIN_INODE64_SYMBOLS", JS::Int32Value(1)}, +#endif // defined(_DARWIN_FEATURE_64_BIT_INODE) + +// Similar feature for Linux +#if defined(_STAT_VER) + INT_CONSTANT(_STAT_VER), +#endif // defined(_STAT_VER) + + PROP_END}; + +#if defined(XP_WIN) +/** + * The properties defined in windows.h. + * + * If you extend this list of properties, please + * separate categories ("errors", "open", etc.), + * keep properties organized by alphabetical order + * and #ifdef-away properties that are not portable. + */ +static const dom::ConstantSpec gWinProperties[] = { + // FormatMessage flags + INT_CONSTANT(FORMAT_MESSAGE_FROM_SYSTEM), + INT_CONSTANT(FORMAT_MESSAGE_IGNORE_INSERTS), + + // The max length of paths + INT_CONSTANT(MAX_PATH), + + // CreateFile desired access + INT_CONSTANT(GENERIC_ALL), + INT_CONSTANT(GENERIC_EXECUTE), + INT_CONSTANT(GENERIC_READ), + INT_CONSTANT(GENERIC_WRITE), + + // CreateFile share mode + INT_CONSTANT(FILE_SHARE_DELETE), + INT_CONSTANT(FILE_SHARE_READ), + INT_CONSTANT(FILE_SHARE_WRITE), + + // CreateFile creation disposition + INT_CONSTANT(CREATE_ALWAYS), + INT_CONSTANT(CREATE_NEW), + INT_CONSTANT(OPEN_ALWAYS), + INT_CONSTANT(OPEN_EXISTING), + INT_CONSTANT(TRUNCATE_EXISTING), + + // CreateFile attributes + INT_CONSTANT(FILE_ATTRIBUTE_ARCHIVE), + INT_CONSTANT(FILE_ATTRIBUTE_DIRECTORY), + INT_CONSTANT(FILE_ATTRIBUTE_HIDDEN), + INT_CONSTANT(FILE_ATTRIBUTE_NORMAL), + INT_CONSTANT(FILE_ATTRIBUTE_READONLY), + INT_CONSTANT(FILE_ATTRIBUTE_REPARSE_POINT), + INT_CONSTANT(FILE_ATTRIBUTE_SYSTEM), + INT_CONSTANT(FILE_ATTRIBUTE_TEMPORARY), + INT_CONSTANT(FILE_FLAG_BACKUP_SEMANTICS), + + // CreateFile error constant + {"INVALID_HANDLE_VALUE", JS::Int32Value(INT_PTR(INVALID_HANDLE_VALUE))}, + + // CreateFile flags + INT_CONSTANT(FILE_FLAG_DELETE_ON_CLOSE), + + // SetFilePointer methods + INT_CONSTANT(FILE_BEGIN), + INT_CONSTANT(FILE_CURRENT), + INT_CONSTANT(FILE_END), + + // SetFilePointer error constant + UINT_CONSTANT(INVALID_SET_FILE_POINTER), + + // File attributes + INT_CONSTANT(FILE_ATTRIBUTE_DIRECTORY), + + // MoveFile flags + INT_CONSTANT(MOVEFILE_COPY_ALLOWED), + INT_CONSTANT(MOVEFILE_REPLACE_EXISTING), + + // GetFileAttributes error constant + INT_CONSTANT(INVALID_FILE_ATTRIBUTES), + + // GetNamedSecurityInfo and SetNamedSecurityInfo constants + INT_CONSTANT(UNPROTECTED_DACL_SECURITY_INFORMATION), + INT_CONSTANT(SE_FILE_OBJECT), + INT_CONSTANT(DACL_SECURITY_INFORMATION), + + // Errors + INT_CONSTANT(ERROR_INVALID_HANDLE), + INT_CONSTANT(ERROR_ACCESS_DENIED), + INT_CONSTANT(ERROR_DIR_NOT_EMPTY), + INT_CONSTANT(ERROR_FILE_EXISTS), + INT_CONSTANT(ERROR_ALREADY_EXISTS), + INT_CONSTANT(ERROR_FILE_NOT_FOUND), + INT_CONSTANT(ERROR_NO_MORE_FILES), + INT_CONSTANT(ERROR_PATH_NOT_FOUND), + INT_CONSTANT(ERROR_BAD_ARGUMENTS), + INT_CONSTANT(ERROR_SHARING_VIOLATION), + INT_CONSTANT(ERROR_NOT_SUPPORTED), + + PROP_END}; +#endif // defined(XP_WIN) + +/** + * Get a field of an object as an object. + * + * If the field does not exist, create it. If it exists but is not an + * object, throw a JS error. + */ +JSObject* GetOrCreateObjectProperty(JSContext* cx, + JS::Handle<JSObject*> aObject, + const char* aProperty) { + JS::Rooted<JS::Value> val(cx); + if (!JS_GetProperty(cx, aObject, aProperty, &val)) { + return nullptr; + } + if (!val.isUndefined()) { + if (val.isObject()) { + return &val.toObject(); + } + + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, + JSMSG_UNEXPECTED_TYPE, aProperty, + "not an object"); + return nullptr; + } + return JS_DefineObject(cx, aObject, aProperty, nullptr, JSPROP_ENUMERATE); +} + +/** + * Set a property of an object from a nsString. + * + * If the nsString is void (i.e. IsVoid is true), do nothing. + */ +bool SetStringProperty(JSContext* cx, JS::Handle<JSObject*> aObject, + const char* aProperty, const nsString aValue) { + if (aValue.IsVoid()) { + return true; + } + JSString* strValue = JS_NewUCStringCopyZ(cx, aValue.get()); + NS_ENSURE_TRUE(strValue, false); + JS::Rooted<JS::Value> valValue(cx, JS::StringValue(strValue)); + return JS_SetProperty(cx, aObject, aProperty, valValue); +} + +/** + * Define OS-specific constants. + * + * This function creates or uses JS object |OS.Constants| to store + * all its constants. + */ +bool OSFileConstantsService::DefineOSFileConstants( + JSContext* aCx, JS::Handle<JSObject*> aGlobal) { + if (!mInitialized) { + JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr, + JSMSG_CANT_OPEN, "OSFileConstants", + "initialization has failed"); + return false; + } + + JS::Rooted<JSObject*> objOS(aCx); + if (!(objOS = GetOrCreateObjectProperty(aCx, aGlobal, "OS"))) { + return false; + } + JS::Rooted<JSObject*> objConstants(aCx); + if (!(objConstants = GetOrCreateObjectProperty(aCx, objOS, "Constants"))) { + return false; + } + + // Build OS.Constants.libc + + JS::Rooted<JSObject*> objLibc(aCx); + if (!(objLibc = GetOrCreateObjectProperty(aCx, objConstants, "libc"))) { + return false; + } + if (!dom::DefineConstants(aCx, objLibc, gLibcProperties)) { + return false; + } + +#if defined(XP_WIN) + // Build OS.Constants.Win + + JS::Rooted<JSObject*> objWin(aCx); + if (!(objWin = GetOrCreateObjectProperty(aCx, objConstants, "Win"))) { + return false; + } + if (!dom::DefineConstants(aCx, objWin, gWinProperties)) { + return false; + } +#endif // defined(XP_WIN) + + // Build OS.Constants.Sys + + JS::Rooted<JSObject*> objSys(aCx); + if (!(objSys = GetOrCreateObjectProperty(aCx, objConstants, "Sys"))) { + return false; + } + + nsCOMPtr<nsIXULRuntime> runtime = + do_GetService(XULRUNTIME_SERVICE_CONTRACTID); + if (runtime) { + nsAutoCString os; + DebugOnly<nsresult> rv = runtime->GetOS(os); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + JSString* strVersion = JS_NewStringCopyZ(aCx, os.get()); + if (!strVersion) { + return false; + } + + JS::Rooted<JS::Value> valVersion(aCx, JS::StringValue(strVersion)); + if (!JS_SetProperty(aCx, objSys, "Name", valVersion)) { + return false; + } + } + +#if defined(DEBUG) + JS::Rooted<JS::Value> valDebug(aCx, JS::TrueValue()); + if (!JS_SetProperty(aCx, objSys, "DEBUG", valDebug)) { + return false; + } +#endif + +#if defined(HAVE_64BIT_BUILD) + JS::Rooted<JS::Value> valBits(aCx, JS::Int32Value(64)); +#else + JS::Rooted<JS::Value> valBits(aCx, JS::Int32Value(32)); +#endif // defined (HAVE_64BIT_BUILD) + if (!JS_SetProperty(aCx, objSys, "bits", valBits)) { + return false; + } + + if (!JS_DefineProperty( + aCx, objSys, "umask", mUserUmask, + JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)) { + return false; + } + + // Build OS.Constants.Path + + JS::Rooted<JSObject*> objPath(aCx); + if (!(objPath = GetOrCreateObjectProperty(aCx, objConstants, "Path"))) { + return false; + } + + // Locate libxul + // Note that we don't actually provide the full path, only the name of the + // library, which is sufficient to link to the library using js-ctypes. + +#if defined(XP_MACOSX) + // Under MacOS X, for some reason, libxul is called simply "XUL", + // and we need to provide the full path. + nsAutoString libxul; + libxul.Append(mPaths->libDir); + libxul.AppendLiteral("/XUL"); +#else + // On other platforms, libxul is a library "xul" with regular + // library prefix/suffix. + nsAutoString libxul; + libxul.AppendLiteral(MOZ_DLL_PREFIX); + libxul.AppendLiteral("xul"); + libxul.AppendLiteral(MOZ_DLL_SUFFIX); +#endif // defined(XP_MACOSX) + + if (!SetStringProperty(aCx, objPath, "libxul", libxul)) { + return false; + } + + if (!SetStringProperty(aCx, objPath, "libDir", mPaths->libDir)) { + return false; + } + + if (!SetStringProperty(aCx, objPath, "tmpDir", mPaths->tmpDir)) { + return false; + } + + // Configure profileDir only if it is available at this stage + if (!mPaths->profileDir.IsVoid() && + !SetStringProperty(aCx, objPath, "profileDir", mPaths->profileDir)) { + return false; + } + + // Configure localProfileDir only if it is available at this stage + if (!mPaths->localProfileDir.IsVoid() && + !SetStringProperty(aCx, objPath, "localProfileDir", + mPaths->localProfileDir)) { + return false; + } + + // sqlite3 is linked from different places depending on the platform + nsAutoString libsqlite3; +#if defined(ANDROID) + // On Android, we use the system's libsqlite3 + libsqlite3.AppendLiteral(MOZ_DLL_PREFIX); + libsqlite3.AppendLiteral("sqlite3"); + libsqlite3.AppendLiteral(MOZ_DLL_SUFFIX); +#elif defined(XP_WIN) + // On Windows, for some reason, this is part of nss3.dll + libsqlite3.AppendLiteral(MOZ_DLL_PREFIX); + libsqlite3.AppendLiteral("nss3"); + libsqlite3.AppendLiteral(MOZ_DLL_SUFFIX); +#else + // On other platforms, we link sqlite3 into libxul + libsqlite3 = libxul; +#endif // defined(ANDROID) || defined(XP_WIN) + + if (!SetStringProperty(aCx, objPath, "libsqlite3", libsqlite3)) { + return false; + } + + return true; +} + +NS_IMPL_ISUPPORTS(OSFileConstantsService, nsIOSFileConstantsService, + nsIObserver) + +/* static */ +already_AddRefed<OSFileConstantsService> OSFileConstantsService::GetOrCreate() { + if (!gInstance) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<OSFileConstantsService> service = new OSFileConstantsService(); + nsresult rv = service->InitOSFileConstants(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + gInstance = std::move(service); + ClearOnShutdown(&gInstance); + } + + RefPtr<OSFileConstantsService> copy = gInstance; + return copy.forget(); +} + +OSFileConstantsService::OSFileConstantsService() + : mInitialized(false), mUserUmask(0) { + MOZ_ASSERT(NS_IsMainThread()); +} + +OSFileConstantsService::~OSFileConstantsService() { + MOZ_ASSERT(NS_IsMainThread()); +} + +NS_IMETHODIMP +OSFileConstantsService::Init(JSContext* aCx) { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv = InitOSFileConstants(); + if (NS_FAILED(rv)) { + return rv; + } + + mozJSComponentLoader* loader = mozJSComponentLoader::Get(); + JS::Rooted<JSObject*> targetObj(aCx); + loader->FindTargetObject(aCx, &targetObj); + + if (!DefineOSFileConstants(aCx, targetObj)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +} // namespace mozilla diff --git a/dom/system/OSFileConstants.h b/dom/system/OSFileConstants.h new file mode 100644 index 0000000000..73b5911579 --- /dev/null +++ b/dom/system/OSFileConstants.h @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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_osfileconstants_h__ +#define mozilla_osfileconstants_h__ + +#include "nsIObserver.h" +#include "nsIOSFileConstantsService.h" +#include "mozilla/Attributes.h" + +namespace mozilla { + +/** + * XPConnect initializer, for use in the main thread. + * This class is thread-safe but it must be first be initialized on the + * main-thread. + */ +class OSFileConstantsService final : public nsIOSFileConstantsService, + public nsIObserver { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOSFILECONSTANTSSERVICE + NS_DECL_NSIOBSERVER + + static already_AddRefed<OSFileConstantsService> GetOrCreate(); + + bool DefineOSFileConstants(JSContext* aCx, JS::Handle<JSObject*> aGlobal); + + private: + nsresult InitOSFileConstants(); + + OSFileConstantsService(); + ~OSFileConstantsService(); + + bool mInitialized; + + struct Paths; + UniquePtr<Paths> mPaths; + + /** + * (Unix) the umask, which goes in OS.Constants.Sys but + * can only be looked up (via the system-info service) + * on the main thread. + */ + uint32_t mUserUmask; +}; + +} // namespace mozilla + +#endif // mozilla_osfileconstants_h__ diff --git a/dom/system/PathUtils.cpp b/dom/system/PathUtils.cpp new file mode 100644 index 0000000000..98c7211b31 --- /dev/null +++ b/dom/system/PathUtils.cpp @@ -0,0 +1,517 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "PathUtils.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/DataMutex.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/Maybe.h" +#include "mozilla/MozPromise.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Result.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/Span.h" +#include "mozilla/dom/DOMParser.h" +#include "mozilla/dom/PathUtilsBinding.h" +#include "mozilla/dom/Promise.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsCOMPtr.h" +#include "nsDirectoryServiceUtils.h" +#include "nsIFile.h" +#include "nsIGlobalObject.h" +#include "nsLocalFile.h" +#include "nsNetUtil.h" +#include "nsString.h" + +namespace mozilla { +namespace dom { + +static constexpr auto ERROR_EMPTY_PATH = + "PathUtils does not support empty paths"_ns; +static constexpr auto ERROR_INITIALIZE_PATH = "Could not initialize path"_ns; +static constexpr auto ERROR_GET_PARENT = "Could not get parent path"_ns; +static constexpr auto ERROR_JOIN = "Could not append to path"_ns; +static constexpr auto ERROR_CREATE_UNIQUE = "Could not create unique path"_ns; + +static void ThrowError(ErrorResult& aErr, const nsresult aResult, + const nsCString& aMessage) { + nsAutoCStringN<32> errName; + GetErrorName(aResult, errName); + + nsAutoCStringN<256> formattedMsg; + formattedMsg.Append(aMessage); + formattedMsg.Append(": "_ns); + formattedMsg.Append(errName); + + switch (aResult) { + case NS_ERROR_FILE_UNRECOGNIZED_PATH: + aErr.ThrowOperationError(formattedMsg); + break; + + case NS_ERROR_FILE_ACCESS_DENIED: + aErr.ThrowInvalidAccessError(formattedMsg); + break; + + case NS_ERROR_FAILURE: + default: + aErr.ThrowUnknownError(formattedMsg); + break; + } +} + +StaticDataMutex<Maybe<PathUtils::DirectoryCache>> PathUtils::sDirCache{ + "sDirCache"}; + +/** + * Return the leaf name, including leading path separators in the case of + * Windows UNC drive paths. + * + * @param aFile The file whose leaf name is to be returned. + * @param aResult The string to hold the resulting leaf name. + * @param aParent The pre-computed parent of |aFile|. If not provided, it will + * be computed. + */ +static nsresult GetLeafNamePreservingRoot(nsIFile* aFile, nsString& aResult, + nsIFile* aParent = nullptr) { + MOZ_ASSERT(aFile); + + nsCOMPtr<nsIFile> parent = aParent; + if (!parent) { + MOZ_TRY(aFile->GetParent(getter_AddRefs(parent))); + } + + if (parent) { + return aFile->GetLeafName(aResult); + } + + // We have reached the root path. On Windows, the leafname for a UNC path + // will not have the leading backslashes, so we need to use the entire path + // here: + // + // * for a UNIX root path (/) this will be /; + // * for a Windows drive path (e.g., C:), this will be the drive path (C:); + // and + // * for a Windows UNC server path (e.g., \\\\server), this will be the full + // server path (\\\\server). + return aFile->GetPath(aResult); +} + +void PathUtils::Filename(const GlobalObject&, const nsAString& aPath, + nsString& aResult, ErrorResult& aErr) { + if (aPath.IsEmpty()) { + aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH); + return; + } + + nsCOMPtr<nsIFile> path = new nsLocalFile(); + if (nsresult rv = path->InitWithPath(aPath); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return; + } + + if (nsresult rv = GetLeafNamePreservingRoot(path, aResult); NS_FAILED(rv)) { + ThrowError(aErr, rv, "Could not get leaf name of path"_ns); + return; + } +} + +void PathUtils::Parent(const GlobalObject&, const nsAString& aPath, + nsString& aResult, ErrorResult& aErr) { + if (aPath.IsEmpty()) { + aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH); + return; + } + + nsCOMPtr<nsIFile> path = new nsLocalFile(); + if (nsresult rv = path->InitWithPath(aPath); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return; + } + + nsCOMPtr<nsIFile> parent; + if (nsresult rv = path->GetParent(getter_AddRefs(parent)); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_GET_PARENT); + return; + } + + if (parent) { + MOZ_ALWAYS_SUCCEEDS(parent->GetPath(aResult)); + } else { + aResult = VoidString(); + } +} + +void PathUtils::Join(const GlobalObject&, const Sequence<nsString>& aComponents, + nsString& aResult, ErrorResult& aErr) { + if (aComponents.IsEmpty()) { + return; + } + if (aComponents[0].IsEmpty()) { + aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH); + return; + } + + nsCOMPtr<nsIFile> path = new nsLocalFile(); + if (nsresult rv = path->InitWithPath(aComponents[0]); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return; + } + + const auto components = Span<const nsString>(aComponents).Subspan(1); + for (const auto& component : components) { + if (nsresult rv = path->Append(component); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_JOIN); + return; + } + } + + MOZ_ALWAYS_SUCCEEDS(path->GetPath(aResult)); +} + +void PathUtils::JoinRelative(const GlobalObject&, const nsAString& aBasePath, + const nsAString& aRelativePath, nsString& aResult, + ErrorResult& aErr) { + if (aRelativePath.IsEmpty()) { + aResult = aBasePath; + return; + } + + nsCOMPtr<nsIFile> path = new nsLocalFile(); + if (nsresult rv = path->InitWithPath(aBasePath); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return; + } + + if (nsresult rv = path->AppendRelativePath(aRelativePath); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_JOIN); + return; + } + + MOZ_ALWAYS_SUCCEEDS(path->GetPath(aResult)); +} + +void PathUtils::CreateUniquePath(const GlobalObject&, const nsAString& aPath, + nsString& aResult, ErrorResult& aErr) { + if (aPath.IsEmpty()) { + aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH); + return; + } + + nsCOMPtr<nsIFile> path = new nsLocalFile(); + if (nsresult rv = path->InitWithPath(aPath); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return; + } + + if (nsresult rv = path->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600); + NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_CREATE_UNIQUE); + return; + } + + MOZ_ALWAYS_SUCCEEDS(path->GetPath(aResult)); +} + +void PathUtils::Normalize(const GlobalObject&, const nsAString& aPath, + nsString& aResult, ErrorResult& aErr) { + if (aPath.IsEmpty()) { + aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH); + return; + } + + nsCOMPtr<nsIFile> path = new nsLocalFile(); + if (nsresult rv = path->InitWithPath(aPath); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return; + } + + if (nsresult rv = path->Normalize(); NS_FAILED(rv)) { + ThrowError(aErr, rv, "Could not normalize path"_ns); + return; + } + + MOZ_ALWAYS_SUCCEEDS(path->GetPath(aResult)); +} + +void PathUtils::Split(const GlobalObject&, const nsAString& aPath, + nsTArray<nsString>& aResult, ErrorResult& aErr) { + if (aPath.IsEmpty()) { + aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH); + return; + } + + nsCOMPtr<nsIFile> path = new nsLocalFile(); + if (nsresult rv = path->InitWithPath(aPath); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return; + } + + while (path) { + auto* component = aResult.EmplaceBack(fallible); + if (!component) { + aErr.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + nsCOMPtr<nsIFile> parent; + if (nsresult rv = path->GetParent(getter_AddRefs(parent)); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_GET_PARENT); + return; + } + + // GetLeafPreservingRoot cannot fail if we pass it a parent path. + MOZ_ALWAYS_SUCCEEDS(GetLeafNamePreservingRoot(path, *component, parent)); + + path = parent; + } + + aResult.Reverse(); +} + +void PathUtils::ToFileURI(const GlobalObject&, const nsAString& aPath, + nsCString& aResult, ErrorResult& aErr) { + if (aPath.IsEmpty()) { + aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH); + return; + } + + nsCOMPtr<nsIFile> path = new nsLocalFile(); + if (nsresult rv = path->InitWithPath(aPath); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return; + } + + nsCOMPtr<nsIURI> uri; + if (nsresult rv = NS_NewFileURI(getter_AddRefs(uri), path); NS_FAILED(rv)) { + ThrowError(aErr, rv, "Could not initialize File URI"_ns); + return; + } + + if (nsresult rv = uri->GetSpec(aResult); NS_FAILED(rv)) { + ThrowError(aErr, rv, "Could not retrieve URI spec"_ns); + return; + } +} + +already_AddRefed<Promise> PathUtils::GetProfileDir(const GlobalObject& aGlobal, + ErrorResult& aErr) { + auto guard = sDirCache.Lock(); + return DirectoryCache::Ensure(guard.ref()) + .GetDirectory(aGlobal, aErr, DirectoryCache::Directory::Profile); +} + +already_AddRefed<Promise> PathUtils::GetLocalProfileDir( + const GlobalObject& aGlobal, ErrorResult& aErr) { + auto guard = sDirCache.Lock(); + return DirectoryCache::Ensure(guard.ref()) + .GetDirectory(aGlobal, aErr, DirectoryCache::Directory::LocalProfile); +} + +already_AddRefed<Promise> PathUtils::GetTempDir(const GlobalObject& aGlobal, + ErrorResult& aErr) { + auto guard = sDirCache.Lock(); + return DirectoryCache::Ensure(guard.ref()) + .GetDirectory(aGlobal, aErr, DirectoryCache::Directory::Temp); +} + +PathUtils::DirectoryCache::DirectoryCache() { + mProfileDir.SetIsVoid(true); + mLocalProfileDir.SetIsVoid(true); + mTempDir.SetIsVoid(true); +} + +PathUtils::DirectoryCache& PathUtils::DirectoryCache::Ensure( + Maybe<PathUtils::DirectoryCache>& aCache) { + if (aCache.isNothing()) { + aCache.emplace(); + + auto clearAtShutdown = []() { + RunOnShutdown([]() { + auto cache = PathUtils::sDirCache.Lock(); + cache->reset(); + }); + }; + + if (NS_IsMainThread()) { + clearAtShutdown(); + } else { + NS_DispatchToMainThread( + NS_NewRunnableFunction(__func__, std::move(clearAtShutdown))); + } + } + + return aCache.ref(); +} + +already_AddRefed<Promise> PathUtils::DirectoryCache::GetDirectory( + const GlobalObject& aGlobal, ErrorResult& aErr, + const Directory aRequestedDir) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<Promise> promise = Promise::Create(global, aErr); + if (aErr.Failed()) { + return nullptr; + } + + if (RefPtr<PopulateDirectoriesPromise> p = + PopulateDirectories(aRequestedDir)) { + p->Then( + GetCurrentSerialEventTarget(), __func__, + [promise, aRequestedDir](const Ok&) { + auto cache = PathUtils::sDirCache.Lock(); + cache.ref()->ResolveWithDirectory(promise, aRequestedDir); + }, + [promise](const nsresult& aRv) { promise->MaybeReject(aRv); }); + } else { + ResolveWithDirectory(promise, aRequestedDir); + } + + return promise.forget(); +} + +void PathUtils::DirectoryCache::ResolveWithDirectory( + Promise* aPromise, const Directory aRequestedDir) { + switch (aRequestedDir) { + case Directory::Profile: + MOZ_RELEASE_ASSERT(!mProfileDir.IsVoid()); + aPromise->MaybeResolve(mProfileDir); + break; + + case Directory::LocalProfile: + MOZ_RELEASE_ASSERT(!mLocalProfileDir.IsVoid()); + aPromise->MaybeResolve(mProfileDir); + break; + + case Directory::Temp: + MOZ_RELEASE_ASSERT(!mTempDir.IsVoid()); + aPromise->MaybeResolve(mTempDir); + break; + + default: + MOZ_ASSERT_UNREACHABLE(); + } +} + +already_AddRefed<PathUtils::DirectoryCache::PopulateDirectoriesPromise> +PathUtils::DirectoryCache::PopulateDirectories( + const PathUtils::DirectoryCache::Directory aRequestedDir) { + // If we have already resolved the requested directory, we can return + // immediately. + if ((aRequestedDir == Directory::Temp && !mTempDir.IsVoid()) || + (aRequestedDir == Directory::Profile && !mProfileDir.IsVoid()) || + (aRequestedDir == Directory::LocalProfile && + !mLocalProfileDir.IsVoid())) { + // We cannot have a state where mProfileDir is not populated but + // mLocalProfileDir is. + if (mProfileDir.IsVoid()) { + MOZ_RELEASE_ASSERT(mLocalProfileDir.IsVoid()); + } + return nullptr; + } + + // We have already fired off a request to populate the entry, so we can return + // the corresponding promise immediately. caller will queue a Thenable onto + // that promise to resolve/reject the request. + if (!mAllDirsPromise.IsEmpty()) { + return mAllDirsPromise.Ensure(__func__); + } + if (aRequestedDir != Directory::Temp && !mProfileDirsPromise.IsEmpty()) { + return mProfileDirsPromise.Ensure(__func__); + } + + RefPtr<PopulateDirectoriesPromise> promise; + if (aRequestedDir == Directory::Temp) { + promise = mAllDirsPromise.Ensure(__func__); + } else { + promise = mProfileDirsPromise.Ensure(__func__); + } + + if (NS_IsMainThread()) { + nsresult rv = PopulateDirectoriesImpl(aRequestedDir); + ResolvePopulateDirectoriesPromise(rv, aRequestedDir); + } else { + nsCOMPtr<nsIRunnable> runnable = + NS_NewRunnableFunction(__func__, [aRequestedDir]() { + auto cache = PathUtils::sDirCache.Lock(); + nsresult rv = cache.ref()->PopulateDirectoriesImpl(aRequestedDir); + cache.ref()->ResolvePopulateDirectoriesPromise(rv, aRequestedDir); + }); + NS_DispatchToMainThread(runnable.forget()); + } + + return promise.forget(); +} + +void PathUtils::DirectoryCache::ResolvePopulateDirectoriesPromise( + nsresult aRv, const PathUtils::DirectoryCache::Directory aRequestedDir) { + if (NS_SUCCEEDED(aRv)) { + if (aRequestedDir == Directory::Temp) { + mAllDirsPromise.Resolve(Ok{}, __func__); + } else { + mProfileDirsPromise.Resolve(Ok{}, __func__); + } + } else { + if (aRequestedDir == Directory::Temp) { + mAllDirsPromise.Reject(aRv, __func__); + } else { + mProfileDirsPromise.Reject(aRv, __func__); + } + } +} + +nsresult PathUtils::DirectoryCache::PopulateDirectoriesImpl( + const PathUtils::DirectoryCache::Directory aRequestedDir) { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIFile> path; + + // We only populate the temporary directory entry when specifically requested + // because the nsDirectoryService will do main thread IO to create the + // directory if it hasn't been created yet. + // + // Additionally, we cannot have second request to populate any of these + // directories if the first request succeeded, so assert that the + // corresponding fields are void. + if (aRequestedDir == Directory::Temp) { + MOZ_RELEASE_ASSERT(mTempDir.IsVoid()); + + MOZ_TRY(NS_GetSpecialDirectory(NS_APP_CONTENT_PROCESS_TEMP_DIR, + getter_AddRefs(path))); + MOZ_TRY(path->GetPath(mTempDir)); + } else if (aRequestedDir == Directory::Profile) { + MOZ_RELEASE_ASSERT(mProfileDir.IsVoid()); + MOZ_RELEASE_ASSERT(mLocalProfileDir.IsVoid()); + } else { + MOZ_RELEASE_ASSERT(aRequestedDir == Directory::LocalProfile); + MOZ_RELEASE_ASSERT(mProfileDir.IsVoid()); + MOZ_RELEASE_ASSERT(mLocalProfileDir.IsVoid()); + } + + if (mProfileDir.IsVoid()) { + MOZ_RELEASE_ASSERT(mLocalProfileDir.IsVoid()); + + nsString profileDir; + nsString localProfileDir; + + MOZ_TRY(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(path))); + MOZ_TRY(path->GetPath(profileDir)); + + MOZ_TRY(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_LOCAL_50_DIR, + getter_AddRefs(path))); + MOZ_TRY(path->GetPath(localProfileDir)); + + // We either set both of these or neither. + mProfileDir = std::move(profileDir); + mLocalProfileDir = std::move(localProfileDir); + } + + return NS_OK; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/system/PathUtils.h b/dom/system/PathUtils.h new file mode 100644 index 0000000000..05059d0d44 --- /dev/null +++ b/dom/system/PathUtils.h @@ -0,0 +1,201 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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_dom_PathUtils__ +#define mozilla_dom_PathUtils__ + +#include "mozilla/DataMutex.h" +#include "mozilla/Maybe.h" +#include "mozilla/MozPromise.h" +#include "mozilla/Mutex.h" +#include "mozilla/Result.h" +#include "mozilla/dom/Promise.h" +#include "nsString.h" +#include "nsTArray.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class PathUtils final { + public: + static void Filename(const GlobalObject&, const nsAString& aPath, + nsString& aResult, ErrorResult& aErr); + + static void Parent(const GlobalObject&, const nsAString& aPath, + nsString& aResult, ErrorResult& aErr); + + static void Join(const GlobalObject&, const Sequence<nsString>& aComponents, + nsString& aResult, ErrorResult& aErr); + + static void JoinRelative(const GlobalObject&, const nsAString& aBasePath, + const nsAString& aRelativePath, nsString& aResult, + ErrorResult& aErr); + + static void CreateUniquePath(const GlobalObject&, const nsAString& aPath, + nsString& aResult, ErrorResult& aErr); + + static void Normalize(const GlobalObject&, const nsAString& aPath, + nsString& aResult, ErrorResult& aErr); + + static void Split(const GlobalObject&, const nsAString& aPath, + nsTArray<nsString>& aResult, ErrorResult& aErr); + + static void ToFileURI(const GlobalObject&, const nsAString& aPath, + nsCString& aResult, ErrorResult& aErr); + + static already_AddRefed<Promise> GetProfileDir(const GlobalObject& aGlobal, + ErrorResult& aErr); + + static already_AddRefed<Promise> GetLocalProfileDir( + const GlobalObject& aGlobal, ErrorResult& aErr); + + static already_AddRefed<Promise> GetTempDir(const GlobalObject& aGlobal, + ErrorResult& aErr); + + private: + class DirectoryCache; + friend class DirectoryCache; + + static StaticDataMutex<Maybe<DirectoryCache>> sDirCache; +}; + +/** + * A cache of commonly used directories + */ +class PathUtils::DirectoryCache final { + public: + /** + * A directory that can be requested via |GetDirectory|. + */ + enum class Directory { + /** + * The user's profile directory. + */ + Profile, + /** + * The user's local profile directory. + */ + LocalProfile, + /** + * The temporary directory for the process. + */ + Temp, + }; + + DirectoryCache(); + DirectoryCache(const DirectoryCache&) = delete; + DirectoryCache(DirectoryCache&&) = delete; + DirectoryCache& operator=(const DirectoryCache&) = delete; + DirectoryCache& operator=(DirectoryCache&&) = delete; + + /** + * Ensure the cache is instantiated and schedule its destructor to run at + * shutdown. + * + * If the cache is already instantiated, this is a no-op. + * + * @param aCache The cache to ensure is instantiated. + */ + static DirectoryCache& Ensure(Maybe<DirectoryCache>& aCache); + + /** + * Request the path of a specific directory. + * + * If the directory has not been requested before, this may require a trip to + * the main thread to retrieve its path. + * + * @param aGlobalObject The JavaScript global. + * @param aErr The error result. + * @param aRequestedDir The directory for which the path is to be retrieved. + * + * @return A promise that resolves to the path of the requested directory. + */ + already_AddRefed<Promise> GetDirectory(const GlobalObject& aGlobalObject, + ErrorResult& aErr, + const Directory aRequestedDir); + + private: + using PopulateDirectoriesPromise = MozPromise<Ok, nsresult, false>; + + /** + * Populate the directory cache entry for the requested directory. + * + * @param aRequestedDir The directory cache entry that was requested via + * |GetDirectory|. + * + * @return If the requested directory has not been populated, this returns a + * promise that resolves when the population is complete. + * + * If the requested directory has already been populated, it returns + * nullptr instead. + */ + already_AddRefed<PopulateDirectoriesPromise> PopulateDirectories( + const Directory aRequestedDir); + + /** + * Initialize the requested directory cache entry. + * + * If |Directory::Temp| is requested, all cache entries will be populated. + * Otherwise, only the profile and local profile cache entries will be + * populated. The profile and local profile cache entries have no additional + * overhead for populating them, but the temp directory requires creating a + * directory on the main thread if it has not already happened. + * + * Must be called on the main thread. + * + * @param aRequestedDir The requested directory. + * + * @return The result of initializing directories. + */ + nsresult PopulateDirectoriesImpl(const Directory aRequestedDir); + + /** + * Resolve the internal PopulateDirectoriesPromise corresponding to + * |aRequestedDir| with the given result. + * + * This will allow all pending queries for the requested directory to resolve + * or be rejected. + * + * @param aRv The return value from PopulateDirectoriesImpl. + * @param aRequestedDir The requested directory cache entry. This is used to + * determine which internal MozPromiseHolder we are + * resolving. + */ + void ResolvePopulateDirectoriesPromise(nsresult aRv, + const Directory aRequestedDir); + + /** + * Resolve the given JS promise with the path of the requested directory + * + * Can only be called once the cache entry for the requested directory is + * populated. + * + * @param aPromise The JS promise to resolve. + * @param aRequestedDir The requested directory cache entry. + */ + void ResolveWithDirectory(Promise* aPromise, const Directory aRequestedDir); + + /** + * A promise that is resolved when |mProfileDir| and |mLocalProfileDir| are + * populated. + */ + MozPromiseHolder<PopulateDirectoriesPromise> mProfileDirsPromise; + nsString mProfileDir; + nsString mLocalProfileDir; + + /** + * A promise that is resolved when *all* cache entries are populated. + */ + MozPromiseHolder<PopulateDirectoriesPromise> mAllDirsPromise; + nsString mTempDir; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/system/android/AndroidLocationProvider.cpp b/dom/system/android/AndroidLocationProvider.cpp new file mode 100644 index 0000000000..dfa87a7fce --- /dev/null +++ b/dom/system/android/AndroidLocationProvider.cpp @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "Geolocation.h" +#include "GeolocationPosition.h" +#include "AndroidLocationProvider.h" +#include "mozilla/java/GeckoAppShellWrappers.h" + +using namespace mozilla; + +extern nsIGeolocationUpdate* gLocationCallback; + +NS_IMPL_ISUPPORTS(AndroidLocationProvider, nsIGeolocationProvider) + +AndroidLocationProvider::AndroidLocationProvider() {} + +AndroidLocationProvider::~AndroidLocationProvider() { + NS_IF_RELEASE(gLocationCallback); +} + +NS_IMETHODIMP +AndroidLocationProvider::Startup() { + if (java::GeckoAppShell::EnableLocation(true)) { + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +AndroidLocationProvider::Watch(nsIGeolocationUpdate* aCallback) { + NS_IF_RELEASE(gLocationCallback); + gLocationCallback = aCallback; + NS_IF_ADDREF(gLocationCallback); + return NS_OK; +} + +NS_IMETHODIMP +AndroidLocationProvider::Shutdown() { + if (java::GeckoAppShell::EnableLocation(false)) { + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +AndroidLocationProvider::SetHighAccuracy(bool enable) { + java::GeckoAppShell::EnableLocationHighAccuracy(enable); + return NS_OK; +} diff --git a/dom/system/android/AndroidLocationProvider.h b/dom/system/android/AndroidLocationProvider.h new file mode 100644 index 0000000000..e0d38f6c8f --- /dev/null +++ b/dom/system/android/AndroidLocationProvider.h @@ -0,0 +1,23 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 AndroidLocationProvider_h +#define AndroidLocationProvider_h + +#include "nsIGeolocationProvider.h" + +class AndroidLocationProvider final : public nsIGeolocationProvider { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONPROVIDER + + AndroidLocationProvider(); + + private: + ~AndroidLocationProvider(); +}; + +#endif /* AndroidLocationProvider_h */ diff --git a/dom/system/android/moz.build b/dom/system/android/moz.build new file mode 100644 index 0000000000..04dffba024 --- /dev/null +++ b/dom/system/android/moz.build @@ -0,0 +1,17 @@ +# -*- 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/. + +SOURCES += [ + "AndroidLocationProvider.cpp", + "nsHapticFeedback.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" +LOCAL_INCLUDES += [ + "/dom/geolocation", +] diff --git a/dom/system/android/nsHapticFeedback.cpp b/dom/system/android/nsHapticFeedback.cpp new file mode 100644 index 0000000000..87c77d8334 --- /dev/null +++ b/dom/system/android/nsHapticFeedback.cpp @@ -0,0 +1,18 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "nsHapticFeedback.h" +#include "mozilla/java/GeckoAppShellWrappers.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(nsHapticFeedback, nsIHapticFeedback) + +NS_IMETHODIMP +nsHapticFeedback::PerformSimpleAction(int32_t aType) { + java::GeckoAppShell::PerformHapticFeedback(aType == LongPress); + return NS_OK; +} diff --git a/dom/system/android/nsHapticFeedback.h b/dom/system/android/nsHapticFeedback.h new file mode 100644 index 0000000000..e55062058b --- /dev/null +++ b/dom/system/android/nsHapticFeedback.h @@ -0,0 +1,16 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "nsIHapticFeedback.h" + +class nsHapticFeedback final : public nsIHapticFeedback { + private: + ~nsHapticFeedback() {} + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHAPTICFEEDBACK +}; diff --git a/dom/system/components.conf b/dom/system/components.conf new file mode 100644 index 0000000000..3523c02147 --- /dev/null +++ b/dom/system/components.conf @@ -0,0 +1,17 @@ +# -*- 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/. + +Classes = [ + { + 'cid': '{77DA64D3-7458-4920-9491-86CC9914F904}', + 'contract_ids': [ + '@mozilla.org/geolocation/provider;1', + '@mozilla.org/geolocation/mls-provider;1', + ], + 'jsm': 'resource://gre/modules/NetworkGeolocationProvider.jsm', + 'constructor': 'NetworkGeolocationProvider', + }, +] diff --git a/dom/system/linux/GpsdLocationProvider.cpp b/dom/system/linux/GpsdLocationProvider.cpp new file mode 100644 index 0000000000..f25949a360 --- /dev/null +++ b/dom/system/linux/GpsdLocationProvider.cpp @@ -0,0 +1,426 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "GpsdLocationProvider.h" +#include <errno.h> +#include <gps.h> +#include "MLSFallback.h" +#include "mozilla/Atomics.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/LazyIdleThread.h" +#include "mozilla/dom/GeolocationPositionErrorBinding.h" +#include "GeolocationPosition.h" +#include "nsProxyRelease.h" +#include "nsThreadUtils.h" + +namespace mozilla { +namespace dom { + +// +// MLSGeolocationUpdate +// + +/** + * |MLSGeolocationUpdate| provides a fallback if gpsd is not supported. + */ +class GpsdLocationProvider::MLSGeolocationUpdate final + : public nsIGeolocationUpdate { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONUPDATE + + explicit MLSGeolocationUpdate(nsIGeolocationUpdate* aCallback); + + protected: + ~MLSGeolocationUpdate() = default; + + private: + nsCOMPtr<nsIGeolocationUpdate> mCallback; +}; + +GpsdLocationProvider::MLSGeolocationUpdate::MLSGeolocationUpdate( + nsIGeolocationUpdate* aCallback) + : mCallback(aCallback) { + MOZ_ASSERT(mCallback); +} + +// nsISupports +// + +NS_IMPL_ISUPPORTS(GpsdLocationProvider::MLSGeolocationUpdate, + nsIGeolocationUpdate); + +// nsIGeolocationUpdate +// + +NS_IMETHODIMP +GpsdLocationProvider::MLSGeolocationUpdate::Update( + nsIDOMGeoPosition* aPosition) { + nsCOMPtr<nsIDOMGeoPositionCoords> coords; + aPosition->GetCoords(getter_AddRefs(coords)); + if (!coords) { + return NS_ERROR_FAILURE; + } + + return mCallback->Update(aPosition); +} + +NS_IMETHODIMP +GpsdLocationProvider::MLSGeolocationUpdate::NotifyError(uint16_t aError) { + return mCallback->NotifyError(aError); +} + +// +// UpdateRunnable +// + +class GpsdLocationProvider::UpdateRunnable final : public Runnable { + public: + UpdateRunnable( + const nsMainThreadPtrHandle<GpsdLocationProvider>& aLocationProvider, + nsIDOMGeoPosition* aPosition) + : mLocationProvider(aLocationProvider), mPosition(aPosition) { + MOZ_ASSERT(mLocationProvider); + MOZ_ASSERT(mPosition); + } + + // nsIRunnable + // + + NS_IMETHOD Run() override { + mLocationProvider->Update(mPosition); + return NS_OK; + } + + private: + nsMainThreadPtrHandle<GpsdLocationProvider> mLocationProvider; + RefPtr<nsIDOMGeoPosition> mPosition; +}; + +// +// NotifyErrorRunnable +// + +class GpsdLocationProvider::NotifyErrorRunnable final : public Runnable { + public: + NotifyErrorRunnable( + const nsMainThreadPtrHandle<GpsdLocationProvider>& aLocationProvider, + int aError) + : mLocationProvider(aLocationProvider), mError(aError) { + MOZ_ASSERT(mLocationProvider); + } + + // nsIRunnable + // + + NS_IMETHOD Run() override { + mLocationProvider->NotifyError(mError); + return NS_OK; + } + + private: + nsMainThreadPtrHandle<GpsdLocationProvider> mLocationProvider; + int mError; +}; + +// +// PollRunnable +// + +/** + * |PollRunnable| does the main work of processing GPS data received + * from gpsd. libgps blocks while polling, so this runnable has to be + * executed on it's own thread. To cancel the poll runnable, invoke + * |StopRunning| and |PollRunnable| will stop within a reasonable time + * frame. + */ +class GpsdLocationProvider::PollRunnable final : public Runnable { + public: + PollRunnable( + const nsMainThreadPtrHandle<GpsdLocationProvider>& aLocationProvider) + : mLocationProvider(aLocationProvider), mRunning(true) { + MOZ_ASSERT(mLocationProvider); + } + + static bool IsSupported() { return GPSD_API_MAJOR_VERSION == 5; } + + bool IsRunning() const { return mRunning; } + + void StopRunning() { mRunning = false; } + + // nsIRunnable + // + + NS_IMETHOD Run() override { + int err; + + switch (GPSD_API_MAJOR_VERSION) { + case 5: + err = PollLoop5(); + break; + default: + err = GeolocationPositionError_Binding::POSITION_UNAVAILABLE; + break; + } + + if (err) { + NS_DispatchToMainThread( + MakeAndAddRef<NotifyErrorRunnable>(mLocationProvider, err)); + } + + mLocationProvider = nullptr; + + return NS_OK; + } + + protected: + int PollLoop5() { +#if GPSD_API_MAJOR_VERSION == 5 + static const int GPSD_WAIT_TIMEOUT_US = + 1000000; /* us to wait for GPS data */ + + struct gps_data_t gpsData; + + auto res = gps_open(nullptr, nullptr, &gpsData); + + if (res < 0) { + return ErrnoToError(errno); + } + + gps_stream(&gpsData, WATCH_ENABLE | WATCH_JSON, NULL); + + int err = 0; + + // nsGeoPositionCoords will convert NaNs to null for optional properties of + // the JavaScript Coordinates object. + double lat = 0; + double lon = 0; + double alt = UnspecifiedNaN<double>(); + double hError = 0; + double vError = UnspecifiedNaN<double>(); + double heading = UnspecifiedNaN<double>(); + double speed = UnspecifiedNaN<double>(); + + while (IsRunning()) { + errno = 0; + auto hasGpsData = gps_waiting(&gpsData, GPSD_WAIT_TIMEOUT_US); + + if (errno) { + err = ErrnoToError(errno); + break; + } + if (!hasGpsData) { + continue; /* woke up from timeout */ + } + + res = gps_read(&gpsData); + + if (res < 0) { + err = ErrnoToError(errno); + break; + } else if (!res) { + continue; /* no data available */ + } + + if (gpsData.status == STATUS_NO_FIX) { + continue; + } + + switch (gpsData.fix.mode) { + case MODE_3D: + if (!IsNaN(gpsData.fix.altitude)) { + alt = gpsData.fix.altitude; + } + [[fallthrough]]; + case MODE_2D: + if (!IsNaN(gpsData.fix.latitude)) { + lat = gpsData.fix.latitude; + } + if (!IsNaN(gpsData.fix.longitude)) { + lon = gpsData.fix.longitude; + } + if (!IsNaN(gpsData.fix.epx) && !IsNaN(gpsData.fix.epy)) { + hError = std::max(gpsData.fix.epx, gpsData.fix.epy); + } else if (!IsNaN(gpsData.fix.epx)) { + hError = gpsData.fix.epx; + } else if (!IsNaN(gpsData.fix.epy)) { + hError = gpsData.fix.epy; + } + if (!IsNaN(gpsData.fix.altitude)) { + alt = gpsData.fix.altitude; + } + if (!IsNaN(gpsData.fix.epv)) { + vError = gpsData.fix.epv; + } + if (!IsNaN(gpsData.fix.track)) { + heading = gpsData.fix.track; + } + if (!IsNaN(gpsData.fix.speed)) { + speed = gpsData.fix.speed; + } + break; + default: + continue; // There's no useful data in this fix; continue. + } + + NS_DispatchToMainThread(MakeAndAddRef<UpdateRunnable>( + mLocationProvider, + new nsGeoPosition(lat, lon, alt, hError, vError, heading, speed, + PR_Now() / PR_USEC_PER_MSEC))); + } + + gps_stream(&gpsData, WATCH_DISABLE, NULL); + gps_close(&gpsData); + + return err; +#else + return GeolocationPositionError_Binding::POSITION_UNAVAILABLE; +#endif // GPSD_MAJOR_API_VERSION + } + + static int ErrnoToError(int aErrno) { + switch (aErrno) { + case EACCES: + [[fallthrough]]; + case EPERM: + [[fallthrough]]; + case EROFS: + return GeolocationPositionError_Binding::PERMISSION_DENIED; + case ETIME: + [[fallthrough]]; + case ETIMEDOUT: + return GeolocationPositionError_Binding::TIMEOUT; + default: + return GeolocationPositionError_Binding::POSITION_UNAVAILABLE; + } + } + + private: + nsMainThreadPtrHandle<GpsdLocationProvider> mLocationProvider; + Atomic<bool> mRunning; +}; + +// +// GpsdLocationProvider +// + +const uint32_t GpsdLocationProvider::GPSD_POLL_THREAD_TIMEOUT_MS = 5000; + +GpsdLocationProvider::GpsdLocationProvider() {} + +GpsdLocationProvider::~GpsdLocationProvider() {} + +void GpsdLocationProvider::Update(nsIDOMGeoPosition* aPosition) { + if (!mCallback || !mPollRunnable) { + return; // not initialized or already shut down + } + + if (mMLSProvider) { + /* We got a location from gpsd, so let's cancel our MLS fallback. */ + mMLSProvider->Shutdown(); + mMLSProvider = nullptr; + } + + mCallback->Update(aPosition); +} + +void GpsdLocationProvider::NotifyError(int aError) { + if (!mCallback) { + return; // not initialized or already shut down + } + + if (!mMLSProvider) { + /* With gpsd failed, we restart MLS. It will be canceled once we + * get another location from gpsd. + */ + mMLSProvider = MakeAndAddRef<MLSFallback>(); + mMLSProvider->Startup(new MLSGeolocationUpdate(mCallback)); + } + + mCallback->NotifyError(aError); +} + +// nsISupports +// + +NS_IMPL_ISUPPORTS(GpsdLocationProvider, nsIGeolocationProvider) + +// nsIGeolocationProvider +// + +NS_IMETHODIMP +GpsdLocationProvider::Startup() { + if (!PollRunnable::IsSupported()) { + return NS_OK; // We'll fall back to MLS. + } + + if (mPollRunnable) { + return NS_OK; // already running + } + + RefPtr<PollRunnable> pollRunnable = + MakeAndAddRef<PollRunnable>(nsMainThreadPtrHandle<GpsdLocationProvider>( + new nsMainThreadPtrHolder<GpsdLocationProvider>(this))); + + // Use existing poll thread... + RefPtr<LazyIdleThread> pollThread = mPollThread; + + // ... or create a new one. + if (!pollThread) { + pollThread = MakeAndAddRef<LazyIdleThread>(GPSD_POLL_THREAD_TIMEOUT_MS, + "Gpsd poll thread"_ns, + LazyIdleThread::ManualShutdown); + } + + auto rv = pollThread->Dispatch(pollRunnable, NS_DISPATCH_NORMAL); + + if (NS_FAILED(rv)) { + return rv; + } + + mPollRunnable = pollRunnable.forget(); + mPollThread = pollThread.forget(); + + return NS_OK; +} + +NS_IMETHODIMP +GpsdLocationProvider::Watch(nsIGeolocationUpdate* aCallback) { + mCallback = aCallback; + + /* The MLS fallback will kick in after a few seconds if gpsd + * doesn't provide location information within time. Once we + * see the first message from gpsd, the fallback will be + * disabled in |Update|. + */ + mMLSProvider = MakeAndAddRef<MLSFallback>(); + mMLSProvider->Startup(new MLSGeolocationUpdate(aCallback)); + + return NS_OK; +} + +NS_IMETHODIMP +GpsdLocationProvider::Shutdown() { + if (mMLSProvider) { + mMLSProvider->Shutdown(); + mMLSProvider = nullptr; + } + + if (!mPollRunnable) { + return NS_OK; // not running + } + + mPollRunnable->StopRunning(); + mPollRunnable = nullptr; + + return NS_OK; +} + +NS_IMETHODIMP +GpsdLocationProvider::SetHighAccuracy(bool aHigh) { return NS_OK; } + +} // namespace dom +} // namespace mozilla diff --git a/dom/system/linux/GpsdLocationProvider.h b/dom/system/linux/GpsdLocationProvider.h new file mode 100644 index 0000000000..544f0e4c69 --- /dev/null +++ b/dom/system/linux/GpsdLocationProvider.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 GpsdLocationProvider_h +#define GpsdLocationProvider_h + +#include "nsCOMPtr.h" +#include "Geolocation.h" +#include "nsIGeolocationProvider.h" + +class MLSFallback; + +namespace mozilla { + +class LazyIdleThread; + +namespace dom { + +class GpsdLocationProvider final : public nsIGeolocationProvider { + class MLSGeolocationUpdate; + class NotifyErrorRunnable; + class PollRunnable; + class UpdateRunnable; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONPROVIDER + + GpsdLocationProvider(); + + private: + ~GpsdLocationProvider(); + + void Update(nsIDOMGeoPosition* aPosition); + void NotifyError(int aError); + + static const uint32_t GPSD_POLL_THREAD_TIMEOUT_MS; + + nsCOMPtr<nsIGeolocationUpdate> mCallback; + RefPtr<LazyIdleThread> mPollThread; + RefPtr<PollRunnable> mPollRunnable; + RefPtr<MLSFallback> mMLSProvider; +}; + +} // namespace dom +} // namespace mozilla + +#endif /* GpsLocationProvider_h */ diff --git a/dom/system/linux/moz.build b/dom/system/linux/moz.build new file mode 100644 index 0000000000..ab9d076deb --- /dev/null +++ b/dom/system/linux/moz.build @@ -0,0 +1,16 @@ +# -*- Mode: python; c-basic-offset: 4; 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/. + +if CONFIG["MOZ_GPSD"]: + SOURCES += ["GpsdLocationProvider.cpp"] + + CXXFLAGS += CONFIG["MOZ_GPSD_CFLAGS"] + + OS_LIBS += CONFIG["MOZ_GPSD_LIBS"] + + LOCAL_INCLUDES += ["/dom/geolocation"] + +FINAL_LIBRARY = "xul" diff --git a/dom/system/mac/CoreLocationLocationProvider.h b/dom/system/mac/CoreLocationLocationProvider.h new file mode 100644 index 0000000000..27b990cf9a --- /dev/null +++ b/dom/system/mac/CoreLocationLocationProvider.h @@ -0,0 +1,61 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "nsCOMPtr.h" +#include "nsIGeolocationProvider.h" +#include "mozilla/Attributes.h" + +/* + * The CoreLocationObjects class contains the CoreLocation objects + * we'll need. + * + * Declaring them directly in CoreLocationLocationProvider + * would require Objective-C++ syntax, which would contaminate all + * files that include this header and require them to be Objective-C++ + * as well. + * + * The solution then is to forward-declare CoreLocationObjects here and + * hold a pointer to it in CoreLocationLocationProvider, and only actually + * define it in CoreLocationLocationProvider.mm, thus making it safe + * for Geolocation.cpp, which is C++-only, to include this header. + */ +class CoreLocationObjects; +class MLSFallback; + +class CoreLocationLocationProvider : public nsIGeolocationProvider { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONPROVIDER + + CoreLocationLocationProvider(); + // MOZ_CAN_RUN_SCRIPT_BOUNDARY because we can't mark Objective-C methods as + // MOZ_CAN_RUN_SCRIPT as far as I can tell, and this method is called from + // Objective-C. + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void NotifyError(uint16_t aErrorCode); + void Update(nsIDOMGeoPosition* aSomewhere); + void CreateMLSFallbackProvider(); + void CancelMLSFallbackProvider(); + + private: + virtual ~CoreLocationLocationProvider() = default; + + CoreLocationObjects* mCLObjects; + nsCOMPtr<nsIGeolocationUpdate> mCallback; + RefPtr<MLSFallback> mMLSFallbackProvider; + + class MLSUpdate : public nsIGeolocationUpdate { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONUPDATE + + explicit MLSUpdate(CoreLocationLocationProvider& parentProvider); + + private: + CoreLocationLocationProvider& mParentLocationProvider; + virtual ~MLSUpdate() = default; + }; +}; diff --git a/dom/system/mac/CoreLocationLocationProvider.mm b/dom/system/mac/CoreLocationLocationProvider.mm new file mode 100644 index 0000000000..781eac9d46 --- /dev/null +++ b/dom/system/mac/CoreLocationLocationProvider.mm @@ -0,0 +1,246 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "nsCOMPtr.h" +#include "GeolocationPosition.h" +#include "nsIConsoleService.h" +#include "nsServiceManagerUtils.h" +#include "CoreLocationLocationProvider.h" +#include "nsCocoaFeatures.h" +#include "prtime.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/Telemetry.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/GeolocationPositionErrorBinding.h" +#include "MLSFallback.h" + +#include <CoreLocation/CLError.h> +#include <CoreLocation/CLLocation.h> +#include <CoreLocation/CLLocationManager.h> +#include <CoreLocation/CLLocationManagerDelegate.h> + +#include <objc/objc.h> +#include <objc/objc-runtime.h> + +#include "nsObjCExceptions.h" + +using namespace mozilla; + +static const CLLocationAccuracy kHIGH_ACCURACY = kCLLocationAccuracyBest; +static const CLLocationAccuracy kDEFAULT_ACCURACY = kCLLocationAccuracyNearestTenMeters; + +@interface LocationDelegate : NSObject <CLLocationManagerDelegate> { + CoreLocationLocationProvider* mProvider; +} + +- (id)init:(CoreLocationLocationProvider*)aProvider; +- (void)locationManager:(CLLocationManager*)aManager didFailWithError:(NSError*)aError; +- (void)locationManager:(CLLocationManager*)aManager didUpdateLocations:(NSArray*)locations; + +@end + +@implementation LocationDelegate +- (id)init:(CoreLocationLocationProvider*)aProvider { + if ((self = [super init])) { + mProvider = aProvider; + } + + return self; +} + +- (void)locationManager:(CLLocationManager*)aManager didFailWithError:(NSError*)aError { + nsCOMPtr<nsIConsoleService> console = do_GetService(NS_CONSOLESERVICE_CONTRACTID); + + NS_ENSURE_TRUE_VOID(console); + + NSString* message = + [@"Failed to acquire position: " stringByAppendingString:[aError localizedDescription]]; + + console->LogStringMessage(NS_ConvertUTF8toUTF16([message UTF8String]).get()); + + if ([aError code] == kCLErrorDenied) { + mProvider->NotifyError(dom::GeolocationPositionError_Binding::PERMISSION_DENIED); + return; + } + + // The CL provider does not fallback to GeoIP, so use NetworkGeolocationProvider for this. + // The concept here is: on error, hand off geolocation to MLS, which will then report + // back a location or error. + mProvider->CreateMLSFallbackProvider(); +} + +- (void)locationManager:(CLLocationManager*)aManager didUpdateLocations:(NSArray*)aLocations { + if (aLocations.count < 1) { + return; + } + + mProvider->CancelMLSFallbackProvider(); + + CLLocation* location = [aLocations objectAtIndex:0]; + + double altitude; + double altitudeAccuracy; + + // A negative verticalAccuracy indicates that the altitude value is invalid. + if (location.verticalAccuracy >= 0) { + altitude = location.altitude; + altitudeAccuracy = location.verticalAccuracy; + } else { + altitude = UnspecifiedNaN<double>(); + altitudeAccuracy = UnspecifiedNaN<double>(); + } + + double speed = location.speed >= 0 ? location.speed : UnspecifiedNaN<double>(); + + double heading = location.course >= 0 ? location.course : UnspecifiedNaN<double>(); + + // nsGeoPositionCoords will convert NaNs to null for optional properties of + // the JavaScript Coordinates object. + nsCOMPtr<nsIDOMGeoPosition> geoPosition = new nsGeoPosition( + location.coordinate.latitude, location.coordinate.longitude, altitude, + location.horizontalAccuracy, altitudeAccuracy, heading, speed, PR_Now() / PR_USEC_PER_MSEC); + + mProvider->Update(geoPosition); + Telemetry::Accumulate(Telemetry::GEOLOCATION_OSX_SOURCE_IS_MLS, false); +} +@end + +NS_IMPL_ISUPPORTS(CoreLocationLocationProvider::MLSUpdate, nsIGeolocationUpdate); + +CoreLocationLocationProvider::MLSUpdate::MLSUpdate(CoreLocationLocationProvider& parentProvider) + : mParentLocationProvider(parentProvider) {} + +NS_IMETHODIMP +CoreLocationLocationProvider::MLSUpdate::Update(nsIDOMGeoPosition* position) { + nsCOMPtr<nsIDOMGeoPositionCoords> coords; + position->GetCoords(getter_AddRefs(coords)); + if (!coords) { + return NS_ERROR_FAILURE; + } + mParentLocationProvider.Update(position); + Telemetry::Accumulate(Telemetry::GEOLOCATION_OSX_SOURCE_IS_MLS, true); + return NS_OK; +} + +NS_IMETHODIMP +CoreLocationLocationProvider::MLSUpdate::NotifyError(uint16_t error) { + mParentLocationProvider.NotifyError(error); + return NS_OK; +} + +class CoreLocationObjects { + public: + nsresult Init(CoreLocationLocationProvider* aProvider) { + mLocationManager = [[CLLocationManager alloc] init]; + NS_ENSURE_TRUE(mLocationManager, NS_ERROR_NOT_AVAILABLE); + + mLocationDelegate = [[LocationDelegate alloc] init:aProvider]; + NS_ENSURE_TRUE(mLocationDelegate, NS_ERROR_NOT_AVAILABLE); + + mLocationManager.desiredAccuracy = kDEFAULT_ACCURACY; + mLocationManager.delegate = mLocationDelegate; + + return NS_OK; + } + + ~CoreLocationObjects() { + if (mLocationManager) { + [mLocationManager release]; + } + + if (mLocationDelegate) { + [mLocationDelegate release]; + } + } + + LocationDelegate* mLocationDelegate; + CLLocationManager* mLocationManager; +}; + +NS_IMPL_ISUPPORTS(CoreLocationLocationProvider, nsIGeolocationProvider) + +CoreLocationLocationProvider::CoreLocationLocationProvider() + : mCLObjects(nullptr), mMLSFallbackProvider(nullptr) {} + +NS_IMETHODIMP +CoreLocationLocationProvider::Startup() { + if (!mCLObjects) { + auto clObjs = MakeUnique<CoreLocationObjects>(); + + nsresult rv = clObjs->Init(this); + NS_ENSURE_SUCCESS(rv, rv); + + mCLObjects = clObjs.release(); + } + + // Must be stopped before starting or response (success or failure) is not guaranteed + [mCLObjects->mLocationManager stopUpdatingLocation]; + [mCLObjects->mLocationManager startUpdatingLocation]; + return NS_OK; +} + +NS_IMETHODIMP +CoreLocationLocationProvider::Watch(nsIGeolocationUpdate* aCallback) { + if (mCallback) { + return NS_OK; + } + + mCallback = aCallback; + return NS_OK; +} + +NS_IMETHODIMP +CoreLocationLocationProvider::Shutdown() { + NS_ENSURE_STATE(mCLObjects); + + [mCLObjects->mLocationManager stopUpdatingLocation]; + + delete mCLObjects; + mCLObjects = nullptr; + + if (mMLSFallbackProvider) { + mMLSFallbackProvider->Shutdown(); + mMLSFallbackProvider = nullptr; + } + + return NS_OK; +} + +NS_IMETHODIMP +CoreLocationLocationProvider::SetHighAccuracy(bool aEnable) { + NS_ENSURE_STATE(mCLObjects); + + mCLObjects->mLocationManager.desiredAccuracy = (aEnable ? kHIGH_ACCURACY : kDEFAULT_ACCURACY); + + return NS_OK; +} + +void CoreLocationLocationProvider::Update(nsIDOMGeoPosition* aSomewhere) { + if (aSomewhere && mCallback) { + mCallback->Update(aSomewhere); + } +} +void CoreLocationLocationProvider::NotifyError(uint16_t aErrorCode) { + nsCOMPtr<nsIGeolocationUpdate> callback(mCallback); + callback->NotifyError(aErrorCode); +} +void CoreLocationLocationProvider::CreateMLSFallbackProvider() { + if (mMLSFallbackProvider) { + return; + } + + mMLSFallbackProvider = new MLSFallback(0); + mMLSFallbackProvider->Startup(new MLSUpdate(*this)); +} + +void CoreLocationLocationProvider::CancelMLSFallbackProvider() { + if (!mMLSFallbackProvider) { + return; + } + + mMLSFallbackProvider->Shutdown(); + mMLSFallbackProvider = nullptr; +} diff --git a/dom/system/mac/moz.build b/dom/system/mac/moz.build new file mode 100644 index 0000000000..6a10090793 --- /dev/null +++ b/dom/system/mac/moz.build @@ -0,0 +1,21 @@ +# -*- 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/. + +SOURCES += [ + "CoreLocationLocationProvider.mm", + "nsOSPermissionRequest.mm", +] + +EXPORTS += [ + "nsOSPermissionRequest.h", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" +LOCAL_INCLUDES += [ + "/dom/geolocation", +] diff --git a/dom/system/mac/nsOSPermissionRequest.h b/dom/system/mac/nsOSPermissionRequest.h new file mode 100644 index 0000000000..62e4360fee --- /dev/null +++ b/dom/system/mac/nsOSPermissionRequest.h @@ -0,0 +1,31 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 nsOSPermissionRequest_h__ +#define nsOSPermissionRequest_h__ + +#include "nsOSPermissionRequestBase.h" + +class nsOSPermissionRequest : public nsOSPermissionRequestBase { + public: + nsOSPermissionRequest(){}; + + NS_IMETHOD GetAudioCapturePermissionState(uint16_t* aAudio) override; + + NS_IMETHOD GetVideoCapturePermissionState(uint16_t* aVideo) override; + + NS_IMETHOD GetScreenCapturePermissionState(uint16_t* aScreen) override; + + NS_IMETHOD RequestVideoCapturePermission( + JSContext* aCx, mozilla::dom::Promise** aPromiseOut) override; + + NS_IMETHOD RequestAudioCapturePermission( + JSContext* aCx, mozilla::dom::Promise** aPromiseOut) override; + + NS_IMETHOD MaybeRequestScreenCapturePermission() override; +}; + +#endif diff --git a/dom/system/mac/nsOSPermissionRequest.mm b/dom/system/mac/nsOSPermissionRequest.mm new file mode 100644 index 0000000000..ccb4516fdc --- /dev/null +++ b/dom/system/mac/nsOSPermissionRequest.mm @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "nsOSPermissionRequest.h" + +#include "mozilla/dom/Promise.h" +#include "nsCocoaFeatures.h" +#include "nsCocoaUtils.h" + +using namespace mozilla; + +using mozilla::dom::Promise; + +NS_IMETHODIMP +nsOSPermissionRequest::GetAudioCapturePermissionState(uint16_t* aAudio) { + MOZ_ASSERT(aAudio); + + if (!nsCocoaFeatures::OnMojaveOrLater()) { + return nsOSPermissionRequestBase::GetAudioCapturePermissionState(aAudio); + } + + return nsCocoaUtils::GetAudioCapturePermissionState(*aAudio); +} + +NS_IMETHODIMP +nsOSPermissionRequest::GetVideoCapturePermissionState(uint16_t* aVideo) { + MOZ_ASSERT(aVideo); + + if (!nsCocoaFeatures::OnMojaveOrLater()) { + return nsOSPermissionRequestBase::GetVideoCapturePermissionState(aVideo); + } + + return nsCocoaUtils::GetVideoCapturePermissionState(*aVideo); +} + +NS_IMETHODIMP +nsOSPermissionRequest::GetScreenCapturePermissionState(uint16_t* aScreen) { + MOZ_ASSERT(aScreen); + + if (!nsCocoaFeatures::OnCatalinaOrLater()) { + return nsOSPermissionRequestBase::GetScreenCapturePermissionState(aScreen); + } + + return nsCocoaUtils::GetScreenCapturePermissionState(*aScreen); +} + +NS_IMETHODIMP +nsOSPermissionRequest::RequestVideoCapturePermission(JSContext* aCx, Promise** aPromiseOut) { + if (!nsCocoaFeatures::OnMojaveOrLater()) { + return nsOSPermissionRequestBase::RequestVideoCapturePermission(aCx, aPromiseOut); + } + + RefPtr<Promise> promiseHandle; + nsresult rv = GetPromise(aCx, promiseHandle); + if (NS_FAILED(rv)) { + return rv; + } + + rv = nsCocoaUtils::RequestVideoCapturePermission(promiseHandle); + promiseHandle.forget(aPromiseOut); + return rv; +} + +NS_IMETHODIMP +nsOSPermissionRequest::RequestAudioCapturePermission(JSContext* aCx, Promise** aPromiseOut) { + if (!nsCocoaFeatures::OnMojaveOrLater()) { + return nsOSPermissionRequestBase::RequestAudioCapturePermission(aCx, aPromiseOut); + } + + RefPtr<Promise> promiseHandle; + nsresult rv = GetPromise(aCx, promiseHandle); + if (NS_FAILED(rv)) { + return rv; + } + + rv = nsCocoaUtils::RequestAudioCapturePermission(promiseHandle); + promiseHandle.forget(aPromiseOut); + return rv; +} + +NS_IMETHODIMP +nsOSPermissionRequest::MaybeRequestScreenCapturePermission() { + if (!nsCocoaFeatures::OnCatalinaOrLater()) { + return nsOSPermissionRequestBase::MaybeRequestScreenCapturePermission(); + } + + return nsCocoaUtils::MaybeRequestScreenCapturePermission(); +} diff --git a/dom/system/moz.build b/dom/system/moz.build new file mode 100644 index 0000000000..9ba8669ea0 --- /dev/null +++ b/dom/system/moz.build @@ -0,0 +1,109 @@ +# -*- 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/. + +# This picks up *hapticfeedback* which is graveyard +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +with Files("*OSFile*"): + BUG_COMPONENT = ("Toolkit", "OS.File") + +with Files("*ocationProvider*"): + BUG_COMPONENT = ("Core", "DOM: Geolocation") + +with Files("windows/*LocationProvider*"): + BUG_COMPONENT = ("Core", "DOM: Geolocation") + +with Files("mac/*LocationProvider*"): + BUG_COMPONENT = ("Core", "DOM: Geolocation") + +with Files("mac/*OSPermissionRequest*"): + BUG_COMPONENT = ("Firefox", "Site Permissions") + +with Files("linux/*LocationProvider*"): + BUG_COMPONENT = ("Core", "DOM: Geolocation") + +with Files("android/*LocationProvider*"): + BUG_COMPONENT = ("Core", "DOM: Geolocation") + +with Files("tests/chrome.ini"): + BUG_COMPONENT = ("Toolkit", "OS.File") + +with Files("tests/*constants*"): + BUG_COMPONENT = ("Toolkit", "OS.File") + +with Files("tests/mochitest.ini"): + BUG_COMPONENT = ("Core", "DOM: Device Interfaces") + +with Files("tests/*1197901*"): + BUG_COMPONENT = ("Core", "DOM: Device Interfaces") + +toolkit = CONFIG["MOZ_WIDGET_TOOLKIT"] + +if toolkit == "windows": + DIRS += ["windows"] +elif toolkit == "cocoa": + DIRS += ["mac"] +elif toolkit == "android": + DIRS += ["android"] +elif toolkit == "gtk": + DIRS += ["linux"] + +if toolkit != "cocoa": + EXPORTS += [ + "nsOSPermissionRequest.h", + ] + +XPIDL_SOURCES += [ + "nsIOSFileConstantsService.idl", + "nsIOSPermissionRequest.idl", +] + +XPIDL_MODULE = "dom_system" + +EXPORTS += [ + "nsDeviceSensors.h", + "nsOSPermissionRequestBase.h", +] + +EXPORTS.mozilla += [ + "OSFileConstants.h", +] + +EXPORTS.mozilla.dom += [ + "IOUtils.h", + "PathUtils.h", +] + +UNIFIED_SOURCES += [ + "IOUtils.cpp", + "nsDeviceSensors.cpp", + "nsOSPermissionRequestBase.cpp", + "OSFileConstants.cpp", + "PathUtils.cpp", +] + +EXTRA_JS_MODULES += [ + "NetworkGeolocationProvider.jsm", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" +# We fire the nsDOMDeviceAcceleration +LOCAL_INCLUDES += [ + "/dom/base", + "/dom/bindings", + "/js/xpconnect/loader", + "/xpcom/base", +] + +MOCHITEST_CHROME_MANIFESTS += ["tests/chrome.ini", "tests/ioutils/chrome.ini"] +MOCHITEST_MANIFESTS += ["tests/mochitest.ini"] diff --git a/dom/system/nsDeviceSensors.cpp b/dom/system/nsDeviceSensors.cpp new file mode 100644 index 0000000000..394c72b7ec --- /dev/null +++ b/dom/system/nsDeviceSensors.cpp @@ -0,0 +1,573 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "mozilla/Hal.h" +#include "mozilla/HalSensor.h" + +#include "nsContentUtils.h" +#include "nsDeviceSensors.h" + +#include "nsPIDOMWindow.h" +#include "nsIScriptObjectPrincipal.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_device.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/DeviceLightEvent.h" +#include "mozilla/dom/DeviceOrientationEvent.h" +#include "mozilla/dom/DeviceProximityEvent.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/UserProximityEvent.h" +#include "mozilla/ErrorResult.h" + +#include <cmath> + +using namespace mozilla; +using namespace mozilla::dom; +using namespace hal; + +class nsIDOMWindow; + +#undef near + +#define DEFAULT_SENSOR_POLL 100 + +static const nsTArray<nsIDOMWindow*>::index_type NoIndex = + nsTArray<nsIDOMWindow*>::NoIndex; + +class nsDeviceSensorData final : public nsIDeviceSensorData { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIDEVICESENSORDATA + + nsDeviceSensorData(unsigned long type, double x, double y, double z); + + private: + ~nsDeviceSensorData(); + + protected: + unsigned long mType; + double mX, mY, mZ; +}; + +nsDeviceSensorData::nsDeviceSensorData(unsigned long type, double x, double y, + double z) + : mType(type), mX(x), mY(y), mZ(z) {} + +NS_INTERFACE_MAP_BEGIN(nsDeviceSensorData) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDeviceSensorData) +NS_INTERFACE_MAP_END + +NS_IMPL_ADDREF(nsDeviceSensorData) +NS_IMPL_RELEASE(nsDeviceSensorData) + +nsDeviceSensorData::~nsDeviceSensorData() = default; + +NS_IMETHODIMP nsDeviceSensorData::GetType(uint32_t* aType) { + NS_ENSURE_ARG_POINTER(aType); + *aType = mType; + return NS_OK; +} + +NS_IMETHODIMP nsDeviceSensorData::GetX(double* aX) { + NS_ENSURE_ARG_POINTER(aX); + *aX = mX; + return NS_OK; +} + +NS_IMETHODIMP nsDeviceSensorData::GetY(double* aY) { + NS_ENSURE_ARG_POINTER(aY); + *aY = mY; + return NS_OK; +} + +NS_IMETHODIMP nsDeviceSensorData::GetZ(double* aZ) { + NS_ENSURE_ARG_POINTER(aZ); + *aZ = mZ; + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsDeviceSensors, nsIDeviceSensors) + +nsDeviceSensors::nsDeviceSensors() { + mIsUserProximityNear = false; + mLastDOMMotionEventTime = TimeStamp::Now(); + + for (int i = 0; i < NUM_SENSOR_TYPE; i++) { + nsTArray<nsIDOMWindow*>* windows = new nsTArray<nsIDOMWindow*>(); + mWindowListeners.AppendElement(windows); + } + + mLastDOMMotionEventTime = TimeStamp::Now(); +} + +nsDeviceSensors::~nsDeviceSensors() { + for (int i = 0; i < NUM_SENSOR_TYPE; i++) { + if (IsSensorEnabled(i)) UnregisterSensorObserver((SensorType)i, this); + } + + for (int i = 0; i < NUM_SENSOR_TYPE; i++) { + delete mWindowListeners[i]; + } +} + +NS_IMETHODIMP nsDeviceSensors::HasWindowListener(uint32_t aType, + nsIDOMWindow* aWindow, + bool* aRetVal) { + if (!IsSensorAllowedByPref(aType, aWindow)) + *aRetVal = false; + else + *aRetVal = mWindowListeners[aType]->IndexOf(aWindow) != NoIndex; + + return NS_OK; +} + +class DeviceSensorTestEvent : public Runnable { + public: + DeviceSensorTestEvent(nsDeviceSensors* aTarget, uint32_t aType) + : mozilla::Runnable("DeviceSensorTestEvent"), + mTarget(aTarget), + mType(aType) {} + + NS_IMETHOD Run() override { + SensorData sensorData; + sensorData.sensor() = static_cast<SensorType>(mType); + sensorData.timestamp() = PR_Now(); + sensorData.values().AppendElement(0.5f); + sensorData.values().AppendElement(0.5f); + sensorData.values().AppendElement(0.5f); + sensorData.values().AppendElement(0.5f); + mTarget->Notify(sensorData); + return NS_OK; + } + + private: + RefPtr<nsDeviceSensors> mTarget; + uint32_t mType; +}; + +NS_IMETHODIMP nsDeviceSensors::AddWindowListener(uint32_t aType, + nsIDOMWindow* aWindow) { + if (!IsSensorAllowedByPref(aType, aWindow)) return NS_OK; + + if (mWindowListeners[aType]->IndexOf(aWindow) != NoIndex) return NS_OK; + + if (!IsSensorEnabled(aType)) { + RegisterSensorObserver((SensorType)aType, this); + } + + mWindowListeners[aType]->AppendElement(aWindow); + + if (StaticPrefs::device_sensors_test_events()) { + nsCOMPtr<nsIRunnable> event = new DeviceSensorTestEvent(this, aType); + NS_DispatchToCurrentThread(event); + } + + return NS_OK; +} + +NS_IMETHODIMP nsDeviceSensors::RemoveWindowListener(uint32_t aType, + nsIDOMWindow* aWindow) { + if (mWindowListeners[aType]->IndexOf(aWindow) == NoIndex) return NS_OK; + + mWindowListeners[aType]->RemoveElement(aWindow); + + if (mWindowListeners[aType]->Length() == 0) + UnregisterSensorObserver((SensorType)aType, this); + + return NS_OK; +} + +NS_IMETHODIMP nsDeviceSensors::RemoveWindowAsListener(nsIDOMWindow* aWindow) { + for (int i = 0; i < NUM_SENSOR_TYPE; i++) { + RemoveWindowListener((SensorType)i, aWindow); + } + return NS_OK; +} + +static bool WindowCannotReceiveSensorEvent(nsPIDOMWindowInner* aWindow) { + // Check to see if this window is in the background. + if (!aWindow || !aWindow->IsCurrentInnerWindow()) { + return true; + } + + nsPIDOMWindowOuter* windowOuter = aWindow->GetOuterWindow(); + BrowsingContext* topBC = aWindow->GetBrowsingContext()->Top(); + if (windowOuter->IsBackground() || !topBC->GetIsActiveBrowserWindow()) { + return true; + } + + // Check to see if this window is a cross-origin iframe: + if (!topBC->IsInProcess()) { + return true; + } + + nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aWindow); + nsCOMPtr<nsIScriptObjectPrincipal> topSop = + do_QueryInterface(topBC->GetDOMWindow()); + if (!sop || !topSop) { + return true; + } + + nsIPrincipal* principal = sop->GetPrincipal(); + nsIPrincipal* topPrincipal = topSop->GetPrincipal(); + if (!principal || !topPrincipal) { + return true; + } + + return !principal->Subsumes(topPrincipal); +} + +// Holds the device orientation in Euler angle degrees (azimuth, pitch, roll). +struct Orientation { + enum OrientationReference { kRelative = 0, kAbsolute }; + + static Orientation RadToDeg(const Orientation& aOrient) { + const static double kRadToDeg = 180.0 / M_PI; + return {aOrient.alpha * kRadToDeg, aOrient.beta * kRadToDeg, + aOrient.gamma * kRadToDeg}; + } + + double alpha; + double beta; + double gamma; +}; + +static Orientation RotationVectorToOrientation(double aX, double aY, double aZ, + double aW) { + double mat[9]; + + mat[0] = 1 - 2 * aY * aY - 2 * aZ * aZ; + mat[1] = 2 * aX * aY - 2 * aZ * aW; + mat[2] = 2 * aX * aZ + 2 * aY * aW; + + mat[3] = 2 * aX * aY + 2 * aZ * aW; + mat[4] = 1 - 2 * aX * aX - 2 * aZ * aZ; + mat[5] = 2 * aY * aZ - 2 * aX * aW; + + mat[6] = 2 * aX * aZ - 2 * aY * aW; + mat[7] = 2 * aY * aZ + 2 * aX * aW; + mat[8] = 1 - 2 * aX * aX - 2 * aY * aY; + + Orientation orient; + + if (mat[8] > 0) { + orient.alpha = atan2(-mat[1], mat[4]); + orient.beta = asin(mat[7]); + orient.gamma = atan2(-mat[6], mat[8]); + } else if (mat[8] < 0) { + orient.alpha = atan2(mat[1], -mat[4]); + orient.beta = -asin(mat[7]); + orient.beta += (orient.beta >= 0) ? -M_PI : M_PI; + orient.gamma = atan2(mat[6], -mat[8]); + } else { + if (mat[6] > 0) { + orient.alpha = atan2(-mat[1], mat[4]); + orient.beta = asin(mat[7]); + orient.gamma = -M_PI_2; + } else if (mat[6] < 0) { + orient.alpha = atan2(mat[1], -mat[4]); + orient.beta = -asin(mat[7]); + orient.beta += (orient.beta >= 0) ? -M_PI : M_PI; + orient.gamma = -M_PI_2; + } else { + orient.alpha = atan2(mat[3], mat[0]); + orient.beta = (mat[7] > 0) ? M_PI_2 : -M_PI_2; + orient.gamma = 0; + } + } + + if (orient.alpha < 0) { + orient.alpha += 2 * M_PI; + } + + return Orientation::RadToDeg(orient); +} + +void nsDeviceSensors::Notify(const mozilla::hal::SensorData& aSensorData) { + uint32_t type = aSensorData.sensor(); + + const nsTArray<float>& values = aSensorData.values(); + size_t len = values.Length(); + double x = len > 0 ? values[0] : 0.0; + double y = len > 1 ? values[1] : 0.0; + double z = len > 2 ? values[2] : 0.0; + double w = len > 3 ? values[3] : 0.0; + PRTime timestamp = aSensorData.timestamp(); + + nsCOMArray<nsIDOMWindow> windowListeners; + for (uint32_t i = 0; i < mWindowListeners[type]->Length(); i++) { + windowListeners.AppendObject(mWindowListeners[type]->SafeElementAt(i)); + } + + for (uint32_t i = windowListeners.Count(); i > 0;) { + --i; + + nsCOMPtr<nsPIDOMWindowInner> pwindow = + do_QueryInterface(windowListeners[i]); + if (WindowCannotReceiveSensorEvent(pwindow)) { + continue; + } + + if (nsCOMPtr<Document> doc = pwindow->GetDoc()) { + nsCOMPtr<mozilla::dom::EventTarget> target = + do_QueryInterface(windowListeners[i]); + if (type == nsIDeviceSensorData::TYPE_ACCELERATION || + type == nsIDeviceSensorData::TYPE_LINEAR_ACCELERATION || + type == nsIDeviceSensorData::TYPE_GYROSCOPE) { + FireDOMMotionEvent(doc, target, type, timestamp, x, y, z); + } else if (type == nsIDeviceSensorData::TYPE_ORIENTATION) { + FireDOMOrientationEvent(target, x, y, z, Orientation::kAbsolute); + } else if (type == nsIDeviceSensorData::TYPE_ROTATION_VECTOR) { + const Orientation orient = RotationVectorToOrientation(x, y, z, w); + FireDOMOrientationEvent(target, orient.alpha, orient.beta, orient.gamma, + Orientation::kAbsolute); + } else if (type == nsIDeviceSensorData::TYPE_GAME_ROTATION_VECTOR) { + const Orientation orient = RotationVectorToOrientation(x, y, z, w); + FireDOMOrientationEvent(target, orient.alpha, orient.beta, orient.gamma, + Orientation::kRelative); + } else if (type == nsIDeviceSensorData::TYPE_PROXIMITY) { + FireDOMProximityEvent(target, x, y, z); + } else if (type == nsIDeviceSensorData::TYPE_LIGHT) { + FireDOMLightEvent(target, x); + } + } + } +} + +void nsDeviceSensors::FireDOMLightEvent(mozilla::dom::EventTarget* aTarget, + double aValue) { + DeviceLightEventInit init; + init.mBubbles = true; + init.mCancelable = false; + init.mValue = round(aValue); + RefPtr<DeviceLightEvent> event = + DeviceLightEvent::Constructor(aTarget, u"devicelight"_ns, init); + + event->SetTrusted(true); + + aTarget->DispatchEvent(*event); +} + +void nsDeviceSensors::FireDOMProximityEvent(mozilla::dom::EventTarget* aTarget, + double aValue, double aMin, + double aMax) { + DeviceProximityEventInit init; + init.mBubbles = true; + init.mCancelable = false; + init.mValue = aValue; + init.mMin = aMin; + init.mMax = aMax; + RefPtr<DeviceProximityEvent> event = + DeviceProximityEvent::Constructor(aTarget, u"deviceproximity"_ns, init); + event->SetTrusted(true); + + aTarget->DispatchEvent(*event); + + // Some proximity sensors only support a binary near or + // far measurement. In this case, the sensor should report + // its maximum range value in the far state and a lesser + // value in the near state. + + bool near = (aValue < aMax); + if (mIsUserProximityNear != near) { + mIsUserProximityNear = near; + FireDOMUserProximityEvent(aTarget, mIsUserProximityNear); + } +} + +void nsDeviceSensors::FireDOMUserProximityEvent( + mozilla::dom::EventTarget* aTarget, bool aNear) { + UserProximityEventInit init; + init.mBubbles = true; + init.mCancelable = false; + init.mNear = aNear; + RefPtr<UserProximityEvent> event = + UserProximityEvent::Constructor(aTarget, u"userproximity"_ns, init); + + event->SetTrusted(true); + + aTarget->DispatchEvent(*event); +} + +void nsDeviceSensors::FireDOMOrientationEvent(EventTarget* aTarget, + double aAlpha, double aBeta, + double aGamma, bool aIsAbsolute) { + DeviceOrientationEventInit init; + init.mBubbles = true; + init.mCancelable = false; + init.mAlpha.SetValue(aAlpha); + init.mBeta.SetValue(aBeta); + init.mGamma.SetValue(aGamma); + init.mAbsolute = aIsAbsolute; + + auto Dispatch = [&](EventTarget* aEventTarget, const nsAString& aType) { + RefPtr<DeviceOrientationEvent> event = + DeviceOrientationEvent::Constructor(aEventTarget, aType, init); + event->SetTrusted(true); + aEventTarget->DispatchEvent(*event); + }; + + Dispatch(aTarget, aIsAbsolute ? u"absolutedeviceorientation"_ns + : u"deviceorientation"_ns); + + // This is used to determine whether relative events have been dispatched + // during the current session, in which case we don't dispatch the additional + // compatibility events. + static bool sIsDispatchingRelativeEvents = false; + sIsDispatchingRelativeEvents = sIsDispatchingRelativeEvents || !aIsAbsolute; + + // Android devices with SENSOR_GAME_ROTATION_VECTOR support dispatch + // relative events for "deviceorientation" by default, while other platforms + // and devices without such support dispatch absolute events by default. + if (aIsAbsolute && !sIsDispatchingRelativeEvents) { + // For absolute events on devices without support for relative events, + // we need to additionally dispatch type "deviceorientation" to keep + // backwards-compatibility. + Dispatch(aTarget, u"deviceorientation"_ns); + } +} + +void nsDeviceSensors::FireDOMMotionEvent(Document* doc, EventTarget* target, + uint32_t type, PRTime timestamp, + double x, double y, double z) { + // Attempt to coalesce events + TimeDuration sensorPollDuration = + TimeDuration::FromMilliseconds(DEFAULT_SENSOR_POLL); + bool fireEvent = + (TimeStamp::Now() > mLastDOMMotionEventTime + sensorPollDuration) || + StaticPrefs::device_sensors_test_events(); + + switch (type) { + case nsIDeviceSensorData::TYPE_LINEAR_ACCELERATION: + if (!mLastAcceleration) { + mLastAcceleration.emplace(); + } + mLastAcceleration->mX.SetValue(x); + mLastAcceleration->mY.SetValue(y); + mLastAcceleration->mZ.SetValue(z); + break; + case nsIDeviceSensorData::TYPE_ACCELERATION: + if (!mLastAccelerationIncludingGravity) { + mLastAccelerationIncludingGravity.emplace(); + } + mLastAccelerationIncludingGravity->mX.SetValue(x); + mLastAccelerationIncludingGravity->mY.SetValue(y); + mLastAccelerationIncludingGravity->mZ.SetValue(z); + break; + case nsIDeviceSensorData::TYPE_GYROSCOPE: + if (!mLastRotationRate) { + mLastRotationRate.emplace(); + } + mLastRotationRate->mAlpha.SetValue(x); + mLastRotationRate->mBeta.SetValue(y); + mLastRotationRate->mGamma.SetValue(z); + break; + } + + if (fireEvent) { + if (!mLastAcceleration) { + mLastAcceleration.emplace(); + } + if (!mLastAccelerationIncludingGravity) { + mLastAccelerationIncludingGravity.emplace(); + } + if (!mLastRotationRate) { + mLastRotationRate.emplace(); + } + } else if (!mLastAcceleration || !mLastAccelerationIncludingGravity || + !mLastRotationRate) { + return; + } + + IgnoredErrorResult ignored; + RefPtr<Event> event = + doc->CreateEvent(u"DeviceMotionEvent"_ns, CallerType::System, ignored); + if (!event) { + return; + } + + DeviceMotionEvent* me = static_cast<DeviceMotionEvent*>(event.get()); + + me->InitDeviceMotionEvent( + u"devicemotion"_ns, true, false, *mLastAcceleration, + *mLastAccelerationIncludingGravity, *mLastRotationRate, + Nullable<double>(DEFAULT_SENSOR_POLL), Nullable<uint64_t>(timestamp)); + + event->SetTrusted(true); + + target->DispatchEvent(*event); + + mLastRotationRate.reset(); + mLastAccelerationIncludingGravity.reset(); + mLastAcceleration.reset(); + mLastDOMMotionEventTime = TimeStamp::Now(); +} + +bool nsDeviceSensors::IsSensorAllowedByPref(uint32_t aType, + nsIDOMWindow* aWindow) { + // checks "device.sensors.enabled" master pref + if (!StaticPrefs::device_sensors_enabled()) { + return false; + } + + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aWindow); + nsCOMPtr<Document> doc; + if (window) { + doc = window->GetExtantDoc(); + } + + switch (aType) { + case nsIDeviceSensorData::TYPE_LINEAR_ACCELERATION: + case nsIDeviceSensorData::TYPE_ACCELERATION: + case nsIDeviceSensorData::TYPE_GYROSCOPE: + // checks "device.sensors.motion.enabled" pref + if (!StaticPrefs::device_sensors_motion_enabled()) { + return false; + } else if (doc) { + doc->WarnOnceAbout(DeprecatedOperations::eMotionEvent); + } + break; + case nsIDeviceSensorData::TYPE_GAME_ROTATION_VECTOR: + case nsIDeviceSensorData::TYPE_ORIENTATION: + case nsIDeviceSensorData::TYPE_ROTATION_VECTOR: + // checks "device.sensors.orientation.enabled" pref + if (!StaticPrefs::device_sensors_orientation_enabled()) { + return false; + } else if (doc) { + doc->WarnOnceAbout(DeprecatedOperations::eOrientationEvent); + } + break; + case nsIDeviceSensorData::TYPE_PROXIMITY: + // checks "device.sensors.proximity.enabled" pref + if (!StaticPrefs::device_sensors_proximity_enabled()) { + return false; + } else if (doc) { + doc->WarnOnceAbout(DeprecatedOperations::eProximityEvent, true); + } + break; + case nsIDeviceSensorData::TYPE_LIGHT: + // checks "device.sensors.ambientLight.enabled" pref + if (!StaticPrefs::device_sensors_ambientLight_enabled()) { + return false; + } else if (doc) { + doc->WarnOnceAbout(DeprecatedOperations::eAmbientLightEvent, true); + } + break; + default: + MOZ_ASSERT_UNREACHABLE("Device sensor type not recognised"); + return false; + } + + if (!window) { + return true; + } + + nsCOMPtr<nsIScriptObjectPrincipal> soPrincipal = do_QueryInterface(window); + return !nsContentUtils::ShouldResistFingerprinting( + soPrincipal->GetPrincipal()); +} diff --git a/dom/system/nsDeviceSensors.h b/dom/system/nsDeviceSensors.h new file mode 100644 index 0000000000..925540ef39 --- /dev/null +++ b/dom/system/nsDeviceSensors.h @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 nsDeviceSensors_h +#define nsDeviceSensors_h + +#include "nsIDeviceSensors.h" +#include "nsCOMArray.h" +#include "nsTArray.h" +#include "nsCOMPtr.h" +#include "mozilla/dom/DeviceMotionEvent.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/HalSensor.h" +#include "nsDataHashtable.h" + +class nsIDOMWindow; + +namespace mozilla { +namespace dom { +class Document; +class EventTarget; +} // namespace dom +} // namespace mozilla + +class nsDeviceSensors : public nsIDeviceSensors, + public mozilla::hal::ISensorObserver { + typedef mozilla::dom::DeviceAccelerationInit DeviceAccelerationInit; + typedef mozilla::dom::DeviceRotationRateInit DeviceRotationRateInit; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIDEVICESENSORS + + nsDeviceSensors(); + + void Notify(const mozilla::hal::SensorData& aSensorData) override; + + private: + virtual ~nsDeviceSensors(); + + // sensor -> window listener + nsTArray<nsTArray<nsIDOMWindow*>*> mWindowListeners; + + void FireDOMLightEvent(mozilla::dom::EventTarget* aTarget, double value); + + void FireDOMProximityEvent(mozilla::dom::EventTarget* aTarget, double aValue, + double aMin, double aMax); + + void FireDOMUserProximityEvent(mozilla::dom::EventTarget* aTarget, + bool aNear); + + void FireDOMOrientationEvent(mozilla::dom::EventTarget* target, double aAlpha, + double aBeta, double aGamma, bool aIsAbsolute); + + void FireDOMMotionEvent(mozilla::dom::Document* domDoc, + mozilla::dom::EventTarget* target, uint32_t type, + PRTime timestamp, double x, double y, double z); + + inline bool IsSensorEnabled(uint32_t aType) { + return mWindowListeners[aType]->Length() > 0; + } + + bool IsSensorAllowedByPref(uint32_t aType, nsIDOMWindow* aWindow); + + mozilla::TimeStamp mLastDOMMotionEventTime; + bool mIsUserProximityNear; + mozilla::Maybe<DeviceAccelerationInit> mLastAcceleration; + mozilla::Maybe<DeviceAccelerationInit> mLastAccelerationIncludingGravity; + mozilla::Maybe<DeviceRotationRateInit> mLastRotationRate; +}; + +#endif diff --git a/dom/system/nsIOSFileConstantsService.idl b/dom/system/nsIOSFileConstantsService.idl new file mode 100644 index 0000000000..2cdde3de97 --- /dev/null +++ b/dom/system/nsIOSFileConstantsService.idl @@ -0,0 +1,28 @@ +/* -*- 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" + +[scriptable, uuid(d6dd239f-34d6-4b34-baa1-f69ab4a20bc4)] +interface nsIOSFileConstantsService: nsISupports +{ + /** + * Inject module OS.Constants in the environment. + * + * This method must be called only from the main thread. + * Method is idempotent. + */ + [implicit_jscontext] + void init(); +}; + +%{ C++ + +// {4BBE1B96-8956-457F-A03F-9C27435F2AFA} +#define OSFILECONSTANTSSERVICE_CID {0x4BBE1B96,0x8956,0x457F,{0xA0,0x3F,0x9C,0x27,0x43,0x5F,0x2A,0xFA}} +#define OSFILECONSTANTSSERVICE_CONTRACTID "@mozilla.org/net/osfileconstantsservice;1" + +%} diff --git a/dom/system/nsIOSPermissionRequest.idl b/dom/system/nsIOSPermissionRequest.idl new file mode 100644 index 0000000000..e0d7b531c7 --- /dev/null +++ b/dom/system/nsIOSPermissionRequest.idl @@ -0,0 +1,69 @@ +/* -*- 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" + +[scriptable, uuid(95790842-75a0-430d-98bf-f5ce3788ea6d)] +interface nsIOSPermissionRequest: nsISupports +{ + /* + * The permission state is not known. As an example, on macOS + * this is used to indicate the user has not been prompted to + * authorize or deny access and there is no policy in place to + * deny access. + */ + const uint16_t PERMISSION_STATE_NOTDETERMINED = 0; + + /* A policy prevents the application from accessing the resource */ + const uint16_t PERMISSION_STATE_RESTRICTED = 1; + + /* Access to the resource is denied */ + const uint16_t PERMISSION_STATE_DENIED = 2; + + /* Access to the resource is allowed */ + const uint16_t PERMISSION_STATE_AUTHORIZED = 3; + + /* Get the permission state for both audio and video capture */ + void getMediaCapturePermissionState(out uint16_t aVideo, + out uint16_t aAudio); + + /* Get the permission state for audio capture */ + void getAudioCapturePermissionState(out uint16_t aAudio); + + /* Get the permission state for video capture */ + void getVideoCapturePermissionState(out uint16_t aVideo); + + /* Get the permission state for screen capture */ + void getScreenCapturePermissionState(out uint16_t aScreen); + + /* + * Request permission to access video capture devices. Returns a + * promise that resolves with |true| after the browser has been + * granted permission to capture video. If capture access is denied, + * the promise is resolved with |false|. The promise is rejected if + * an error occurs. + */ + [implicit_jscontext, must_use] + Promise requestVideoCapturePermission(); + + /* + * Request permission to access audio capture devices. Returns a + * promise with the same semantics as |requestVideoCapturePermission|. + */ + [implicit_jscontext, must_use] + Promise requestAudioCapturePermission(); + + /* + * Request permission to capture the screen using an unreliable method. + * Attemps to trigger a screen capture permission dialog. Whether or not + * the dialog is displayed and whether or not the user grants permission + * to record the screen is not available to the caller. This method has + * limited utility because it does not block to wait for a dialog + * prompt or the user's reponse if a dialog is displayed. And the dialog + * is not guaranteed to be displayed per OS restrictions. + */ + void maybeRequestScreenCapturePermission(); +}; diff --git a/dom/system/nsOSPermissionRequest.h b/dom/system/nsOSPermissionRequest.h new file mode 100644 index 0000000000..660434c863 --- /dev/null +++ b/dom/system/nsOSPermissionRequest.h @@ -0,0 +1,18 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 nsOSPermissionRequest_h__ +#define nsOSPermissionRequest_h__ + +#include "nsOSPermissionRequestBase.h" + +/* + * The default implementation of nsOSPermissionRequestBase used on platforms + * that don't have a platform-specific version. + */ +class nsOSPermissionRequest : public nsOSPermissionRequestBase {}; + +#endif /* nsOSPermissionRequest_h__ */ diff --git a/dom/system/nsOSPermissionRequestBase.cpp b/dom/system/nsOSPermissionRequestBase.cpp new file mode 100644 index 0000000000..32b32c38cd --- /dev/null +++ b/dom/system/nsOSPermissionRequestBase.cpp @@ -0,0 +1,96 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "nsOSPermissionRequestBase.h" + +#include "mozilla/dom/Promise.h" + +using namespace mozilla; + +using mozilla::dom::Promise; + +NS_IMPL_ISUPPORTS(nsOSPermissionRequestBase, nsIOSPermissionRequest, + nsISupportsWeakReference) + +NS_IMETHODIMP +nsOSPermissionRequestBase::GetMediaCapturePermissionState( + uint16_t* aCamera, uint16_t* aMicrophone) { + nsresult rv = GetVideoCapturePermissionState(aCamera); + if (NS_FAILED(rv)) { + return rv; + } + return GetAudioCapturePermissionState(aMicrophone); +} + +NS_IMETHODIMP +nsOSPermissionRequestBase::GetAudioCapturePermissionState(uint16_t* aAudio) { + MOZ_ASSERT(aAudio); + *aAudio = PERMISSION_STATE_AUTHORIZED; + return NS_OK; +} + +NS_IMETHODIMP +nsOSPermissionRequestBase::GetVideoCapturePermissionState(uint16_t* aVideo) { + MOZ_ASSERT(aVideo); + *aVideo = PERMISSION_STATE_AUTHORIZED; + return NS_OK; +} + +NS_IMETHODIMP +nsOSPermissionRequestBase::GetScreenCapturePermissionState(uint16_t* aScreen) { + MOZ_ASSERT(aScreen); + *aScreen = PERMISSION_STATE_AUTHORIZED; + return NS_OK; +} + +nsresult nsOSPermissionRequestBase::GetPromise(JSContext* aCx, + RefPtr<Promise>& aPromiseOut) { + nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx); + if (NS_WARN_IF(!globalObject)) { + return NS_ERROR_UNEXPECTED; + } + + ErrorResult result; + aPromiseOut = Promise::Create(globalObject, result); + if (NS_WARN_IF(result.Failed())) { + return result.StealNSResult(); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsOSPermissionRequestBase::RequestVideoCapturePermission( + JSContext* aCx, Promise** aPromiseOut) { + RefPtr<Promise> promiseHandle; + nsresult rv = GetPromise(aCx, promiseHandle); + if (NS_FAILED(rv)) { + return rv; + } + + promiseHandle->MaybeResolve(true /* access authorized */); + promiseHandle.forget(aPromiseOut); + return NS_OK; +} + +NS_IMETHODIMP +nsOSPermissionRequestBase::RequestAudioCapturePermission( + JSContext* aCx, Promise** aPromiseOut) { + RefPtr<Promise> promiseHandle; + nsresult rv = GetPromise(aCx, promiseHandle); + if (NS_FAILED(rv)) { + return rv; + } + + promiseHandle->MaybeResolve(true /* access authorized */); + promiseHandle.forget(aPromiseOut); + return NS_OK; +} + +NS_IMETHODIMP +nsOSPermissionRequestBase::MaybeRequestScreenCapturePermission() { + return NS_OK; +} diff --git a/dom/system/nsOSPermissionRequestBase.h b/dom/system/nsOSPermissionRequestBase.h new file mode 100644 index 0000000000..6fecce42c1 --- /dev/null +++ b/dom/system/nsOSPermissionRequestBase.h @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 nsOSPermissionRequestBase_h__ +#define nsOSPermissionRequestBase_h__ + +#include "nsIOSPermissionRequest.h" +#include "nsWeakReference.h" + +namespace mozilla { +namespace dom { +class Promise; +} // namespace dom +} // namespace mozilla + +using mozilla::dom::Promise; + +/* + * The base implementation of nsIOSPermissionRequest to be subclassed on + * platforms that require permission requests for access to resources such + * as media captures devices. This implementation always returns results + * indicating access is permitted. + */ +class nsOSPermissionRequestBase : public nsIOSPermissionRequest, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOSPERMISSIONREQUEST + + nsOSPermissionRequestBase() = default; + + protected: + nsresult GetPromise(JSContext* aCx, RefPtr<Promise>& aPromiseOut); + virtual ~nsOSPermissionRequestBase() = default; +}; + +#endif diff --git a/dom/system/tests/.eslintrc.js b/dom/system/tests/.eslintrc.js new file mode 100644 index 0000000000..19d9df957a --- /dev/null +++ b/dom/system/tests/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/chrome-test", "plugin:mozilla/mochitest-test"], +}; diff --git a/dom/system/tests/chrome.ini b/dom/system/tests/chrome.ini new file mode 100644 index 0000000000..296cf0b133 --- /dev/null +++ b/dom/system/tests/chrome.ini @@ -0,0 +1,6 @@ +[DEFAULT] +support-files = + worker_constants.js + +[test_constants.xhtml] +[test_pathutils.html] diff --git a/dom/system/tests/file_bug1197901.html b/dom/system/tests/file_bug1197901.html new file mode 100644 index 0000000000..e4ca9fd380 --- /dev/null +++ b/dom/system/tests/file_bug1197901.html @@ -0,0 +1,16 @@ +<pre>Sensor events testing</pre> +<script> + +window.onmessage = function(event) { + if (event.data.command == "addEventListener") { + window.addEventListener( + "devicemotion", function() { + event.source.postMessage({ result: event.data.expected, + message: event.data.message }, + "*"); + } + ); + } +}; + +</script> diff --git a/dom/system/tests/ioutils/chrome.ini b/dom/system/tests/ioutils/chrome.ini new file mode 100644 index 0000000000..b3f109a554 --- /dev/null +++ b/dom/system/tests/ioutils/chrome.ini @@ -0,0 +1,16 @@ +[DEFAULT] +support-files = + file_ioutils_test_fixtures.js + file_ioutils_worker.js + +[test_ioutils.html] +[test_ioutils_copy_move.html] +[test_ioutils_dir_iteration.html] +[test_ioutils_mkdir.html] +[test_ioutils_read_write.html] +[test_ioutils_read_write_json.html] +[test_ioutils_read_write_utf8.html] +[test_ioutils_remove.html] +[test_ioutils_stat_touch.html] +[test_ioutils_worker.xhtml] +[test_ioutils_set_permissions.html] diff --git a/dom/system/tests/ioutils/file_ioutils_test_fixtures.js b/dom/system/tests/ioutils/file_ioutils_test_fixtures.js new file mode 100644 index 0000000000..5d2e5011c9 --- /dev/null +++ b/dom/system/tests/ioutils/file_ioutils_test_fixtures.js @@ -0,0 +1,78 @@ +// Utility functions. + +Uint8Array.prototype.equals = function equals(other) { + if (this.byteLength !== other.byteLength) { + return false; + } + return this.every((val, i) => val === other[i]); +}; + +async function createFile(location, contents = "") { + if (typeof contents === "string") { + contents = new TextEncoder().encode(contents); + } + await IOUtils.write(location, contents); + const exists = await fileExists(location); + ok(exists, `Created temporary file at: ${location}`); +} + +async function createDir(location) { + await IOUtils.makeDirectory(location, { + ignoreExisting: true, + createAncestors: true, + }); + const exists = await dirExists(location); + ok(exists, `Created temporary directory at: ${location}`); +} + +async function fileHasBinaryContents(location, expectedContents) { + if (!(expectedContents instanceof Uint8Array)) { + throw new TypeError("expectedContents must be a byte array"); + } + info(`Opening ${location} for reading`); + const bytes = await IOUtils.read(location); + return bytes.equals(expectedContents); +} + +async function fileHasTextContents(location, expectedContents) { + if (typeof expectedContents !== "string") { + throw new TypeError("expectedContents must be a string"); + } + info(`Opening ${location} for reading`); + const bytes = await IOUtils.read(location); + const contents = new TextDecoder().decode(bytes); + return contents === expectedContents; +} + +async function fileExists(file) { + try { + let { type } = await IOUtils.stat(file); + return type === "regular"; + } catch (ex) { + return false; + } +} + +async function dirExists(dir) { + try { + let { type } = await IOUtils.stat(dir); + return type === "directory"; + } catch (ex) { + return false; + } +} + +async function cleanup(...files) { + for (const file of files) { + await IOUtils.remove(file, { + ignoreAbsent: true, + recursive: true, + }); + const exists = await IOUtils.exists(file); + ok(!exists, `Removed temporary file: ${file}`); + } +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/dom/system/tests/ioutils/file_ioutils_worker.js b/dom/system/tests/ioutils/file_ioutils_worker.js new file mode 100644 index 0000000000..68c0e81289 --- /dev/null +++ b/dom/system/tests/ioutils/file_ioutils_worker.js @@ -0,0 +1,102 @@ +// Any copyright is dedicated to the Public Domain. +// - http://creativecommons.org/publicdomain/zero/1.0/ + +/* eslint-env mozilla/chrome-worker, node */ +/* global finish, log */ + +"use strict"; + +importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js"); +importScripts("resource://gre/modules/ObjectUtils.jsm"); + +// TODO: Remove this import for OS.File. It is currently being used as a +// stop gap for missing IOUtils functionality. +importScripts("resource://gre/modules/osfile.jsm"); +importScripts("file_ioutils_test_fixtures.js"); + +self.onmessage = async function(msg) { + const tmpDir = OS.Constants.Path.tmpDir; + + // IOUtils functionality is the same when called from the main thread, or a + // web worker. These tests are a modified subset of the main thread tests, and + // serve as a confidence check that the implementation is thread-safe. + await test_api_is_available_on_worker(); + await test_full_read_and_write(); + await test_move_file(); + await test_copy_file(); + await test_make_directory(); + + finish(); + info("test_ioutils_worker.xhtml: Test finished"); + + async function test_api_is_available_on_worker() { + ok(self.IOUtils, "IOUtils is present in web workers"); + } + + async function test_full_read_and_write() { + // Write a file. + const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_numbers.tmp"); + const bytes = Uint8Array.of(...new Array(50).keys()); + const bytesWritten = await IOUtils.write(tmpFileName, bytes); + is(bytesWritten, 50, "IOUtils::write can write entire byte array to file"); + + // Read it back. + let fileContents = await IOUtils.read(tmpFileName); + ok( + ObjectUtils.deepEqual(bytes, fileContents) && + bytes.length == fileContents.length, + "IOUtils::read can read back entire file" + ); + + const tooManyBytes = bytes.length + 1; + fileContents = await IOUtils.read(tmpFileName, { maxBytes: tooManyBytes }); + ok( + ObjectUtils.deepEqual(bytes, fileContents) && + fileContents.length == bytes.length, + "IOUtils::read can read entire file when requested maxBytes is too large" + ); + + await cleanup(tmpFileName); + } + + async function test_move_file() { + const src = OS.Path.join(tmpDir, "test_move_file_src.tmp"); + const dest = OS.Path.join(tmpDir, "test_move_file_dest.tmp"); + const bytes = Uint8Array.of(...new Array(50).keys()); + await IOUtils.write(src, bytes); + + await IOUtils.move(src, dest); + ok( + !(await fileExists(src)) && (await fileExists(dest)), + "IOUtils::move can move files from a worker" + ); + + await cleanup(dest); + } + + async function test_copy_file() { + const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_orig.tmp"); + const destFileName = OS.Path.join(tmpDir, "test_ioutils_copy.tmp"); + await createFile(tmpFileName, "original"); + + await IOUtils.copy(tmpFileName, destFileName); + ok( + (await fileExists(tmpFileName)) && + (await fileHasTextContents(destFileName, "original")), + "IOUtils::copy can copy source to dest in same directory" + ); + + await cleanup(tmpFileName, destFileName); + } + + async function test_make_directory() { + const dir = OS.Path.join(tmpDir, "test_make_dir.tmp.d"); + await IOUtils.makeDirectory(dir); + ok( + OS.File.stat(dir).isDir, + "IOUtils::makeDirectory can make a new directory from a worker" + ); + + await cleanup(dir); + } +}; diff --git a/dom/system/tests/ioutils/test_ioutils.html b/dom/system/tests/ioutils/test_ioutils.html new file mode 100644 index 0000000000..cf62c4c388 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils.html @@ -0,0 +1,26 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script> + "use strict"; + + add_task(async function test_api_is_available_on_window() { + ok(window.IOUtils, "IOUtils is present on the window"); + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_copy_move.html b/dom/system/tests/ioutils/test_ioutils_copy_move.html new file mode 100644 index 0000000000..239295bf7e --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_copy_move.html @@ -0,0 +1,366 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm"); + + // TODO: Remove this import for OS.File. It is currently being used as a + // stop gap for missing IOUtils functionality. + const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + + + const tmpDir = OS.Constants.Path.tmpDir; + + add_task(async function test_move_relative_path() { + const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_move_relative_path.tmp"); + const dest = "relative_to_cwd.tmp"; + await createFile(tmpFileName, "source"); + + info("Test moving a file to a relative destination"); + await Assert.rejects( + IOUtils.move(tmpFileName, dest), + /Could not parse path/, + "IOUtils::move only works with absolute paths" + ); + ok( + await fileHasTextContents(tmpFileName, "source"), + "IOUtils::move doesn't change source file when move fails" + ); + + await cleanup(tmpFileName); + }); + + add_task(async function test_move_rename() { + // Set up. + const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_move_src.tmp"); + const destFileName = OS.Path.join(tmpDir, "test_ioutils_move_dest.tmp"); + await createFile(tmpFileName, "dest"); + // Test. + info("Test move to new file in same directory"); + await IOUtils.move(tmpFileName, destFileName); + info(`Moved ${tmpFileName} to ${destFileName}`); + ok( + !await fileExists(tmpFileName) + && await fileHasTextContents(destFileName, "dest"), + "IOUtils::move can move source to dest in same directory" + ) + + // Set up. + info("Test move to existing file with no overwrite"); + await createFile(tmpFileName, "source"); + // Test. + await Assert.rejects( + IOUtils.move(tmpFileName, destFileName, { noOverwrite: true }), + /Could not move source file\(.*\) to destination\(.*\) because the destination already exists and overwrites are not allowed/, + "IOUtils::move will refuse to move a file if overwrites are disabled" + ); + ok( + await fileExists(tmpFileName) + && await fileHasTextContents(destFileName, "dest"), + "Failed IOUtils::move doesn't move the source file" + ); + + // Test. + info("Test move to existing file with overwrite"); + await IOUtils.move(tmpFileName, destFileName, { noOverwrite: false }); + ok(!await fileExists(tmpFileName), "IOUtils::move moved source"); + ok( + await fileHasTextContents(destFileName, "source"), + "IOUtils::move overwrote the destination with the source" + ); + + // Clean up. + await cleanup(tmpFileName, destFileName); + }); + + add_task(async function test_move_to_dir() { + // Set up. + info("Test move and rename to non-existing directory"); + const tmpFileName = OS.Path.join(tmpDir, "test_move_to_dir.tmp"); + const destDir = OS.Path.join(tmpDir, "test_move_to_dir.tmp.d"); + const dest = OS.Path.join(destDir, "dest.tmp"); + await createFile(tmpFileName); + // Test. + ok(!await IOUtils.exists(destDir), "Expected path not to exist"); + await IOUtils.move(tmpFileName, dest); + ok( + !await fileExists(tmpFileName) && await fileExists(dest), + "IOUtils::move creates non-existing parents if needed" + ); + + // Set up. + info("Test move and rename to existing directory.") + await createFile(tmpFileName); + // Test. + ok(await dirExists(destDir), `Expected ${destDir} to be a directory`); + await IOUtils.move(tmpFileName, dest); + ok( + !await fileExists(tmpFileName) + && await fileExists(dest), + "IOUtils::move can move/rename a file into an existing dir" + ); + + // Set up. + info("Test move to existing directory without specifying leaf name.") + await createFile(tmpFileName); + // Test. + await IOUtils.move(tmpFileName, destDir); + ok(await dirExists(destDir), `Expected ${destDir} to be a directory`); + ok( + !await fileExists(tmpFileName) + && await fileExists(OS.Path.join(destDir, OS.Path.basename(tmpFileName))), + "IOUtils::move can move a file into an existing dir" + ); + + // Clean up. + await cleanup(destDir); + }); + + add_task(async function test_move_dir() { + // Set up. + info("Test rename an empty directory"); + const srcDir = OS.Path.join(tmpDir, "test_move_dir.tmp.d"); + const destDir = OS.Path.join(tmpDir, "test_move_dir_dest.tmp.d"); + await createDir(srcDir); + // Test. + await IOUtils.move(srcDir, destDir); + ok( + !await IOUtils.exists(srcDir) && await dirExists(destDir), + "IOUtils::move can rename directories" + ); + + // Set up. + info("Test move directory and its content into another directory"); + await createDir(srcDir); + await createFile(OS.Path.join(srcDir, "file.tmp"), "foo"); + // Test. + await IOUtils.move(srcDir, destDir); + const destFile = OS.Path.join(destDir, OS.Path.basename(srcDir), "file.tmp"); + ok( + !await IOUtils.exists(srcDir) + && await dirExists(destDir) + && await dirExists(OS.Path.join(destDir, OS.Path.basename(srcDir))) + && await fileHasTextContents(destFile, "foo"), + "IOUtils::move can move a directory and its contents into another one" + ) + + // Clean up. + await cleanup(srcDir, destDir); + }); + + add_task(async function test_move_failures() { + // Set up. + info("Test attempt to rename a non-existent source file"); + const notExistsSrc = OS.Path.join(tmpDir, "not_exists_src.tmp"); + const notExistsDest = OS.Path.join(tmpDir, "not_exists_dest.tmp"); + // Test. + await Assert.rejects( + IOUtils.move(notExistsSrc, notExistsDest), + /Could not move source file\(.*\) because it does not exist/, + "IOUtils::move throws if source file does not exist" + ); + ok( + !await fileExists(notExistsSrc) && !await fileExists(notExistsDest), + "IOUtils::move fails if source file does not exist" + ); + + // Set up. + info("Test attempt to move a directory to a file"); + const destFile = OS.Path.join(tmpDir, "test_move_failures_file_dest.tmp"); + const srcDir = OS.Path.join(tmpDir, "test_move_failure_src.tmp.d"); + await createFile(destFile); + await createDir(srcDir); + // Test. + await Assert.rejects( + IOUtils.move(srcDir, destFile), + /Could not move the source directory\(.*\) to the destination\(.*\) because the destination is not a directory/, + "IOUtils::move throws if try to move dir into an existing file" + ); + + // Clean up. + await cleanup(destFile, srcDir); + }); + + add_task(async function test_copy() { + // Set up. + const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_orig.tmp"); + const destFileName = OS.Path.join(tmpDir, "test_ioutils_copy.tmp"); + await createFile(tmpFileName, "original"); + // Test. + info("Test copy to new file in same directory"); + await IOUtils.copy(tmpFileName, destFileName); + ok( + await fileExists(tmpFileName) + && await fileHasTextContents(destFileName, "original"), + "IOUtils::copy can copy source to dest in same directory" + ); + + // Set up. + info("Test copy to existing file with no overwrite"); + await createFile(tmpFileName, "new contents"); + // Test. + await Assert.rejects( + IOUtils.copy(tmpFileName, destFileName, { noOverwrite: true }), + /Could not copy source file\(.*\) to destination\(.*\) because the destination already exists and overwrites are not allowed/, + "IOUtils::copy will refuse to copy to existing destination if overwrites are disabled" + ); + ok( + await fileExists(tmpFileName) + && await fileHasTextContents(destFileName, "original"), + "Failed IOUtils::move doesn't move the source file" + ); + + // Test. + info("Test copy to existing file with overwrite"); + await IOUtils.copy(tmpFileName, destFileName, { noOverwrite: false }); + ok(await fileExists(tmpFileName), "IOUtils::copy retains source"); + ok( + await fileHasTextContents(destFileName, "new contents"), + "IOUtils::copy overwrote the destination with the source" + ); + + // Clean up. + await cleanup(tmpFileName, destFileName); + }); + + add_task(async function test_copy_file_to_dir() { + // Set up. + info("Test copy file to non-existing directory"); + const tmpFileName = OS.Path.join(tmpDir, "test_copy_file_to_dir.tmp"); + const destDir = OS.Path.join(tmpDir, "test_copy_file_to_dir.tmp.d"); + const dest = OS.Path.join(destDir, "dest.tmp"); + await createFile(tmpFileName); + // Test. + ok(!await IOUtils.exists(destDir), "Expected path not to exist"); + await IOUtils.copy(tmpFileName, dest); + ok( + await fileExists(tmpFileName) && await fileExists(dest), + "IOUtils::copy creates non-existing parents if needed" + ); + + // Set up. + info("Test copy file to existing directory") + await createFile(tmpFileName); + // Test. + ok(await dirExists(destDir), `Expected ${destDir} to be a directory`); + await IOUtils.copy(tmpFileName, dest); + ok( + await fileExists(tmpFileName) + && await fileExists(dest), + "IOUtils::copy can copy a file into an existing dir" + ); + + // Set up. + info("Test copy file to existing directory without specifying leaf name") + await createFile(tmpFileName); + // Test. + await IOUtils.copy(tmpFileName, destDir); + ok(await dirExists(destDir), `Expected ${destDir} to be a directory`); + ok( + await fileExists(tmpFileName) + && await fileExists(OS.Path.join(destDir, OS.Path.basename(tmpFileName))), + "IOUtils::copy can copy a file into an existing dir" + ); + + // Clean up. + await cleanup(tmpFileName, destDir); + }); + + add_task(async function test_copy_dir_recursive() { + // Set up. + info("Test rename an empty directory"); + const srcDir = OS.Path.join(tmpDir, "test_copy_dir.tmp.d"); + const destDir = OS.Path.join(tmpDir, "test_copy_dir_dest.tmp.d"); + await createDir(srcDir); + // Test. + await IOUtils.copy(srcDir, destDir, { recursive: true }); + ok( + await dirExists(srcDir) && await dirExists(destDir), + "IOUtils::copy can recursively copy entire directories" + ); + + // Set up. + info("Test copy directory and its content into another directory"); + await createDir(srcDir); + await createFile(OS.Path.join(srcDir, "file.tmp"), "foo"); + // Test. + await IOUtils.copy(srcDir, destDir, { recursive: true }); + const destFile = OS.Path.join(destDir, OS.Path.basename(srcDir), "file.tmp"); + ok( + await dirExists(srcDir) + && await dirExists(destDir) + && await dirExists(OS.Path.join(destDir, OS.Path.basename(srcDir))) + && await fileHasTextContents(destFile, "foo"), + "IOUtils::copy can move a directory and its contents into another one" + ) + + // Clean up. + await cleanup(srcDir, destDir); + }); + + add_task(async function test_copy_failures() { + // Set up. + info("Test attempt to copy a non-existent source file"); + const notExistsSrc = OS.Path.join(tmpDir, "test_copy_not_exists_src.tmp"); + const notExistsDest = OS.Path.join(tmpDir, "test_copy_not_exists_dest.tmp"); + // Test. + await Assert.rejects( + IOUtils.copy(notExistsSrc, notExistsDest), + /Could not copy source file\(.*\) because it does not exist/, + "IOUtils::copy throws if source file does not exist" + ); + ok( + !await fileExists(notExistsSrc) && !await fileExists(notExistsDest), + "IOUtils::copy failure due to missing source file does not affect destination" + ); + + // Set up. + info("Test attempt to copy a directory to a file"); + const destFile = OS.Path.join(tmpDir, "test_copy_failures_file_dest.tmp"); + const srcDir = OS.Path.join(tmpDir, "test_copy_failure_src.tmp.d"); + await createFile(destFile); + await createDir(srcDir); + // Test. + await Assert.rejects( + IOUtils.copy(srcDir, destFile, { recursive: true }), + /Could not copy the source directory\(.*\) to the destination\(.*\) because the destination is not a directory/, + "IOUtils::copy throws if try to move dir into an existing file" + ); + ok(await fileHasTextContents(destFile, ""), "IOUtils::copy failure does not affect destination"); + + // Set up. + info("Test copy directory without recursive option"); + await createDir(srcDir); + // Test. + await Assert.rejects( + IOUtils.copy(srcDir, notExistsDest, { recursive: false }), + /Refused to copy source directory\(.*\) to the destination\(.*\)/, + "IOUtils::copy throws if try to copy a directory with { recursive: false }" + ); + console.log(`${notExistsDest} exists?`, await IOUtils.exists(notExistsDest)) + ok(!await IOUtils.exists(notExistsDest), "IOUtils::copy failure does not affect destination"); + + // Clean up. + await cleanup(destFile, srcDir); + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_dir_iteration.html b/dom/system/tests/ioutils/test_ioutils_dir_iteration.html new file mode 100644 index 0000000000..2f1181fa23 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_dir_iteration.html @@ -0,0 +1,84 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + + const tmpDir = OS.Constants.Path.tmpDir; + + add_task(async function iterate_dir_failure() { + let notExists = OS.Path.join(tmpDir, 'does_not_exist_dir.tmp.d'); + + await Assert.rejects( + IOUtils.getChildren(notExists), + /Could not get children of file\(.*\) because it does not exist/, + "IOUtils::getChildren rejects if the file does not exist" + ); + ok(!await fileExists(notExists), `Expected ${notExists} not to exist`); + + info('Try to get the children of a regular file'); + + let tmpFileName = OS.Path.join(tmpDir, 'iterator_file.tmp'); + await createFile(tmpFileName) + await Assert.rejects(IOUtils.getChildren(tmpFileName), + /Could not get children of file\(.*\) because it is not a directory/, + "IOUtils::getChildren rejects if the file is not a dir" + ); + + await cleanup(tmpFileName); + }); + + add_task(async function iterate_dir() { + info('Try to get the children of a multi-level directory hierarchy'); + + let root = OS.Path.join(tmpDir, 'iterator.tmp.d'); + let child1 = OS.Path.join(root, 'child1.tmp'); + let child2 = OS.Path.join(root, 'child2.tmp'); + let grandchild = OS.Path.join(child1, 'grandchild.tmp'); + + await createDir(grandchild); // Ancestors will be created. + await createDir(child2); + + let entries = await IOUtils.getChildren(root); + + is(entries.length, 2, `Expected 2 entries below the path at ${root}`); + ok(!entries.includes(grandchild), "IOUtils::getChildren does not enter subdirectories"); + + await cleanup(root); + }); + + add_task(async function iterate_empty_dir() { + info('Try to get the children of an empty directory'); + + let emptyDir = OS.Path.join(tmpDir, 'iterator_empty_dir.tmp.d'); + await createDir(emptyDir); + + is( + (await IOUtils.getChildren(emptyDir)).length, + 0, + "IOUtils::getChildren return an empty array when called on an empty dir" + ); + + await cleanup(emptyDir); + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_mkdir.html b/dom/system/tests/ioutils/test_ioutils_mkdir.html new file mode 100644 index 0000000000..2439443e87 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_mkdir.html @@ -0,0 +1,113 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + + + add_task(async function test_make_directory() { + info("Test creating a new directory"); + const tmpDir = await PathUtils.getTempDir(); + const newDirectoryName = PathUtils.join(tmpDir, "test_ioutils_new_dir.tmp.d"); + await IOUtils.makeDirectory(newDirectoryName); + ok( + await IOUtils.exists(newDirectoryName), + "IOUtils::makeDirectory can create a new directory" + ); + + info("Test creating an existing directory"); + await IOUtils.makeDirectory(newDirectoryName, { ignoreExisting: true }); + ok( + await IOUtils.exists(newDirectoryName), + "IOUtils::makeDirectory can ignore existing directories" + ); + await Assert.rejects( + IOUtils.makeDirectory(newDirectoryName, { ignoreExisting: false }), + /Could not create directory because it already exists at .*/, + "IOUtils::makeDirectory can throw if the target dir exists" + ) + + info("Test creating a nested directory"); + const parentDirName = PathUtils.join(tmpDir, "test_ioutils_mkdir_parent.tmp.d"); + const nestedDirName = PathUtils.join( + parentDirName, + "test_ioutils_mkdir_child.tmp.d" + ); + await Assert.rejects( + IOUtils.makeDirectory(nestedDirName, { createAncestors: false }), + /Could not create directory at .* because the path has missing ancestor components/, + "IOUtils::makeDirectory can fail if the target is missing parents" + ); + ok(!await IOUtils.exists(nestedDirName), `Expected ${nestedDirName} not to exist`); + await IOUtils.makeDirectory(nestedDirName, { createAncestors: true }); + ok( + await IOUtils.exists(nestedDirName), + "IOUtils::makeDirectory can create ancestors of the target directory" + ); + + await cleanup(newDirectoryName, parentDirName); + }); + + add_task(async function test_make_directory_failure() { + info("Try to create a directory where a file already exists"); + const tmpDir = await PathUtils.getTempDir(); + const notADirFileName = PathUtils.join(tmpDir, "test_ioutils_not_a_dir.tmp"); + await createFile(notADirFileName); + + await Assert.rejects( + IOUtils.makeDirectory(notADirFileName, { ignoreExisting: false }), + /Could not create directory because the target file\(.*\) exists and is not a directory/, + "IOUtils::makeDirectory [ignoreExisting: false] throws when the target is an existing file" + ); + ok(await fileExists(notADirFileName), `Expected ${notADirFileName} to exist`); + + await Assert.rejects( + IOUtils.makeDirectory(notADirFileName, { ignoreExisting: true }), + /Could not create directory because the target file\(.*\) exists and is not a directory/, + "IOUtils::makeDirectory [ignoreExisting: true] throws when the target is an existing file" + ); + ok(await fileExists(notADirFileName), `Expected ${notADirFileName} to exist`); + + await cleanup(notADirFileName); + }); + + add_task(async function test_make_directory_permissions() { + if (Services.appinfo.OS === "WINNT") { + ok(true, "Skipping test on unsupported platform (Windows)"); + return; + } + + const tmpDir = await PathUtils.getTempDir(); + const newDir = PathUtils.join(tmpDir, "test_ioutils_mkdir_perms.tmp.d"); + + ok(!await IOUtils.exists(newDir), "Directory does not exist before creation"); + await IOUtils.makeDirectory(newDir, { permissions: 0o751 }); + ok(await IOUtils.exists(newDir), "Directory created"); + + const stat = await IOUtils.stat(newDir); + is(stat.type, "directory", "Directory stat() as directory"); + is(stat.permissions, 0o751, "Directory created with expected permissions"); + + await cleanup(newDir); + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_read_write.html b/dom/system/tests/ioutils/test_ioutils_read_write.html new file mode 100644 index 0000000000..f888f03449 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_read_write.html @@ -0,0 +1,432 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm"); + + // This is presently only used to test compatability between OS.File and + // IOUtils when it comes to writing compressed files. The import and the + // test `test_lz4_osfile_compat` can be removed with OS.File is removed. + const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + + add_task(async function test_read_failure() { + const tmpDir = await PathUtils.getTempDir(); + const doesNotExist = PathUtils.join(tmpDir, "does_not_exist.tmp"); + await Assert.rejects( + IOUtils.read(doesNotExist), + /Could not open the file at .*/, + "IOUtils::read rejects when file does not exist" + ); + }); + + add_task(async function test_write_no_overwrite() { + const tmpDir = await PathUtils.getTempDir(); + + // Make a new file, and try to write to it with overwrites disabled. + const tmpFileName = PathUtils.join(tmpDir, "test_ioutils_overwrite.tmp"); + const untouchableContents = new TextEncoder().encode("Can't touch this!\n"); + + let exists = await IOUtils.exists(tmpFileName); + ok(!exists, `File ${tmpFileName} should not exist before writing`); + + await IOUtils.write(tmpFileName, untouchableContents); + + exists = await IOUtils.exists(tmpFileName); + ok(exists, `File ${tmpFileName} should exist after writing`); + + const newContents = new TextEncoder().encode("Nah nah nah!\n"); + await Assert.rejects( + IOUtils.write(tmpFileName, newContents, { + noOverwrite: true, + }), + /Refusing to overwrite the file at */, + "IOUtils::write rejects writing to existing file if overwrites are disabled" + ); + ok( + await fileHasBinaryContents(tmpFileName, untouchableContents), + "IOUtils::write doesn't change target file when overwrite is refused" + ); + + const bytesWritten = await IOUtils.write( + tmpFileName, + newContents, + { noOverwrite: false /* Default. */ } + ); + is( + bytesWritten, + newContents.length, + "IOUtils::write can overwrite files if specified" + ); + + await cleanup(tmpFileName); + }); + + add_task(async function test_write_with_backup() { + info("Test backup file option with non-existing file"); + + const tmpDir = await PathUtils.getTempDir(); + + let fileContents = new TextEncoder().encode("Original file contents"); + let destFileName = PathUtils.join(tmpDir, "test_write_with_backup_option.tmp"); + let backupFileName = destFileName + ".backup"; + let bytesWritten = + await IOUtils.write(destFileName, fileContents, { + backupFile: backupFileName, + }); + ok( + await fileHasTextContents(destFileName, "Original file contents"), + "IOUtils::write creates a new file with the correct contents" + ); + ok( + !await fileExists(backupFileName), + "IOUtils::write does not create a backup if the target file does not exist" + ); + is( + bytesWritten, + fileContents.length, + "IOUtils::write correctly writes to a new file without performing a backup" + ); + + info("Test backup file option with existing destination"); + let newFileContents = new TextEncoder().encode("New file contents"); + ok(await fileExists(destFileName), `Expected ${destFileName} to exist`); + bytesWritten = + await IOUtils.write(destFileName, newFileContents, { + backupFile: backupFileName, + }); + ok( + await fileHasTextContents(backupFileName, "Original file contents"), + "IOUtils::write can backup an existing file before writing" + ); + ok( + await fileHasTextContents(destFileName, "New file contents"), + "IOUtils::write can create the target with the correct contents" + ); + is( + bytesWritten, + newFileContents.length, + "IOUtils::write correctly writes to the target after taking a backup" + ); + + await cleanup(destFileName, backupFileName); + }); + + add_task(async function test_write_with_backup_and_tmp() { + info("Test backup with tmp and backup file options, non-existing destination"); + + const tmpDir = await PathUtils.getTempDir(); + + let fileContents = new TextEncoder().encode("Original file contents"); + let destFileName = PathUtils.join(tmpDir, "test_write_with_backup_and_tmp_options.tmp"); + let backupFileName = destFileName + ".backup"; + let tmpFileName = PathUtils.join(tmpDir, "temp_file.tmp"); + let bytesWritten = + await IOUtils.write(destFileName, fileContents, { + backupFile: backupFileName, + tmpPath: tmpFileName, + }); + ok(!await fileExists(tmpFileName), "IOUtils::write cleans up the tmpFile"); + ok( + !await fileExists(backupFileName), + "IOUtils::write does not create a backup if the target file does not exist" + ); + ok( + await fileHasTextContents(destFileName, "Original file contents"), + "IOUtils::write can write to the destination when a temporary file is used" + ); + is( + bytesWritten, + fileContents.length, + "IOUtils::write can copy tmp file to destination without performing a backup" + ); + + info("Test backup with tmp and backup file options, existing destination"); + let newFileContents = new TextEncoder().encode("New file contents"); + bytesWritten = + await IOUtils.write(destFileName, newFileContents, { + backupFile: backupFileName, + tmpPath: tmpFileName, + }); + + ok(!await fileExists(tmpFileName), "IOUtils::write cleans up the tmpFile"); + ok( + await fileHasTextContents(backupFileName, "Original file contents"), + "IOUtils::write can create a backup if the target file exists" + ); + ok( + await fileHasTextContents(destFileName, "New file contents"), + "IOUtils::write can write to the destination when a temporary file is used" + ); + is( + bytesWritten, + newFileContents.length, + "IOUtils::write IOUtils::write can move tmp file to destination after performing a backup" + ); + + await cleanup(destFileName, backupFileName); + }); + + add_task(async function test_partial_read() { + const tmpDir = await PathUtils.getTempDir(); + + const tmpFileName = PathUtils.join(tmpDir, "test_ioutils_partial_read.tmp"); + const bytes = Uint8Array.of(...new Array(50).keys()); + const bytesWritten = await IOUtils.write(tmpFileName, bytes); + is( + bytesWritten, + 50, + "IOUtils::write can write entire byte array to file" + ); + + // Read just the first 10 bytes. + const first10 = bytes.slice(0, 10); + const bytes10 = await IOUtils.read(tmpFileName, { maxBytes: 10 }); + ok( + ObjectUtils.deepEqual(bytes10, first10), + "IOUtils::read can read part of a file, up to specified max bytes" + ); + + // Trying to explicitly read nothing isn't useful, but it should still + // succeed. + const bytes0 = await IOUtils.read(tmpFileName, { maxBytes: 0 }); + is(bytes0.length, 0, "IOUtils::read can read 0 bytes"); + + await cleanup(tmpFileName); + }); + + add_task(async function test_empty_read_and_write() { + // Trying to write an empty file isn't very useful, but it should still + // succeed. + + const tmpDir = await PathUtils.getTempDir(); + + const tmpFileName = PathUtils.join(tmpDir, "test_ioutils_empty.tmp"); + const emptyByteArray = new Uint8Array(0); + const bytesWritten = await IOUtils.write( + tmpFileName, + emptyByteArray + ); + is(bytesWritten, 0, "IOUtils::write can create an empty file"); + + // Trying to explicitly read nothing isn't useful, but it should still + // succeed. + const bytes0 = await IOUtils.read(tmpFileName, { maxBytes: 0 }); + is(bytes0.length, 0, "IOUtils::read can read 0 bytes"); + + // Implicitly try to read nothing. + const nothing = await IOUtils.read(tmpFileName); + is(nothing.length, 0, "IOUtils:: read can read empty files"); + + await cleanup(tmpFileName); + }); + + add_task(async function test_full_read_and_write() { + // Write a file. + + const tmpDir = await PathUtils.getTempDir(); + + info("Test writing to a new binary file"); + const tmpFileName = PathUtils.join(tmpDir, "test_ioutils_numbers.tmp"); + const bytes = Uint8Array.of(...new Array(50).keys()); + const bytesWritten = await IOUtils.write(tmpFileName, bytes); + is( + bytesWritten, + 50, + "IOUtils::write can write entire byte array to file" + ); + + // Read it back. + info("Test reading a binary file"); + let fileContents = await IOUtils.read(tmpFileName); + ok( + ObjectUtils.deepEqual(bytes, fileContents) && + bytes.length == fileContents.length, + "IOUtils::read can read back entire file" + ); + + const tooManyBytes = bytes.length + 1; + fileContents = await IOUtils.read(tmpFileName, { maxBytes: tooManyBytes }); + ok( + ObjectUtils.deepEqual(bytes, fileContents) && + fileContents.length == bytes.length, + "IOUtils::read can read entire file when requested maxBytes is too large" + ); + + // Clean up. + await cleanup(tmpFileName); + }); + + add_task(async function test_write_relative_path() { + const tmpFileName = "test_ioutils_write_relative_path.tmp"; + const bytes = Uint8Array.of(...new Array(50).keys()); + + info("Test writing a file at a relative destination"); + await Assert.rejects( + IOUtils.write(tmpFileName, bytes), + /Could not parse path/, + "IOUtils::write only works with absolute paths" + ); + }); + + add_task(async function test_read_relative_path() { + const tmpFileName = "test_ioutils_read_relative_path.tmp"; + + info("Test reading a file at a relative destination"); + await Assert.rejects( + IOUtils.read(tmpFileName), + /Could not parse path/, + "IOUtils::write only works with absolute paths" + ); + }); + + add_task(async function test_lz4() { + const tmpDir = await PathUtils.getTempDir(); + const tmpFileName = PathUtils.join(tmpDir, "test_ioutils_lz4.tmp"); + + info("Test writing lz4 encoded data"); + const varyingBytes = Uint8Array.of(...new Array(50).keys()); + let bytesWritten = await IOUtils.write(tmpFileName, varyingBytes, { compress: true }); + is(bytesWritten, 64, "Expected to write 64 bytes"); + + info("Test reading lz4 encoded data"); + let readData = await IOUtils.read(tmpFileName, { decompress: true }); + ok(readData.equals(varyingBytes), "IOUtils can write and read back LZ4 encoded data"); + + info("Test writing lz4 compressed data"); + const repeatedBytes = Uint8Array.of(...new Array(50).fill(1)); + bytesWritten = await IOUtils.write(tmpFileName, repeatedBytes, { compress: true }); + is(bytesWritten, 23, "Expected 50 bytes to compress to 23 bytes"); + + info("Test reading lz4 encoded data"); + readData = await IOUtils.read(tmpFileName, { decompress: true }); + ok(readData.equals(repeatedBytes), "IOUtils can write and read back LZ4 compressed data"); + + info("Test writing empty lz4 compressed data") + const empty = new Uint8Array(); + bytesWritten = await IOUtils.write(tmpFileName, empty, { compress: true }); + is(bytesWritten, 12, "Expected to write just the LZ4 header, with a content length of 0"); + + + info("Test reading empty lz4 compressed data") + const readEmpty = await IOUtils.read(tmpFileName, { decompress: true }); + ok(readEmpty.equals(empty), "IOUtils can write and read back empty buffers with LZ4"); + const readEmptyRaw = await IOUtils.read(tmpFileName, { decompress: false }); + is(readEmptyRaw.length, 12, "Expected to read back just the LZ4 header"); + const expectedHeader = Uint8Array.of(109, 111, 122, 76, 122, 52, 48, 0, 0, 0, 0, 0); // "mozLz40\0\0\0\0" + ok(readEmptyRaw.equals(expectedHeader), "Expected to read header with content length of 0"); + + await cleanup(tmpFileName); + }); + + add_task(async function test_lz4_osfile_compat() { + const tmpDir = await PathUtils.getTempDir(); + const osfileTmpFile = PathUtils.join(tmpDir, "test_ioutils_lz4_compat_osfile.tmp"); + const ioutilsTmpFile = PathUtils.join(tmpDir, "test_ioutils_lz4_compat_ioutils.tmp"); + + info("Test OS.File and IOUtils write the same file with LZ4 compression enabled") + const repeatedBytes = Uint8Array.of(...new Array(50).fill(1)); + let expectedBytes = 23; + let ioutilsBytes = await IOUtils.write(ioutilsTmpFile, repeatedBytes, { compress: true }); + let osfileBytes = await OS.File.writeAtomic(osfileTmpFile, repeatedBytes, { compression: "lz4" }); + is(ioutilsBytes, expectedBytes, "IOUtils writes the expected number of bytes for compression"); + is(osfileBytes, ioutilsBytes, "OS.File and IOUtils write the same number of bytes for LZ4 compression"); + + info("Test OS.File can read a file compressed by IOUtils"); + const osfileReadBytes = await OS.File.read(ioutilsTmpFile, { compression: "lz4" }); + ok(osfileReadBytes.every(byte => byte === 1), "OS.File can read a file compressed by IOUtils"); + is(osfileReadBytes.length, 50, "OS.File reads the right number of bytes from a file compressed by IOUtils") + + info("Test IOUtils can read a file compressed by OS.File"); + const ioutilsReadBytes = await IOUtils.read(osfileTmpFile, { decompress: true }); + ok(ioutilsReadBytes.every(byte => byte === 1), "IOUtils can read a file compressed by OS.File"); + is(ioutilsReadBytes.length, 50, "IOUtils reads the right number of bytes from a file compressed by OS.File") + + await cleanup(osfileTmpFile, ioutilsTmpFile); + }); + + add_task(async function test_lz4_bad_call() { + const tmpDir = await PathUtils.getTempDir(); + const tmpFileName = PathUtils.join(tmpDir, "test_ioutils_lz4_bad_call.tmp"); + + info("Test decompression with invalid options"); + const varyingBytes = Uint8Array.of(...new Array(50).keys()); + let bytesWritten = await IOUtils.write(tmpFileName, varyingBytes, { compress: true }); + is(bytesWritten, 64, "Expected to write 64 bytes"); + await Assert.rejects( + IOUtils.read(tmpFileName, { maxBytes: 4, decompress: true }), + /The `maxBytes` and `decompress` options are not compatible/, + "IOUtils::read rejects when maxBytes and decompress options are both used" + ); + + await cleanup(tmpFileName) + }); + + add_task(async function test_lz4_failure() { + const tmpDir = await PathUtils.getTempDir(); + const tmpFileName = PathUtils.join(tmpDir, "test_ioutils_lz4_fail.tmp"); + + info("Test decompression of non-lz4 data"); + const repeatedBytes = Uint8Array.of(...new Array(50).fill(1)); + await IOUtils.write(tmpFileName, repeatedBytes, { compress: false }); + + await Assert.rejects( + IOUtils.read(tmpFileName, { decompress: true }), + (actual) => { + is(actual.constructor, DOMException, + "rejection reason constructor for decompress with bad header"); + is(actual.name, "NotReadableError", + "rejection error name for decompress with bad header"); + ok(/Could not decompress file because it has an invalid LZ4 header \(wrong magic number: .*\)/ + .test(actual.message), + "rejection error message for decompress with bad header. Got " + + actual.message); + return true; + }, + "IOUtils::read fails to decompress LZ4 data with a bad header" + ); + + info("Test decompression of short byte buffer"); + const elevenBytes = Uint8Array.of(...new Array(11).fill(1)); + await IOUtils.write(tmpFileName, elevenBytes, { compress: false }); + + await Assert.rejects( + IOUtils.read(tmpFileName, { decompress: true }), + /Could not decompress file because the buffer is too short/, + "IOUtils::read fails to decompress LZ4 data with missing header" + ); + + info("Test decompression of valid header, but corrupt contents"); + const headerFor10bytes = [109, 111, 122, 76, 122, 52, 48, 0, 10, 0, 0, 0] // "mozlz40\0" + 4 byte length + const badContents = new Array(11).fill(255); // Bad leading byte, followed by uncompressed stream. + const goodHeaderBadContents = Uint8Array.of(...headerFor10bytes, ...badContents); + await IOUtils.write(tmpFileName, goodHeaderBadContents, { compress: false }); + + await Assert.rejects( + IOUtils.read(tmpFileName, { decompress: true }), + /Could not decompress file contents, the file may be corrupt/, + "IOUtils::read fails to read corrupt LZ4 contents with a correct header" + ); + + await cleanup(tmpFileName); + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_read_write_json.html b/dom/system/tests/ioutils/test_ioutils_read_write_json.html new file mode 100644 index 0000000000..01d2771888 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_read_write_json.html @@ -0,0 +1,153 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + const { ObjectUtils } = ChromeUtils.import( + "resource://gre/modules/ObjectUtils.jsm" + ); + + const OBJECT = { + "foo": [ + "bar", + 123, + 456.789, + true, + false, + null, + ], + "bar": { + "baz": {}, + }, + }; + + const ARRAY = [1, 2.3, true, false, null, { "foo": "bar" }]; + + const PRIMITIVES = [123, true, false, "hello, world", null]; + + add_task(async function read_json() { + const tmpDir = await PathUtils.getTempDir(); + const filename = PathUtils.join(tmpDir, "test_ioutils_read_json.tmp"); + + info("Testing IOUtils.readJSON() with a serialized object..."); + await IOUtils.writeUTF8(filename, JSON.stringify(OBJECT)); + const readObject = await IOUtils.readJSON(filename); + const parsedObject = JSON.parse(await IOUtils.readUTF8(filename)); + ok(ObjectUtils.deepEqual(OBJECT, readObject), "JSON objects should round-trip"); + ok( + ObjectUtils.deepEqual(parsedObject, readObject), + "IOUtils.readJSON() equivalent to JSON.parse() for objects" + ); + + info("Testing IOUtils.readJSON() with a serialized array..."); + await IOUtils.writeUTF8(filename, JSON.stringify(ARRAY)); + const readArray = await IOUtils.readJSON(filename); + const parsedArray = JSON.parse(await IOUtils.readUTF8(filename)); + ok(ObjectUtils.deepEqual(ARRAY, readArray), "JSON arrays should round-trip"); + ok( + ObjectUtils.deepEqual(parsedArray, readArray), + "IOUtils.readJSON() equivalent to JSON.parse(IOUtils.readUTF8()) for arrays" + ); + + info("Testing IOUtils.readJSON() with serialized primitives..."); + for (const primitive of PRIMITIVES) { + await IOUtils.writeUTF8(filename, JSON.stringify(primitive)); + const readPrimitive = await IOUtils.readJSON(filename); + const parsedPrimitive = JSON.parse(await IOUtils.readUTF8(filename)); + ok(primitive === readPrimitive, `JSON primitive ${primitive} should round trip`); + ok( + readPrimitive === parsedPrimitive, + `${readPrimitive} === ${parsedPrimitive} -- IOUtils.readJSON() equivalent to JSON.parse() for primitive` + ); + } + + info("Testing IOUtils.readJSON() with a file that does not exist..."); + const notExistsFilename = PathUtils.join(tmpDir, "test_ioutils_read_json_not_exists.tmp"); + ok(!await IOUtils.exists(notExistsFilename), `${notExistsFilename} should not exist`); + await Assert.rejects( + IOUtils.readJSON(notExistsFilename), + /NotFoundError: Could not open the file at/, + "IOUtils::readJSON rejects when file does not exist" + ); + + info("Testing IOUtils.readJSON() with a file that does not contain JSON"); + const invalidFilename = PathUtils.join(tmpDir, "test_ioutils_read_json_invalid.tmp"); + await IOUtils.writeUTF8(invalidFilename, ":)"); + + await Assert.rejects( + IOUtils.readJSON(invalidFilename), + /SyntaxError: JSON\.parse/, + "IOUTils::readJSON rejects when the file contains invalid JSON" + ); + + await cleanup(filename, invalidFilename); + }); + + add_task(async function write_json() { + const tmpDir = await PathUtils.getTempDir(); + const filename = PathUtils.join(tmpDir, "test_ioutils_write_json.tmp"); + + info("Testing IOUtils.writeJSON() with an object..."); + await IOUtils.writeJSON(filename, OBJECT); + const readObject = await IOUtils.readJSON(filename); + const readObjectStr = await IOUtils.readUTF8(filename); + ok(ObjectUtils.deepEqual(OBJECT, readObject), "JSON objects should round-trip"); + ok( + readObjectStr === JSON.stringify(OBJECT), + "IOUtils.writeJSON() eqvuialent to JSON.stringify() for an object" + ); + + info("Testing IOUtils.writeJSON() with an array..."); + await IOUtils.writeJSON(filename, ARRAY); + const readArray = await IOUtils.readJSON(filename); + const readArrayStr = await IOUtils.readUTF8(filename); + ok(ObjectUtils.deepEqual(ARRAY, readArray), "JSON arrays should round-trip"); + ok( + readArrayStr === JSON.stringify(ARRAY), + "IOUtils.writeJSON() equivalent to JSON.stringify() for an array" + ); + + info("Testing IOUtils.writeJSON() with primitives..."); + for (const primitive of PRIMITIVES) { + await IOUtils.writeJSON(filename, primitive); + const readPrimitive = await IOUtils.readJSON(filename); + const readPrimitiveStr = await IOUtils.readUTF8(filename); + ok( + primitive === readPrimitive, + `${primitive} === ${readPrimitive} -- IOUtils.writeJSON() should round trip primitive` + ); + ok( + readPrimitiveStr === JSON.stringify(primitive), + `${readPrimitiveStr} === ${JSON.stringify(primitive)} -- IOUtils.writeJSON() equivalent to JSON.stringify for primitive` + ); + } + + info("Testing IOUtils.writeJSON() with unserializable objects..."); + await Assert.rejects( + IOUtils.writeJSON(filename, window), + /TypeError: cyclic object value/, + "IOUtils.writeJSON() cannot write cyclic objects" + ); + + await cleanup(filename); + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html b/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html new file mode 100644 index 0000000000..80a384a21e --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html @@ -0,0 +1,389 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm"); + + // TODO: Remove this import for OS.File. It is currently being used as a + // stop gap for missing IOUtils functionality. + const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + + + const tmpDir = OS.Constants.Path.tmpDir; + + // This is an impossible sequence of bytes in an UTF-8 encoded file. + // See section 3.5.3 of this text: + // https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt + const invalidUTF8 = Uint8Array.of(0xfe, 0xfe, 0xff, 0xff); + + add_task(async function test_read_utf8_failure() { + info("Test attempt to read non-existent file (UTF8)"); + const doesNotExist = OS.Path.join(tmpDir, "does_not_exist.tmp"); + await Assert.rejects( + IOUtils.readUTF8(doesNotExist), + /Could not open the file at .*/, + "IOUtils::readUTF8 rejects when file does not exist" + ); + + info("Test attempt to read invalid UTF-8"); + const invalidUTF8File = OS.Path.join(tmpDir, "invalid_utf8.tmp"); + + // Deliberately write the invalid byte sequence to file. + await IOUtils.write(invalidUTF8File, invalidUTF8); + + await Assert.rejects( + IOUtils.readUTF8(invalidUTF8File), + /Could not read file\(.*\) because it is not UTF-8 encoded/, + "IOUtils::readUTF8 will reject when reading a file that is not valid UTF-8" + ); + + await cleanup(invalidUTF8File); + }); + + add_task(async function test_write_utf8_no_overwrite() { + // Make a new file, and try to write to it with overwrites disabled. + const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_write_utf8_overwrite.tmp"); + const untouchableContents = "Can't touch this!\n"; + await IOUtils.writeUTF8(tmpFileName, untouchableContents); + + const newContents = "Nah nah nah!\n"; + await Assert.rejects( + IOUtils.writeUTF8(tmpFileName, newContents, { + noOverwrite: true, + }), + /Refusing to overwrite the file at */, + "IOUtils::writeUTF8 rejects writing to existing file if overwrites are disabled" + ); + ok( + await fileHasTextContents(tmpFileName, untouchableContents), + "IOUtils::writeUTF8 doesn't change target file when overwrite is refused" + ); + + const bytesWritten = await IOUtils.writeUTF8( + tmpFileName, + newContents, + { noOverwrite: false /* Default. */ } + ); + is( + bytesWritten, + newContents.length, + "IOUtils::writeUTF8 can overwrite files if specified" + ); + ok( + await fileHasTextContents(tmpFileName, newContents), + "IOUtils::writeUTF8 overwrites with the expected contents" + ); + + await cleanup(tmpFileName); + }); + + add_task(async function test_write_with_backup() { + info("Test backup file option with non-existing file"); + let fileContents = "Original file contents"; + let destFileName = OS.Path.join(tmpDir, "test_write_utf8_with_backup_option.tmp"); + let backupFileName = destFileName + ".backup"; + let bytesWritten = + await IOUtils.writeUTF8(destFileName, fileContents, { + backupFile: backupFileName, + }); + ok( + await fileHasTextContents(destFileName, "Original file contents"), + "IOUtils::writeUTF8 creates a new file with the correct contents" + ); + ok( + !await fileExists(backupFileName), + "IOUtils::writeUTF8 does not create a backup if the target file does not exist" + ); + is( + bytesWritten, + fileContents.length, + "IOUtils::write correctly writes to a new file without performing a backup" + ); + + info("Test backup file option with existing destination"); + let newFileContents = "New file contents"; + ok(await fileExists(destFileName), `Expected ${destFileName} to exist`); + bytesWritten = + await IOUtils.writeUTF8(destFileName, newFileContents, { + backupFile: backupFileName, + }); + ok( + await fileHasTextContents(backupFileName, "Original file contents"), + "IOUtils::writeUTF8 can backup an existing file before writing" + ); + ok( + await fileHasTextContents(destFileName, "New file contents"), + "IOUtils::writeUTF8 can create the target with the correct contents" + ); + is( + bytesWritten, + newFileContents.length, + "IOUtils::writeUTF8 correctly writes to the target after taking a backup" + ); + + await cleanup(destFileName, backupFileName); + }); + + add_task(async function test_write_with_backup_and_tmp() { + info("Test backup with tmp and backup file options, non-existing destination"); + let fileContents = "Original file contents"; + let destFileName = OS.Path.join(tmpDir, "test_write_utf8_with_backup_and_tmp_options.tmp"); + let backupFileName = destFileName + ".backup"; + let tmpFileName = OS.Path.join(tmpDir, "temp_file.tmp"); + let bytesWritten = + await IOUtils.writeUTF8(destFileName, fileContents, { + backupFile: backupFileName, + tmpPath: tmpFileName, + }); + ok(!await fileExists(tmpFileName), "IOUtils::writeUTF8 cleans up the tmpFile"); + ok( + !await fileExists(backupFileName), + "IOUtils::writeUTF8 does not create a backup if the target file does not exist" + ); + ok( + await fileHasTextContents(destFileName, "Original file contents"), + "IOUtils::writeUTF8 can write to the destination when a temporary file is used" + ); + is( + bytesWritten, + fileContents.length, + "IOUtils::writeUTF8 can copy tmp file to destination without performing a backup" + ); + + info("Test backup with tmp and backup file options, existing destination"); + let newFileContents = "New file contents"; + bytesWritten = + await IOUtils.writeUTF8(destFileName, newFileContents, { + backupFile: backupFileName, + tmpPath: tmpFileName, + }); + + ok(!await fileExists(tmpFileName), "IOUtils::writeUTF8 cleans up the tmpFile"); + ok( + await fileHasTextContents(backupFileName, "Original file contents"), + "IOUtils::writeUTF8 can create a backup if the target file exists" + ); + ok( + await fileHasTextContents(destFileName, "New file contents"), + "IOUtils::writeUTF8 can write to the destination when a temporary file is used" + ); + is( + bytesWritten, + newFileContents.length, + "IOUtils::writeUTF8 can move tmp file to destination after performing a backup" + ); + + await cleanup(destFileName, backupFileName); + }); + + add_task(async function test_empty_read_and_write_utf8() { + const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_empty_utf8.tmp"); + const emptyString = "" + const bytesWritten = await IOUtils.writeUTF8( + tmpFileName, + emptyString + ); + is(bytesWritten, 0, "IOUtils::writeUTF8 can create an empty file"); + + const nothing = await IOUtils.readUTF8(tmpFileName); + is(nothing.length, 0, "IOUtils::readUTF8 can read empty files"); + + await cleanup(tmpFileName); + }); + + add_task(async function test_full_read_and_write_utf8() { + // Write a file. + info("Test writing emoji file"); + const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_emoji.tmp"); + + // Make sure non-ASCII text is supported for writing and reading back. + // For fun, a sampling of space-separated emoji characters from different + // Unicode versions, including multi-byte glyphs that are rendered using + // ZWJ sequences. + const emoji = "☕️ ⚧️ 😀 🖖🏿 🤠 🏳️🌈 🥠 🏴☠️ 🪐"; + const expectedBytes = 71; + const bytesWritten = await IOUtils.writeUTF8(tmpFileName, emoji); + is( + bytesWritten, + expectedBytes, + "IOUtils::writeUTF8 can write emoji to file" + ); + + // Read it back. + info("Test reading emoji from file"); + let fileContents = await IOUtils.readUTF8(tmpFileName); + ok( + emoji == fileContents && + emoji.length == fileContents.length, + "IOUtils::readUTF8 can read back entire file" + ); + + // Clean up. + await cleanup(tmpFileName); + }); + + add_task(async function test_write_utf8_relative_path() { + const tmpFileName = "test_ioutils_write_utf8_relative_path.tmp"; + + info("Test writing a file at a relative destination"); + await Assert.rejects( + IOUtils.writeUTF8(tmpFileName, "foo"), + /Could not parse path/, + "IOUtils::writeUTF8 only works with absolute paths" + ); + }); + + add_task(async function test_read_utf8_relative_path() { + const tmpFileName = "test_ioutils_read_utf8_relative_path.tmp"; + + info("Test reading a file at a relative destination"); + await Assert.rejects( + IOUtils.readUTF8(tmpFileName), + /Could not parse path/, + "IOUtils::readUTF8 only works with absolute paths" + ); + }); + + + add_task(async function test_utf8_lz4() { + const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_utf8_lz4.tmp"); + + info("Test writing lz4 encoded UTF-8 string"); + const emoji = "☕️ ⚧️ 😀 🖖🏿 🤠 🏳️🌈 🥠 🏴☠️ 🪐"; + let bytesWritten = await IOUtils.writeUTF8(tmpFileName, emoji, { compress: true }); + is(bytesWritten, 83, "Expected to write 64 bytes"); + + info("Test reading lz4 encoded UTF-8 string"); + let readData = await IOUtils.readUTF8(tmpFileName, { decompress: true }); + is(readData, emoji, "IOUtils can write and read back UTF-8 LZ4 encoded data"); + + info("Test writing lz4 compressed UTF-8 string"); + const lotsOfCoffee = new Array(24).fill("☕️").join(""); // ☕️ is 3 bytes in UTF-8: \0xe2 \0x98 \0x95 + bytesWritten = await IOUtils.writeUTF8(tmpFileName, lotsOfCoffee, { compress: true }); + console.log(bytesWritten); + is(bytesWritten, 28, "Expected 72 bytes to compress to 28 bytes"); + + info("Test reading lz4 encoded UTF-8 string"); + readData = await IOUtils.readUTF8(tmpFileName, { decompress: true }); + is(readData, lotsOfCoffee, "IOUtils can write and read back UTF-8 LZ4 compressed data"); + + info("Test writing empty lz4 compressed UTF-8 string") + const empty = ""; + bytesWritten = await IOUtils.writeUTF8(tmpFileName, empty, { compress: true }); + is(bytesWritten, 12, "Expected to write just the LZ4 header"); + + info("Test reading empty lz4 compressed UTF-8 string") + const readEmpty = await IOUtils.readUTF8(tmpFileName, { decompress: true }); + is(readEmpty, empty, "IOUtils can write and read back empty buffers with LZ4"); + const readEmptyRaw = await IOUtils.readUTF8(tmpFileName, { decompress: false }); + is(readEmptyRaw.length, 12, "Expected to read back just the LZ4 header"); + + await cleanup(tmpFileName); + }); + + add_task(async function test_utf8_lz4_osfile_compat() { + const osfileTmpFile = OS.Path.join(tmpDir, "test_ioutils_utf8_lz4_compat_osfile.tmp"); + const ioutilsTmpFile = OS.Path.join(tmpDir, "test_ioutils_utf8_lz4_compat_ioutils.tmp"); + + info("Test OS.File and IOUtils write the same UTF-8 file with LZ4 compression enabled") + const emoji = "☕️ ⚧️ 😀 🖖🏿 🤠 🏳️🌈 🥠 🏴☠️ 🪐"; + let expectedBytes = 83; + let ioutilsBytes = await IOUtils.writeUTF8(ioutilsTmpFile, emoji, { compress: true }); + let osfileBytes = await OS.File.writeAtomic(osfileTmpFile, emoji, { compression: "lz4" }); + is(ioutilsBytes, expectedBytes, "IOUtils writes the expected number of bytes for compression"); + is(osfileBytes, ioutilsBytes, "OS.File and IOUtils write the same number of bytes for LZ4 compression"); + + info("Test OS.File can read an UTF-8 file compressed by IOUtils"); + const osfileReadStr = await OS.File.read(ioutilsTmpFile, { compression: "lz4", encoding: "utf-8" }); + is(osfileReadStr, emoji, "OS.File can read an UTF-8 file compressed by IOUtils") + + info("Test IOUtils can read an UTF-8 file compressed by OS.File"); + const ioutilsReadString = await IOUtils.readUTF8(ioutilsTmpFile, { decompress: true }); + is(ioutilsReadString, emoji, "IOUtils can read an UTF-8 file compressed by OS.File"); + + await cleanup(osfileTmpFile, ioutilsTmpFile); + }); + + add_task(async function test_utf8_lz4_bad_call() { + const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_utf8_lz4_bad_call.tmp"); + + info("readUTF8 ignores the maxBytes option if provided"); + const emoji = "☕️ ⚧️ 😀 🖖🏿 🤠 🏳️🌈 🥠 🏴☠️ 🪐"; + let bytesWritten = await IOUtils.writeUTF8(tmpFileName, emoji, { compress: true }); + is(bytesWritten, 83, "Expected to write 83 bytes"); + + let readData = await IOUtils.readUTF8(tmpFileName, { maxBytes: 4, decompress: true }); + is(readData, emoji, "IOUtils can write and read back UTF-8 LZ4 encoded data"); + + await cleanup(tmpFileName) + }); + + add_task(async function test_utf8_lz4_failure() { + const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_utf8_lz4_fail.tmp"); + + info("Test decompression of non-lz4 UTF-8 string"); + const repeatedBytes = Uint8Array.of(...new Array(50).fill(1)); + await IOUtils.write(tmpFileName, repeatedBytes, { compress: false }); + + await Assert.rejects( + IOUtils.readUTF8(tmpFileName, { decompress: true }), + /Could not decompress file because it has an invalid LZ4 header \(wrong magic number: .*\)/, + "IOUtils::readUTF8 fails to decompress LZ4 data with a bad header" + ); + + info("Test UTF-8 decompression of short byte buffer"); + const elevenBytes = Uint8Array.of(...new Array(11).fill(1)); + await IOUtils.write(tmpFileName, elevenBytes, { compress: false }); + + await Assert.rejects( + IOUtils.readUTF8(tmpFileName, { decompress: true }), + /Could not decompress file because the buffer is too short/, + "IOUtils::readUTF8 fails to decompress LZ4 data with missing header" + ); + + info("Test UTF-8 decompression of valid header, but corrupt contents"); + const headerFor10bytes = [109, 111, 122, 76, 122, 52, 48, 0, 10, 0, 0, 0] // "mozlz40\0" + 4 byte length + const badContents = new Array(11).fill(255); // Bad leading byte, followed by uncompressed stream. + const goodHeaderBadContents = Uint8Array.of(...headerFor10bytes, ...badContents); + await IOUtils.write(tmpFileName, goodHeaderBadContents, { compress: false }); + + await Assert.rejects( + IOUtils.readUTF8(tmpFileName, { decompress: true }), + /Could not decompress file contents, the file may be corrupt/, + "IOUtils::readUTF8 fails to read corrupt LZ4 contents with a correct header" + ); + + info("Testing decompression of an empty file (no header)"); + { + const n = await IOUtils.writeUTF8(tmpFileName, ""); + ok(n === 0, "Overwrote with empty file"); + } + await Assert.rejects( + IOUtils.readUTF8(tmpFileName, { decompress: true }), + /Could not decompress file because the buffer is too short/, + "IOUtils::readUTF8 fails to decompress empty files" + ); + + await cleanup(tmpFileName); + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_remove.html b/dom/system/tests/ioutils/test_ioutils_remove.html new file mode 100644 index 0000000000..b30d86654d --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_remove.html @@ -0,0 +1,97 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm"); + + // TODO: Remove this import for OS.File. It is currently being used as a + // stop gap for missing IOUtils functionality. + const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + + + const tmpDir = OS.Constants.Path.tmpDir; + + add_task(async function test_create_and_remove_file() { + info("Test creating and removing a single file"); + const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_create_and_remove.tmp"); + await IOUtils.write(tmpFileName, new Uint8Array(0)); + ok(await fileExists(tmpFileName), `Expected file ${tmpFileName} to exist`); + + await IOUtils.remove(tmpFileName); + ok(!await fileExists(tmpFileName), "IOUtils::remove can remove files"); + + info("Test creating and removing an empty directory"); + const tmpDirName = OS.Path.join(tmpDir, "test_ioutils_create_and_remove.tmp.d"); + await IOUtils.makeDirectory(tmpDirName); + ok(await dirExists(tmpDirName), `Expected directory ${tmpDirName} to exist`); + + await IOUtils.remove(tmpDirName); + ok(!await dirExists(tmpDirName), "IOUtils::remove can remove empty directories"); + }); + + add_task(async function test_remove_non_existing() { + const tmpFileName = OS.Path.join(tmpDir, "test_ioutil_remove_non_existing.tmp"); + ok(!await fileExists(tmpFileName), `Expected file ${tmpFileName} not to exist`); + + await IOUtils.remove(tmpFileName, { ignoreAbsent: true }); + ok(!await fileExists(tmpFileName), "IOUtils::remove can ignore missing files without error"); + + await Assert.rejects( + IOUtils.remove(tmpFileName, { ignoreAbsent: false }), + /Could not remove the file at .* because it does not exist/, + "IOUtils::remove can throw an error when target file is missing" + ); + ok(!await fileExists(tmpFileName), `Expected file ${tmpFileName} not to exist`); + }); + + add_task(async function test_remove_recursive() { + const tmpParentDir = OS.Path.join(tmpDir, "test_ioutils_remove.tmp.d"); + const tmpChildDir = OS.Path.join(tmpParentDir, "child.tmp.d"); + const tmpTopLevelFileName = OS.Path.join(tmpParentDir, "top.tmp"); + const tmpNestedFileName = OS.Path.join(tmpChildDir, "nested.tmp"); + await createDir(tmpChildDir); + await createFile(tmpTopLevelFileName, ""); + await createFile(tmpNestedFileName, ""); + + ok( + await fileExists(tmpTopLevelFileName), + `Expected file ${tmpTopLevelFileName} to exist` + ); + ok( + await fileExists(tmpNestedFileName), + `Expected file ${tmpNestedFileName} to exist` + ); + + await Assert.rejects( + IOUtils.remove(tmpParentDir, { recursive: false }), + /Could not remove the non-empty directory at .*/, + "IOUtils::remove fails if non-recursively removing directory with contents" + ); + + await IOUtils.remove(tmpParentDir, { recursive: true }); + ok( + !await dirExists(tmpParentDir), + "IOUtils::remove can recursively remove a directory" + ); + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_set_permissions.html b/dom/system/tests/ioutils/test_ioutils_set_permissions.html new file mode 100644 index 0000000000..8cf4494c5d --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_set_permissions.html @@ -0,0 +1,56 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + + add_task(async function test_setPermissions() { + const tempDir = await PathUtils.getTempDir(); + const tempFile = PathUtils.join(tempDir, "setPermissions.tmp"); + + await IOUtils.writeUTF8(tempFile, ""); + await IOUtils.setPermissions(tempFile, 0o421); + + let stat = await IOUtils.stat(tempFile); + + if (Services.appinfo.OS === "WINNT") { + // setPermissions ignores the x bit on Windows. + is(stat.permissions, 0o666, "Permissions munged on Windows"); + } else { + is(stat.permissions, 0o421, "Permissions match"); + } + + await IOUtils.setPermissions(tempFile, 0o400); + stat = await IOUtils.stat(tempFile); + + if (Services.appinfo.OS === "WINNT") { + is(stat.permissions, 0o444, "Permissions munged on Windows"); + + // We need to make the file writable to delete it on Windows. + await IOUtils.setPermissions(tempFile, 0o600); + } else { + is(stat.permissions, 0o400, "Permissions match"); + } + + await cleanup(tempFile); + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_stat_touch.html b/dom/system/tests/ioutils/test_ioutils_stat_touch.html new file mode 100644 index 0000000000..c8b052fadb --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_stat_touch.html @@ -0,0 +1,204 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm"); + const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + + add_task(async function test_stat() { + info("Test attempt to stat a regular empty file"); + + const tmpDir = await PathUtils.getTempDir(); + + const emptyFileName = PathUtils.join(tmpDir, "test_stat_empty.tmp"); + await createFile(emptyFileName); + + const emptyFileInfo = await IOUtils.stat(emptyFileName); + is(emptyFileInfo.size, 0, "IOUtils::stat can get correct (empty) file size"); + is(emptyFileInfo.path, emptyFileName, "IOUtils::stat result contains the path"); + is(emptyFileInfo.type, "regular", "IOUtils::stat can stat regular (empty) files"); + Assert.less( + (emptyFileInfo.lastModified - new Date().valueOf()), + 1000, // Allow for 1 second deviation in case of slow tests. + "IOUtils::stat can get the last modification date for a regular file" + ); + + info("Test attempt to stat a regular binary file"); + const tempFileName = PathUtils.join(tmpDir, "test_stat_binary.tmp"); + const bytes = Uint8Array.of(...new Array(50).keys()); + await createFile(tempFileName, bytes); + + const fileInfo = await IOUtils.stat(tempFileName); + is(fileInfo.size, 50, "IOUtils::stat can get correct file size"); + is(fileInfo.path, tempFileName, "IOUtils::stat result contains the path"); + is(fileInfo.type, "regular", "IOUtils::stat can stat regular files"); + Assert.less( + (fileInfo.lastModified - new Date().valueOf()), + 1000, // Allow for 1 second deviation in case of slow tests. + "IOUtils::stat can get the last modification date for a regular file" + ); + + info("Test attempt to stat a directory"); + const tempDirName = PathUtils.join(tmpDir, "test_stat_dir.tmp.d"); + await IOUtils.makeDirectory(tempDirName); + + const dirInfo = await IOUtils.stat(tempDirName); + is(dirInfo.size, -1, "IOUtils::stat reports -1 size for directories") + is(fileInfo.path, tempFileName, "IOUtils::stat result contains the path"); + is(fileInfo.type, "regular", "IOUtils::stat can stat directories"); + Assert.less( + (fileInfo.lastModified - new Date().valueOf()), + 1000, // Allow for 1 second deviation in case of slow tests. + "IOUtils::stat can get the last modification date for a regular file" + ); + + await cleanup(emptyFileName, tempFileName, tempDirName) + }); + + add_task(async function test_stat_failures() { + info("Test attempt to stat a non-existing file"); + + const tmpDir = await PathUtils.getTempDir(); + + const notExistsFile = PathUtils.join(tmpDir, "test_stat_not_exists.tmp"); + + await Assert.rejects( + IOUtils.stat(notExistsFile), + /Could not stat file\(.*\) because it does not exist/, + "IOUtils::stat throws if the target file does not exist" + ); + }); + + add_task(async function test_touch_and_stat() { + info("Test attempt to touch a file"); + + const tmpDir = await PathUtils.getTempDir(); + + const tmpFileName = PathUtils.join(tmpDir, "test_touch_and_stat.tmp"); + await createFile(tmpFileName); + + const oldFileInfo = await IOUtils.stat(tmpFileName); + await sleep(500); + + // Now update the time stamp. + const stamp = await IOUtils.touch(tmpFileName); + const newFileInfo = await IOUtils.stat(tmpFileName); + + ok( + newFileInfo.lastModified > oldFileInfo.lastModified, + "IOUtils::touch can update the lastModified time stamp on the file system" + ); + is( + stamp, + newFileInfo.lastModified, + "IOUtils::touch returns the updated time stamp." + ); + + info("Test attempt to touch a directory"); + const tmpDirName = PathUtils.join(tmpDir, "test_touch_and_stat.tmp.d"); + await createDir(tmpDirName); + + await cleanup(tmpFileName, tmpDirName); + }); + + add_task(async function test_touch_custom_mod_time() { + const tmpDir = await PathUtils.getTempDir(); + + const tempFileName = PathUtils.join(tmpDir, "test_touch_custom_mod_time.tmp"); + await createFile(tempFileName); + const originalInfo = await IOUtils.stat(tempFileName); + const now = originalInfo.lastModified; + + const oneMinute = 60 * 1000; // milliseconds + + info("Test attempt to set modification time to the future"); + const future = now + oneMinute; + let newModTime = await IOUtils.touch(tempFileName, future); + const futureInfo = await IOUtils.stat(tempFileName); + Assert.less(originalInfo.lastModified, futureInfo.lastModified, "IOUtils::touch can set a future modification time for the file"); + + is(newModTime, futureInfo.lastModified, "IOUtils::touch returns the updated time stamp"); + is(newModTime, future, "IOUtils::touch return value matches the argument value exactly"); + + info("Test attempt to set modification time to the past"); + const past = now - 2 * oneMinute; + newModTime = await IOUtils.touch(tempFileName, past); + const pastInfo = await IOUtils.stat(tempFileName); + Assert.greater(originalInfo.lastModified, pastInfo.lastModified, "IOUtils::touch can set a past modification time for the file"); + + is(newModTime, pastInfo.lastModified, "IOUtils::touch returns the updated time stamp"); + is(newModTime, past, "IOUtils::touch return value matches the argument value exactly"); + + await cleanup(tempFileName); + }); + + add_task(async function test_stat_btime() { + if (["Darwin", "WINNT"].includes(Services.appinfo.OS)) { + const tmpDir = await PathUtils.getTempDir(); + + const tempFileName = PathUtils.join(tmpDir, "test_stat_btime.tmp"); + await createFile(tempFileName); + const originalInfo = await IOUtils.stat(tempFileName); + + const future = originalInfo.lastModified + 6000; + await IOUtils.touch(tempFileName, future); + const futureInfo = await IOUtils.stat(tempFileName); + + ok(originalInfo.hasOwnProperty("creationTime"), "originalInfo has creationTime field"); + ok(originalInfo.creationTime !== undefined && originalInfo.creationTime !== null, "originalInfo has non-null creationTime"); + + ok(futureInfo.hasOwnProperty("creationTime"), "futureInfo has creationTime field"); + ok(futureInfo.creationTime !== undefined && futureInfo.creationTime !== null, "futureInfo has non-null creationTime"); + + is(originalInfo.creationTime, futureInfo.creationTime, "creationTime matches"); + + await cleanup(tempFileName); + } else { + ok(true, `skipping test_stat_btime() on unsupported platform ${Services.appinfo.OS}`); + } + }); + + add_task(async function test_touch_failures() { + info("Test attempt to touch a non-existing file"); + const tmpDir = await PathUtils.getTempDir(); + const notExistsFile = PathUtils.join(tmpDir, "test_touch_not_exists.tmp"); + + await Assert.rejects( + IOUtils.touch(notExistsFile), + /Could not touch file\(.*\) because it does not exist/, + "IOUtils::touch throws if the target file does not exist" + ); + + info("Test attempt to set modification time to Epoch"); + const tempFileName = PathUtils.join(tmpDir, "test_touch_epoch.tmp"); + await createFile(tempFileName); + + await Assert.rejects( + IOUtils.touch(tempFileName, 0), + /Refusing to set the modification time of file\(.*\) to 0/, + "IOUtils::touch cannot set the file modification time to Epoch" + ); + + await cleanup(tempFileName); + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_worker.xhtml b/dom/system/tests/ioutils/test_ioutils_worker.xhtml new file mode 100644 index 0000000000..df67d48676 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_worker.xhtml @@ -0,0 +1,40 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Testing IOUtils on a chrome worker thread" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onload="test();"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/WorkerHandler.js"/> + + <script type="application/javascript"> + <![CDATA[ + + // Test IOUtils in a chrome worker. + function test() { + // finish() will be called in the worker. + SimpleTest.waitForExplicitFinish(); + info("test_ioutils_worker.xhtml: Starting test"); + + const worker = new ChromeWorker("file_ioutils_worker.js"); + info("test_ioutils_worker.xhtml: Chrome worker created"); + + // Set up the worker with testing facilities, and start it. + listenForTests(worker, { verbose: false }); + worker.postMessage(0); + info("test_ioutils_worker.xhtml: 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/dom/system/tests/location_service.sjs b/dom/system/tests/location_service.sjs new file mode 100644 index 0000000000..04779b7d76 --- /dev/null +++ b/dom/system/tests/location_service.sjs @@ -0,0 +1,39 @@ +function parseQueryString(str) { + if (str == "") { + return {}; + } + + var paramArray = str.split("&"); + var regex = /^([^=]+)=(.*)$/; + var params = {}; + for (var i = 0, sz = paramArray.length; i < sz; i++) { + var match = regex.exec(paramArray[i]); + if (!match) { + throw new Error("Bad parameter in queryString! '" + paramArray[i] + "'"); + } + params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]); + } + + return params; +} + +function getPosition(params) { + var response = { + status: "OK", + accuracy: 100, + location: { + lat: params.lat, + lng: params.lng, + }, + }; + + return JSON.stringify(response); +} + +function handleRequest(request, response) { + let params = parseQueryString(request.queryString); + response.setStatusLine("1.0", 200, "OK"); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "application/x-javascript", false); + response.write(getPosition(params)); +} diff --git a/dom/system/tests/location_services_parent.js b/dom/system/tests/location_services_parent.js new file mode 100644 index 0000000000..3112e60117 --- /dev/null +++ b/dom/system/tests/location_services_parent.js @@ -0,0 +1,20 @@ +/** + * Loaded as a frame script fetch telemetry for + * test_location_services_telemetry.html + */ + +/* global addMessageListener, sendAsyncMessage */ + +"use strict"; + +const HISTOGRAM_KEY = "REGION_LOCATION_SERVICES_DIFFERENCE"; + +addMessageListener("getTelemetryEvents", options => { + let result = Services.telemetry.getHistogramById(HISTOGRAM_KEY).snapshot(); + sendAsyncMessage("getTelemetryEvents", result); +}); + +addMessageListener("clear", options => { + Services.telemetry.getHistogramById(HISTOGRAM_KEY).clear(); + sendAsyncMessage("clear", true); +}); diff --git a/dom/system/tests/mochitest.ini b/dom/system/tests/mochitest.ini new file mode 100644 index 0000000000..724dbf73bb --- /dev/null +++ b/dom/system/tests/mochitest.ini @@ -0,0 +1,11 @@ +[DEFAULT] +scheme = https + +support-files = + file_bug1197901.html + location_services_parent.js + location_service.sjs + +[test_bug1197901.html] +[test_location_services_telemetry.html] +skip-if = os == "android" diff --git a/dom/system/tests/test_bug1197901.html b/dom/system/tests/test_bug1197901.html new file mode 100644 index 0000000000..7e1866ffa3 --- /dev/null +++ b/dom/system/tests/test_bug1197901.html @@ -0,0 +1,96 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1197901 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1197901</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1197901 **/ + SimpleTest.requestFlakyTimeout("requestFlakyTimeout is silly"); + SimpleTest.waitForExplicitFinish(); + window.onload = function() { + SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({"set": [["device.sensors.test.events", true]]}, + doTest); + }, window); + }; + + function doTest() { + window.onmessage = function(event) { + ok(event.data.result, event.data.message); + }; + + // Only same-origin iframe should get the events. + var xo = document.getElementById("cross-origin"); + xo.contentWindow.postMessage( + { command: "addEventListener", + expected: false, + message: "Cross-origin iframe shouldn't get the sensor events."}, + "*"); + + var so = document.getElementById("same-origin"); + so.contentWindow.postMessage( + { command: "addEventListener", + expected: true, + message: "Same-origin iframe should get the sensor events." }, + "*"); + + // We need a timeout here to check that something does not happen. + setTimeout(function() { + so.remove(); + xo.remove(); + doWindowTest(); + }, 500); + } + + function doWindowTest() { + var win = window.open("file_bug1197901.html", "w1", "height=100,width=100"); + win.onload = function() { + win.focus(); + SimpleTest.waitForFocus(function() { + var win2 = window.open("file_bug1197901.html", "w2", "height=100,width=100,left=100"); + win2.onload = function() { + win2.focus(); + SimpleTest.waitForFocus(function() { + // Only focused window should get the events. + win.postMessage( + { command: "addEventListener", + expected: false, + message: "Only focused window should get the sensor events." }, + "*"); + win2.postMessage( + { command: "addEventListener", + expected: true, + message: "Focused window should get the sensor events." }, + "*"); + setTimeout(function() { + window.onmessage = null; + win.close(); + win2.close(); + SimpleTest.finish(); + }, 500); + }, win2); + }; + }, win); + }; + } + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<iframe src="file_bug1197901.html" id="same-origin"></iframe> +<iframe src="http://example.com/tests/dom/system/tests/file_bug1197901.html" id="cross-origin"></iframe> +</body> +</html> diff --git a/dom/system/tests/test_constants.xhtml b/dom/system/tests/test_constants.xhtml new file mode 100644 index 0000000000..e5afc67f1d --- /dev/null +++ b/dom/system/tests/test_constants.xhtml @@ -0,0 +1,139 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Testing constants on a chrome worker thread" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript"> + <![CDATA[ + +let worker; + +function test_xul() { + let lib; + isnot(null, OS.Constants.Path.libxul, "libxulpath is defined"); + try { + lib = ctypes.open(OS.Constants.Path.libxul); + lib.declare("DumpJSStack", ctypes.default_abi, ctypes.void_t); + } catch (x) { + success = false; + ok(false, "Could not open libxul " + x); + } + if (lib) { + lib.close(); + } + ok(true, "test_xul: opened libxul successfully"); +} + +// Test that OS.Constants.libc is defined +function test_libc() { + isnot(null, OS.Constants.libc, "OS.Constants.libc is defined"); + is(0001, OS.Constants.libc.S_IXOTH, "OS.Constants.libc.S_IXOTH is defined"); + is(0002, OS.Constants.libc.S_IWOTH, "OS.Constants.libc.S_IWOTH is defined"); + is(0007, OS.Constants.libc.S_IRWXO, "OS.Constants.libc.S_IRWXO is defined"); + is(0010, OS.Constants.libc.S_IXGRP, "OS.Constants.libc.S_IXGRP is defined"); + is(0020, OS.Constants.libc.S_IWGRP, "OS.Constants.libc.S_IWGRP is defined"); + is(0040, OS.Constants.libc.S_IRGRP, "OS.Constants.libc.S_IRGRP is defined"); + is(0070, OS.Constants.libc.S_IRWXG, "OS.Constants.libc.S_IRWXG is defined"); + is(0100, OS.Constants.libc.S_IXUSR, "OS.Constants.libc.S_IXUSR is defined"); + is(0200, OS.Constants.libc.S_IWUSR, "OS.Constants.libc.S_IWUSR is defined"); + is(0400, OS.Constants.libc.S_IRUSR, "OS.Constants.libc.S_IRUSR is defined"); + is(0700, OS.Constants.libc.S_IRWXU, "OS.Constants.libc.S_IRWXU is defined"); +} + +// Test that OS.Constants.Win is defined +function test_Win() { + var xulRuntime = Cc["@mozilla.org/xre/app-info;1"] + .getService(Ci.nsIXULRuntime); + if(xulRuntime.OS == "Windows") { + ok("Win" in OS.Constants, "OS.Constants.Win is defined"); + is(OS.Constants.Win.INVALID_HANDLE_VALUE, -1, + "OS.Constants.Win.INVALID_HANDLE_VALUE is defined and correct"); + } +} + +// Test that OS.Constants.Sys.DEBUG is set properly on main thread +function test_debugBuildMainThread(isDebugBuild) { + is(isDebugBuild, !!OS.Constants.Sys.DEBUG, "OS.Constants.Sys.DEBUG is set properly on main thread"); +} + +// Test that OS.Constants.Sys.umask is set properly on main thread +function test_umaskMainThread(umask) { + is(umask, OS.Constants.Sys.umask, + "OS.Constants.Sys.umask is set properly on main thread: " + + ("0000"+umask.toString(8)).slice(-4)); +} + +var ctypes; +function test() { + ok(true, "test_constants.xhtml: Starting test"); + + // Test 1: Load libxul from main thread + Cc["@mozilla.org/net/osfileconstantsservice;1"]. + getService(Ci.nsIOSFileConstantsService). + init(); + ({ctypes} = ChromeUtils.import("resource://gre/modules/ctypes.jsm")); + test_xul(); + test_libc(); + test_Win(); + + let isDebugBuild = Cc["@mozilla.org/xpcom/debug;1"] + .getService(Ci.nsIDebug2).isDebugBuild; + test_debugBuildMainThread(isDebugBuild); + + let umask = Cc["@mozilla.org/system-info;1"]. + getService(Ci.nsIPropertyBag2). + getProperty("umask"); + test_umaskMainThread(umask); + + // Test 2: Load libxul from chrome thread + worker = new ChromeWorker("worker_constants.js"); + SimpleTest.waitForExplicitFinish(); + ok(true, "test_constants.xhtml: Chrome worker created"); + worker.onerror = function onerror(error) { + error.preventDefault(); + ok(false, "error " + error); + } + worker.onmessage = function onmessage(msg) { + switch (msg.data.kind) { + case "is": + SimpleTest.is(msg.data.a, msg.data.b, msg.data.description); + return; + case "isnot": + SimpleTest.isnot(msg.data.a, msg.data.b, msg.data.description); + return; + case "ok": + SimpleTest.ok(msg.data.condition, msg.data.description); + return; + case "finish": + SimpleTest.finish(); + return; + default: + SimpleTest.ok(false, "test_constants.xhtml: wrong message " + JSON.stringify(msg.data)); + return; + } + }; + + // pass expected values that are unavailable off-main-thread + // to the worker + worker.postMessage({ + isDebugBuild: isDebugBuild, + umask: umask + }); + ok(true, "test_constants.xhtml: 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/dom/system/tests/test_location_services_telemetry.html b/dom/system/tests/test_location_services_telemetry.html new file mode 100644 index 0000000000..dff1efe0f2 --- /dev/null +++ b/dom/system/tests/test_location_services_telemetry.html @@ -0,0 +1,143 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1637402 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1637402</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + SimpleTest.requestLongerTimeout(2); + + const BASE_GEO_URL = "http://mochi.test:8888/tests/dom/system/tests/location_service.sjs"; + + const GEO_PREF = "geo.provider.network.url"; + const BACKUP_PREF = "geo.provider.network.compare.url"; + + const PARENT = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("location_services_parent.js")); + + function sendToParent(msg, options) { + return new Promise(resolve => { + PARENT.addMessageListener(msg, events => { + PARENT.removeMessageListener(msg); + resolve(events); + }); + PARENT.sendAsyncMessage(msg, options); + }); + } + + function getCurrentPosition() { + return new Promise(function(resolve, reject) { + navigator.geolocation.getCurrentPosition(resolve, reject); + }); + } + + let tries = 0; + let MAX_RETRIES = 500; + async function waitFor(fun) { + let passing = false; + while (!passing && ++tries < MAX_RETRIES) { + passing = await fun(); + } + tries = 0; + if (!passing) { + ok(false, "waitFor condition never passed"); + } + } + + // Keeps track of how many telemetry results we have + // seen so we can wait for new ones. + let telemetryResultCount = 0; + async function newTelemetryResult() { + let results = await sendToParent("getTelemetryEvents"); + let total = Object.values(results.values) + .reduce((val, acc) => acc + val, 0); + if (total <= telemetryResultCount) { + return false; + } + telemetryResultCount++; + return true; + } + + SimpleTest.waitForExplicitFinish(); + window.onload = () => { + SimpleTest.waitForFocus(() => { + SpecialPowers.pushPrefEnv({"set": + [ + ["geo.prompt.testing", true], + ["geo.prompt.testing.allow", true], + ["geo.provider.network.logging.enabled", true], + ["geo.provider.network.debug.requestCache.enabled", false] + ], + }, doTest); + }, window); + }; + + const BASE_LOCATION = {lat: 55.867055, lng: -4.271041}; + const LOCATIONS = [ + {lat: "foo", lng: "bar", skipWait: true}, // Nan + {lat: 55.867055, lng: -4.271041}, // 0M + {lat: 50.8251639, lng: -0.1622551}, // 623KM + {lat: 55.9438948, lng: -3.1845417}, // 68KM + {lat: 39.4780911, lng: -0.3821706}, // 1844KM + {lat: 55.867160, lng: -4.271041}, // 10M + {lat: 41.8769913, lng: 12.4835351}, // 1969KM + {lat: 55.867055, lng: -4.271041}, // 0M + ] + + async function setLocations(main, backup) { + await SpecialPowers.setCharPref( + GEO_PREF, + `${BASE_GEO_URL}?lat=${main.lat}&lng=${main.lng}` + ); + await SpecialPowers.setCharPref( + BACKUP_PREF, + `${BASE_GEO_URL}?lat=${backup.lat}&lng=${backup.lng}` + ); + } + + async function doTest() { + // Not all treeherder builds can collect telemetry. + if (!SpecialPowers.Services.telemetry.canRecordPrereleaseData) { + ok(true, "Cant run any tests without telemetry"); + SimpleTest.finish(); + return; + } + await sendToParent("clear"); + + for (let location of LOCATIONS) { + await setLocations(BASE_LOCATION, location); + await getCurrentPosition(); + // Not all requests (NaN) will report telemetry. + if (!location.skipWait) { + await waitFor(newTelemetryResult, ""); + } + } + + let res = await sendToParent("getTelemetryEvents"); + let total = Object.values(res.values) + .reduce((val, acc) => acc + val, 0); + + is(total, 7, "Should have correct number of results"); + is(res.values["0"], 2, "Two results were same location"); + // Telemetry could change how exact bucketing + // implementation, so check the low bucket + // and that the rest are spead out. + is( + Object.keys(res.values).length, + 6, + "Split the rest of the results across buckets" + ); + + SimpleTest.finish(); + } +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1637402">Mozilla Bug </a> +<pre id="test"></pre> +</body> +</html> diff --git a/dom/system/tests/test_pathutils.html b/dom/system/tests/test_pathutils.html new file mode 100644 index 0000000000..555942e13d --- /dev/null +++ b/dom/system/tests/test_pathutils.html @@ -0,0 +1,446 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> +<html> + +<head> + <meta charset="utf-8"> + <title>PathUtils tests</title> +</head> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +<script> + "use strict"; + + const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + + const UNRECOGNIZED_PATH = /Could not initialize path: NS_ERROR_FILE_UNRECOGNIZED_PATH/; + const EMPTY_PATH = /PathUtils does not support empty paths/; + const JOIN = /Could not append to path/; + + add_task(function test_filename() { + Assert.throws( + () => PathUtils.filename(""), + EMPTY_PATH, + "PathUtils.filename() does not support empty paths" + ); + Assert.throws( + () => PathUtils.filename("foo.txt"), + UNRECOGNIZED_PATH, + "PathUtils.filename() does not support relative paths" + ); + + if (Services.appinfo.OS === "WINNT") { + is( + PathUtils.filename("C:"), + "C:", + "PathUtils.filename() with a drive path" + ); + is( + PathUtils.filename("C:\\"), + "C:", + "PathUtils.filename() with a drive path" + ); + is( + PathUtils.filename("C:\\Windows"), + "Windows", + "PathUtils.filename() with a path with 2 components" + ); + is( + PathUtils.filename("C:\\Windows\\"), + "Windows", + "PathUtils.filename() with a path with 2 components and a trailing slash" + ); + is( + PathUtils.filename("C:\\Windows\\System32"), + "System32", + "PathUtils.filename() with a path with 3 components" + ); + is( + PathUtils.filename("\\\\server"), + "\\\\server", + "PathUtils.filename() with a UNC server path" + ); + is( + PathUtils.filename("C:\\file.dat"), + "file.dat", + "PathUtils.filename() with a file path" + ); + } else { + is( + PathUtils.filename("/"), + "/", + "PathUtils.filename() with a root path" + ); + is( + PathUtils.filename("/usr/"), + "usr", + "PathUtils.filename() with a non-root path" + ); + is( + PathUtils.filename("/usr/lib/libfoo.so"), + "libfoo.so", + "PathUtils.filename() with a path with 3 components" + ); + } + }); + + add_task(function test_parent() { + Assert.throws( + () => PathUtils.parent("."), + UNRECOGNIZED_PATH, + "PathUtils.parent() does not support relative paths" + ); + Assert.throws( + () => PathUtils.parent(""), + EMPTY_PATH, + "PathUtils.parent() does not support empty paths" + ); + + if (Services.appinfo.OS === "WINNT") { + is( + PathUtils.parent("C:"), + null, + "PathUtils.parent() with a drive path" + ); + is( + PathUtils.parent("\\\\server"), + null, + "PathUtils.parent() with a UNC server path" + ); + is( + PathUtils.parent("\\\\server\\foo"), + "\\\\server", + "PathUtils.parent() with a UNC server path and child component" + ); + } else { + is( + PathUtils.parent("/"), + null, + "PathUtils.parent() with a root path" + ); + is( + PathUtils.parent("/var"), + "/", + "PathUtils.parent() with a 2 component path" + ); + is( + PathUtils.parent("/var/run"), + "/var", + "PathUtils.parent() with a 3 component path" + ); + } + }); + + add_task(function test_join() { + is( + PathUtils.join(), + "", + "PathUtils.join() with an empty sequence" + ); + Assert.throws( + () => PathUtils.join(""), + EMPTY_PATH, + "PathUtils.join() does not support empty paths" + ); + Assert.throws( + () => PathUtils.join("foo", "bar"), + UNRECOGNIZED_PATH, + "PathUtils.join() does not support relative paths" + ); + Assert.throws( + () => PathUtils.join("."), + UNRECOGNIZED_PATH, + "PathUtils.join() does not support relative paths" + ); + + if (Services.appinfo.OS === "WINNT") { + is( + PathUtils.join("C:"), + "C:", + "PathUtils.join() with a single path" + ); + is( + PathUtils.join("C:\\Windows", "System32"), + "C:\\Windows\\System32", + "PathUtils.join() with a 2 component path and an additional component" + ); + is( + PathUtils.join("C:", "Users", "Example"), + "C:\\Users\\Example", + "PathUtils.join() with a root path and two additional components" + ); + is( + PathUtils.join("\\\\server", "Files", "Example.dat"), + "\\\\server\\Files\\Example.dat", + "PathUtils.join() with a server path" + ); + } else { + is( + PathUtils.join("/"), + "/", + "PathUtils.join() with a root path" + ); + is( + PathUtils.join("/usr", "lib"), + "/usr/lib", + "PathUtils.join() with a 2 component path and an additional component" + ); + is( + PathUtils.join("/", "home", "example"), + "/home/example", + "PathUtils.join() with a root path and two additional components" + ); + } + }); + + add_task(function test_join_relative() { + if (Services.appinfo.OS === "WINNT") { + is( + PathUtils.joinRelative("C:", ""), + "C:", + "PathUtils.joinRelative() with an empty relative path" + ); + + is( + PathUtils.joinRelative("C:", "foo\\bar\\baz"), + "C:\\foo\\bar\\baz", + "PathUtils.joinRelative() with a relative path containing path separators" + ); + } else { + is( + PathUtils.joinRelative("/", ""), + "/", + "PathUtils.joinRelative() with an empty relative path" + ); + + is( + PathUtils.joinRelative("/", "foo/bar/baz"), + "/foo/bar/baz", + "PathUtils.joinRelative() with a relative path containing path separators" + ); + } + }); + + add_task(async function test_normalize() { + Assert.throws( + () => PathUtils.normalize(""), + EMPTY_PATH, + "PathUtils.normalize() does not support empty paths" + ); + Assert.throws( + () => PathUtils.normalize("."), + UNRECOGNIZED_PATH, + "PathUtils.normalize() does not support relative paths" + ); + + if (Services.appinfo.OS === "WINNT") { + is( + PathUtils.normalize("C:\\\\Windows\\\\..\\\\\\.\\Users\\..\\Windows"), + "C:\\Windows", + "PathUtils.normalize() with a non-normalized path" + ); + } else { + // nsLocalFileUnix::Normalize() calls realpath, which resolves symlinks + // and requires the file to exist. + // + // On Darwin, the temp directory is located in `/private/var`, which is a + // symlink to `/var`, so we need to pre-normalize our temporary directory + // or expected paths won't match. + const tmpDir = PathUtils.join( + PathUtils.normalize(await PathUtils.getTempDir()), + "pathutils_test" + ); + + await IOUtils.makeDirectory(tmpDir, { ignoreExisting: true }); + info(`created tmpDir ${tmpDir}`); + SimpleTest.registerCleanupFunction(async () => { + await IOUtils.remove(tmpDir, { + recursive: true, + }); + }); + + await IOUtils.makeDirectory(PathUtils.join(tmpDir, "foo", "bar"), { + createAncestors: true, + }); + + is( + PathUtils.normalize("/"), + "/", + "PathUtils.normalize() with a normalized path" + ); + + is( + PathUtils.normalize( + PathUtils.join( + tmpDir, + "foo", + ".", + "..", + "foo", + ".", + "bar", + "..", + "bar" + ) + ), + PathUtils.join(tmpDir, "foo", "bar"), + "PathUtils.normalize() with a non-normalized path" + ); + } + }); + + add_task(function test_split() { + Assert.throws( + () => PathUtils.split("foo"), + UNRECOGNIZED_PATH, + "PathUtils.split() does not support relative paths" + ); + Assert.throws( + () => PathUtils.split(""), + EMPTY_PATH, + "PathUtils.split() does not support empty paths" + ); + + if (Services.appinfo.OS === "WINNT") { + Assert.deepEqual( + PathUtils.split("C:\\Users\\Example"), + ["C:", "Users", "Example"], + "PathUtils.split() on an absolute path" + ); + + Assert.deepEqual( + PathUtils.split("C:\\Users\\Example\\"), + ["C:", "Users", "Example"], + "PathUtils.split() on an absolute path with a trailing slash" + ); + + Assert.deepEqual( + PathUtils.split("\\\\server\\Files\\Example.dat"), + ["\\\\server", "Files", "Example.dat"], + "PathUtils.split() with a server as the root" + ); + } else { + Assert.deepEqual( + PathUtils.split("/home/foo"), + ["/", "home", "foo"], + "PathUtils.split() on absolute path" + ); + + Assert.deepEqual( + PathUtils.split("/home/foo/"), + ["/", "home", "foo"], + "PathUtils.split() on absolute path with trailing slash" + ); + } + }); + + add_task(function test_toFileURI() { + Assert.throws( + () => PathUtils.toFileURI("."), + UNRECOGNIZED_PATH, + "PathUtils.toFileURI() does not support relative paths" + ); + Assert.throws( + () => PathUtils.toFileURI(""), + EMPTY_PATH, + "PathUtils.toFileURI() does not support empty paths" + ); + + if (Services.appinfo.OS === "WINNT") { + is( + PathUtils.toFileURI("C:\\"), + "file:///C:/", + "PathUtils.toFileURI() with a root path" + ); + + is( + PathUtils.toFileURI("C:\\Windows\\"), + "file:///C:/Windows/", + "PathUtils.toFileURI() with a non-root directory path" + ); + + is( + PathUtils.toFileURI("C:\\Windows\\system32\\notepad.exe"), + "file:///C:/Windows/system32/notepad.exe", + "PathUtils.toFileURI() with a file path" + ); + } else { + is( + PathUtils.toFileURI("/"), + "file:///", + "PathUtils.toFileURI() with a root path" + ); + + is( + PathUtils.toFileURI("/bin"), + "file:///bin/", + "PathUtils.toFileURI() with a non-root directory path" + ); + + is( + PathUtils.toFileURI("/bin/ls"), + "file:///bin/ls", + "PathUtils.toFileURI() with a file path" + ); + } + }); + + add_task(async function test_getDirectories() { + const profile = await PathUtils.getProfileDir(); + is( + profile, + Services.dirsvc.get("ProfD", Ci.nsIFile).path, + "PathUtils.getProfileDir() should match dirsvc" + ); + + const localProfile = await PathUtils.getLocalProfileDir(); + is( + localProfile, + Services.dirsvc.get("ProfLD", Ci.nsIFile).path, + "PathUtils.getLocalProfileDir() should match dirsvc" + ); + + // See: nsAppDirectoryServiceDefs.h + const tempDir = await PathUtils.getTempDir(); + if (AppConstants.MOZ_SANDBOX) { + is( + tempDir, + Services.dirsvc.get("ContentTmpD", Ci.nsIFile).path, + "PathUtils.getTempDir() should match dirsvc" + ); + } else { + is( + tempDir, + Services.dirsvc.get("TmpD", Ci.nsIFile).path, + "PathUtils.getTempDir() should match dirsvc" + ); + } + }); + + add_task(async function test_createUniquePath() { + let path = PathUtils.join(await PathUtils.getProfileDir(), ".test"); + + let firstPath = PathUtils.createUniquePath(path); + let secondPath = PathUtils.createUniquePath(path); + SimpleTest.registerCleanupFunction(async () => { + await IOUtils.remove(firstPath); + await IOUtils.remove(secondPath); + }); + + isnot(firstPath, secondPath, "Create unique paths returns different paths"); + is(PathUtils.filename(firstPath), ".test", "PathUtils.createUniquePath() matches filename for first path"); + is(PathUtils.filename(secondPath), "-1.test", "PathUtils.createUniquePath() has unique filename for second path"); + }); +</script> + +<body> +</body> + +</html> diff --git a/dom/system/tests/worker_constants.js b/dom/system/tests/worker_constants.js new file mode 100644 index 0000000000..4894667dfd --- /dev/null +++ b/dom/system/tests/worker_constants.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-env mozilla/chrome-worker */ + +function log(text) { + dump("WORKER " + text + "\n"); +} + +function send(message) { + self.postMessage(message); +} + +self.onmessage = function(msg) { + self.onmessage = function(msgInner) { + log("ignored message " + JSON.stringify(msgInner.data)); + }; + let { isDebugBuild, umask } = msg.data; + try { + test_name(); + test_xul(); + test_debugBuildWorkerThread(isDebugBuild); + test_umaskWorkerThread(umask); + test_bits(); + } catch (x) { + log("Catching error: " + x); + log("Stack: " + x.stack); + log("Source: " + x.toSource()); + ok(false, x.toString() + "\n" + x.stack); + } + finish(); +}; + +function finish() { + send({ kind: "finish" }); +} + +function ok(condition, description) { + send({ kind: "ok", condition, description }); +} +function is(a, b, description) { + send({ kind: "is", a, b, description }); +} +function isnot(a, b, description) { + send({ kind: "isnot", a, b, description }); +} + +// Test that OS.Constants.Sys.Name is defined +function test_name() { + isnot(null, OS.Constants.Sys.Name, "OS.Constants.Sys.Name is defined"); +} + +// Test that OS.Constants.Sys.DEBUG is set properly in ChromeWorker thread +function test_debugBuildWorkerThread(isDebugBuild) { + is( + isDebugBuild, + !!OS.Constants.Sys.DEBUG, + "OS.Constants.Sys.DEBUG is set properly on worker thread" + ); +} + +// Test that OS.Constants.Sys.umask is set properly in ChromeWorker thread +function test_umaskWorkerThread(umask) { + is( + umask, + OS.Constants.Sys.umask, + "OS.Constants.Sys.umask is set properly on worker thread: " + + ("0000" + umask.toString(8)).slice(-4) + ); +} + +// Test that OS.Constants.Path.libxul lets us open libxul +function test_xul() { + let lib; + isnot(null, OS.Constants.Path.libxul, "libxul is defined"); + try { + lib = ctypes.open(OS.Constants.Path.libxul); + lib.declare("DumpJSStack", ctypes.default_abi, ctypes.void_t); + } catch (x) { + ok(false, "test_xul: Could not open libxul: " + x); + } + if (lib) { + lib.close(); + } + ok(true, "test_xul: opened libxul successfully"); +} + +// Check if the value of OS.Constants.Sys.bits is 32 or 64 +function test_bits() { + is( + OS.Constants.Sys.bits, + ctypes.int.ptr.size * 8, + "OS.Constants.Sys.bits is either 32 or 64" + ); +} diff --git a/dom/system/windows/WindowsLocationProvider.cpp b/dom/system/windows/WindowsLocationProvider.cpp new file mode 100644 index 0000000000..51018156b2 --- /dev/null +++ b/dom/system/windows/WindowsLocationProvider.cpp @@ -0,0 +1,272 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "WindowsLocationProvider.h" +#include "GeolocationPosition.h" +#include "nsComponentManagerUtils.h" +#include "prtime.h" +#include "MLSFallback.h" +#include "mozilla/Attributes.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/Telemetry.h" +#include "mozilla/dom/GeolocationPositionErrorBinding.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_ISUPPORTS(WindowsLocationProvider::MLSUpdate, nsIGeolocationUpdate); + +WindowsLocationProvider::MLSUpdate::MLSUpdate(nsIGeolocationUpdate* aCallback) + : mCallback(aCallback) {} + +NS_IMETHODIMP +WindowsLocationProvider::MLSUpdate::Update(nsIDOMGeoPosition* aPosition) { + if (!mCallback) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIDOMGeoPositionCoords> coords; + aPosition->GetCoords(getter_AddRefs(coords)); + if (!coords) { + return NS_ERROR_FAILURE; + } + Telemetry::Accumulate(Telemetry::GEOLOCATION_WIN8_SOURCE_IS_MLS, true); + return mCallback->Update(aPosition); +} +NS_IMETHODIMP +WindowsLocationProvider::MLSUpdate::NotifyError(uint16_t aError) { + if (!mCallback) { + return NS_ERROR_FAILURE; + } + nsCOMPtr<nsIGeolocationUpdate> callback(mCallback); + return callback->NotifyError(aError); +} + +class LocationEvent final : public ILocationEvents { + public: + LocationEvent(nsIGeolocationUpdate* aCallback, + WindowsLocationProvider* aProvider) + : mCallback(aCallback), mProvider(aProvider), mCount(0) {} + + // IUnknown interface + STDMETHODIMP_(ULONG) AddRef() override; + STDMETHODIMP_(ULONG) Release() override; + STDMETHODIMP QueryInterface(REFIID iid, void** ppv) override; + + // ILocationEvents interface + MOZ_CAN_RUN_SCRIPT_BOUNDARY + STDMETHODIMP OnStatusChanged(REFIID aReportType, + LOCATION_REPORT_STATUS aStatus) override; + STDMETHODIMP OnLocationChanged(REFIID aReportType, + ILocationReport* aReport) override; + + private: + nsCOMPtr<nsIGeolocationUpdate> mCallback; + RefPtr<WindowsLocationProvider> mProvider; + ULONG mCount; +}; + +STDMETHODIMP_(ULONG) +LocationEvent::AddRef() { return InterlockedIncrement(&mCount); } + +STDMETHODIMP_(ULONG) +LocationEvent::Release() { + ULONG count = InterlockedDecrement(&mCount); + if (!count) { + delete this; + return 0; + } + return count; +} + +STDMETHODIMP +LocationEvent::QueryInterface(REFIID iid, void** ppv) { + if (iid == IID_IUnknown) { + *ppv = static_cast<IUnknown*>(this); + } else if (iid == IID_ILocationEvents) { + *ppv = static_cast<ILocationEvents*>(this); + } else { + return E_NOINTERFACE; + } + AddRef(); + return S_OK; +} + +STDMETHODIMP +LocationEvent::OnStatusChanged(REFIID aReportType, + LOCATION_REPORT_STATUS aStatus) { + if (aReportType != IID_ILatLongReport) { + return S_OK; + } + + // When registering event, REPORT_INITIALIZING is fired at first. + // Then, when the location is found, REPORT_RUNNING is fired. + if (aStatus == REPORT_RUNNING) { + // location is found by Windows Location provider, we use it. + mProvider->CancelMLSProvider(); + return S_OK; + } + + // Cannot get current location at this time. We use MLS instead until + // Location API returns RUNNING status. + if (NS_SUCCEEDED(mProvider->CreateAndWatchMLSProvider(mCallback))) { + return S_OK; + } + + // Cannot watch location by MLS provider. We must return error by + // Location API. + uint16_t err; + switch (aStatus) { + case REPORT_ACCESS_DENIED: + err = GeolocationPositionError_Binding::PERMISSION_DENIED; + break; + case REPORT_NOT_SUPPORTED: + case REPORT_ERROR: + err = GeolocationPositionError_Binding::POSITION_UNAVAILABLE; + break; + default: + return S_OK; + } + nsCOMPtr<nsIGeolocationUpdate> callback(mCallback); + callback->NotifyError(err); + return S_OK; +} + +STDMETHODIMP +LocationEvent::OnLocationChanged(REFIID aReportType, ILocationReport* aReport) { + if (aReportType != IID_ILatLongReport) { + return S_OK; + } + + RefPtr<ILatLongReport> latLongReport; + if (FAILED(aReport->QueryInterface(IID_ILatLongReport, + getter_AddRefs(latLongReport)))) { + return E_FAIL; + } + + DOUBLE latitude = 0.0; + latLongReport->GetLatitude(&latitude); + + DOUBLE longitude = 0.0; + latLongReport->GetLongitude(&longitude); + + DOUBLE alt = UnspecifiedNaN<double>(); + latLongReport->GetAltitude(&alt); + + DOUBLE herror = 0.0; + latLongReport->GetErrorRadius(&herror); + + DOUBLE verror = UnspecifiedNaN<double>(); + latLongReport->GetAltitudeError(&verror); + + double heading = UnspecifiedNaN<double>(); + double speed = UnspecifiedNaN<double>(); + + // nsGeoPositionCoords will convert NaNs to null for optional properties of + // the JavaScript Coordinates object. + RefPtr<nsGeoPosition> position = + new nsGeoPosition(latitude, longitude, alt, herror, verror, heading, + speed, PR_Now() / PR_USEC_PER_MSEC); + mCallback->Update(position); + + Telemetry::Accumulate(Telemetry::GEOLOCATION_WIN8_SOURCE_IS_MLS, false); + + return S_OK; +} + +NS_IMPL_ISUPPORTS(WindowsLocationProvider, nsIGeolocationProvider) + +WindowsLocationProvider::WindowsLocationProvider() {} + +WindowsLocationProvider::~WindowsLocationProvider() {} + +NS_IMETHODIMP +WindowsLocationProvider::Startup() { + RefPtr<ILocation> location; + if (FAILED(::CoCreateInstance(CLSID_Location, nullptr, CLSCTX_INPROC_SERVER, + IID_ILocation, getter_AddRefs(location)))) { + // We will use MLS provider + return NS_OK; + } + + IID reportTypes[] = {IID_ILatLongReport}; + if (FAILED(location->RequestPermissions(nullptr, reportTypes, 1, FALSE))) { + // We will use MLS provider + return NS_OK; + } + + mLocation = location; + return NS_OK; +} + +NS_IMETHODIMP +WindowsLocationProvider::Watch(nsIGeolocationUpdate* aCallback) { + if (mLocation) { + RefPtr<LocationEvent> event = new LocationEvent(aCallback, this); + if (SUCCEEDED(mLocation->RegisterForReport(event, IID_ILatLongReport, 0))) { + return NS_OK; + } + } + + // Cannot use Location API. We will use MLS instead. + mLocation = nullptr; + + return CreateAndWatchMLSProvider(aCallback); +} + +NS_IMETHODIMP +WindowsLocationProvider::Shutdown() { + if (mLocation) { + mLocation->UnregisterForReport(IID_ILatLongReport); + mLocation = nullptr; + } + + CancelMLSProvider(); + + return NS_OK; +} + +NS_IMETHODIMP +WindowsLocationProvider::SetHighAccuracy(bool enable) { + if (!mLocation) { + // MLS provider doesn't support HighAccuracy + return NS_OK; + } + + LOCATION_DESIRED_ACCURACY desiredAccuracy; + if (enable) { + desiredAccuracy = LOCATION_DESIRED_ACCURACY_HIGH; + } else { + desiredAccuracy = LOCATION_DESIRED_ACCURACY_DEFAULT; + } + if (FAILED( + mLocation->SetDesiredAccuracy(IID_ILatLongReport, desiredAccuracy))) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +nsresult WindowsLocationProvider::CreateAndWatchMLSProvider( + nsIGeolocationUpdate* aCallback) { + if (mMLSProvider) { + return NS_OK; + } + + mMLSProvider = new MLSFallback(0); + return mMLSProvider->Startup(new MLSUpdate(aCallback)); +} + +void WindowsLocationProvider::CancelMLSProvider() { + if (!mMLSProvider) { + return; + } + + mMLSProvider->Shutdown(); + mMLSProvider = nullptr; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/system/windows/WindowsLocationProvider.h b/dom/system/windows/WindowsLocationProvider.h new file mode 100644 index 0000000000..6be395df73 --- /dev/null +++ b/dom/system/windows/WindowsLocationProvider.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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_dom_WindowsLocationProvider_h__ +#define mozilla_dom_WindowsLocationProvider_h__ + +#include "nsCOMPtr.h" +#include "nsIGeolocationProvider.h" + +#include <locationapi.h> + +class MLSFallback; + +namespace mozilla { +namespace dom { + +class WindowsLocationProvider final : public nsIGeolocationProvider { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONPROVIDER + + WindowsLocationProvider(); + + nsresult CreateAndWatchMLSProvider(nsIGeolocationUpdate* aCallback); + void CancelMLSProvider(); + + class MLSUpdate : public nsIGeolocationUpdate { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONUPDATE + explicit MLSUpdate(nsIGeolocationUpdate* aCallback); + + private: + nsCOMPtr<nsIGeolocationUpdate> mCallback; + virtual ~MLSUpdate() {} + }; + + private: + ~WindowsLocationProvider(); + + RefPtr<ILocation> mLocation; + RefPtr<MLSFallback> mMLSProvider; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_WindowsLocationProvider_h__ diff --git a/dom/system/windows/moz.build b/dom/system/windows/moz.build new file mode 100644 index 0000000000..45131af39e --- /dev/null +++ b/dom/system/windows/moz.build @@ -0,0 +1,11 @@ +# -*- 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/. + +SOURCES += ["nsHapticFeedback.cpp", "WindowsLocationProvider.cpp"] + +LOCAL_INCLUDES += ["/dom/geolocation"] + +FINAL_LIBRARY = "xul" diff --git a/dom/system/windows/nsHapticFeedback.cpp b/dom/system/windows/nsHapticFeedback.cpp new file mode 100644 index 0000000000..f85c5889d9 --- /dev/null +++ b/dom/system/windows/nsHapticFeedback.cpp @@ -0,0 +1,15 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "nsHapticFeedback.h" + +NS_IMPL_ISUPPORTS(nsHapticFeedback, nsIHapticFeedback) + +NS_IMETHODIMP +nsHapticFeedback::PerformSimpleAction(int32_t aType) { + // Todo + return NS_OK; +} diff --git a/dom/system/windows/nsHapticFeedback.h b/dom/system/windows/nsHapticFeedback.h new file mode 100644 index 0000000000..b15cb00f31 --- /dev/null +++ b/dom/system/windows/nsHapticFeedback.h @@ -0,0 +1,15 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "nsIHapticFeedback.h" + +class nsHapticFeedback final : public nsIHapticFeedback { + ~nsHapticFeedback() {} + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHAPTICFEEDBACK +}; |