summaryrefslogtreecommitdiffstats
path: root/dom/system
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /dom/system
parentInitial commit. (diff)
downloadfirefox-upstream/124.0.1.tar.xz
firefox-upstream/124.0.1.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--dom/system/IOUtils.cpp2898
-rw-r--r--dom/system/IOUtils.h925
-rw-r--r--dom/system/NetworkGeolocationProvider.sys.mjs504
-rw-r--r--dom/system/PathUtils.cpp635
-rw-r--r--dom/system/PathUtils.h260
-rw-r--r--dom/system/android/AndroidLocationProvider.cpp52
-rw-r--r--dom/system/android/AndroidLocationProvider.h23
-rw-r--r--dom/system/android/moz.build17
-rw-r--r--dom/system/android/nsHapticFeedback.cpp18
-rw-r--r--dom/system/android/nsHapticFeedback.h16
-rw-r--r--dom/system/components.conf17
-rw-r--r--dom/system/linux/GeoclueLocationProvider.cpp1060
-rw-r--r--dom/system/linux/GeoclueLocationProvider.h32
-rw-r--r--dom/system/linux/GpsdLocationProvider.cpp446
-rw-r--r--dom/system/linux/GpsdLocationProvider.h51
-rw-r--r--dom/system/linux/PortalLocationProvider.cpp351
-rw-r--r--dom/system/linux/PortalLocationProvider.h54
-rw-r--r--dom/system/linux/moz.build23
-rw-r--r--dom/system/mac/CoreLocationLocationProvider.h61
-rw-r--r--dom/system/mac/CoreLocationLocationProvider.mm253
-rw-r--r--dom/system/mac/moz.build21
-rw-r--r--dom/system/mac/nsOSPermissionRequest.h31
-rw-r--r--dom/system/mac/nsOSPermissionRequest.mm65
-rw-r--r--dom/system/moz.build113
-rw-r--r--dom/system/nsDeviceSensors.cpp557
-rw-r--r--dom/system/nsDeviceSensors.h72
-rw-r--r--dom/system/nsIOSPermissionRequest.idl69
-rw-r--r--dom/system/nsOSPermissionRequest.h18
-rw-r--r--dom/system/nsOSPermissionRequestBase.cpp96
-rw-r--r--dom/system/nsOSPermissionRequestBase.h38
-rw-r--r--dom/system/tests/chrome.toml6
-rw-r--r--dom/system/tests/file_bug1197901.html16
-rw-r--r--dom/system/tests/ioutils/chrome.toml39
-rw-r--r--dom/system/tests/ioutils/file_ioutils_test_fixtures.js78
-rw-r--r--dom/system/tests/ioutils/file_ioutils_worker.js219
-rw-r--r--dom/system/tests/ioutils/test_ioutils.html26
-rw-r--r--dom/system/tests/ioutils/test_ioutils_compute_hex_digest.html55
-rw-r--r--dom/system/tests/ioutils/test_ioutils_copy_move.html360
-rw-r--r--dom/system/tests/ioutils/test_ioutils_create_unique.html86
-rw-r--r--dom/system/tests/ioutils/test_ioutils_dir_iteration.html96
-rw-r--r--dom/system/tests/ioutils/test_ioutils_getfile.html84
-rw-r--r--dom/system/tests/ioutils/test_ioutils_mac_xattr.html91
-rw-r--r--dom/system/tests/ioutils/test_ioutils_mkdir.html135
-rw-r--r--dom/system/tests/ioutils/test_ioutils_read_write.html524
-rw-r--r--dom/system/tests/ioutils/test_ioutils_read_write_json.html193
-rw-r--r--dom/system/tests/ioutils/test_ioutils_read_write_utf8.html384
-rw-r--r--dom/system/tests/ioutils/test_ioutils_remove.html118
-rw-r--r--dom/system/tests/ioutils/test_ioutils_set_permissions.html84
-rw-r--r--dom/system/tests/ioutils/test_ioutils_stat_set_modification_time.html242
-rw-r--r--dom/system/tests/ioutils/test_ioutils_windows_file_attributes.html137
-rw-r--r--dom/system/tests/ioutils/test_ioutils_worker.xhtml40
-rw-r--r--dom/system/tests/mochitest.toml11
-rw-r--r--dom/system/tests/pathutils_worker.js37
-rw-r--r--dom/system/tests/test_bug1197901.html96
-rw-r--r--dom/system/tests/test_pathutils.html602
-rw-r--r--dom/system/tests/test_pathutils_worker.xhtml38
-rw-r--r--dom/system/windows/PWindowsUtils.ipdl23
-rw-r--r--dom/system/windows/WindowsUtilsChild.h30
-rw-r--r--dom/system/windows/WindowsUtilsParent.h52
-rw-r--r--dom/system/windows/location/PWindowsLocation.ipdl38
-rw-r--r--dom/system/windows/location/WindowsLocationChild.cpp257
-rw-r--r--dom/system/windows/location/WindowsLocationChild.h44
-rw-r--r--dom/system/windows/location/WindowsLocationParent.cpp36
-rw-r--r--dom/system/windows/location/WindowsLocationParent.h52
-rw-r--r--dom/system/windows/location/WindowsLocationProvider.cpp350
-rw-r--r--dom/system/windows/location/WindowsLocationProvider.h78
-rw-r--r--dom/system/windows/location/moz.build29
-rw-r--r--dom/system/windows/moz.build24
-rw-r--r--dom/system/windows/nsHapticFeedback.cpp15
-rw-r--r--dom/system/windows/nsHapticFeedback.h15
70 files changed, 13616 insertions, 0 deletions
diff --git a/dom/system/IOUtils.cpp b/dom/system/IOUtils.cpp
new file mode 100644
index 0000000000..08e2173452
--- /dev/null
+++ b/dom/system/IOUtils.cpp
@@ -0,0 +1,2898 @@
+/* -*- 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 "TypedArray.h"
+#include "js/ArrayBuffer.h"
+#include "js/ColumnNumber.h" // JS::ColumnNumberOneOrigin
+#include "js/JSON.h"
+#include "js/Utility.h"
+#include "js/experimental/TypedData.h"
+#include "jsfriendapi.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/Compression.h"
+#include "mozilla/Encoding.h"
+#include "mozilla/EndianUtils.h"
+#include "mozilla/ErrorNames.h"
+#include "mozilla/FileUtils.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/Try.h"
+#include "mozilla/Unused.h"
+#include "mozilla/Utf8.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/IOUtilsBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/WorkerCommon.h"
+#include "mozilla/dom/WorkerRef.h"
+#include "mozilla/ipc/LaunchError.h"
+#include "PathUtils.h"
+#include "nsCOMPtr.h"
+#include "nsError.h"
+#include "nsFileStreams.h"
+#include "nsIDirectoryEnumerator.h"
+#include "nsIFile.h"
+#include "nsIGlobalObject.h"
+#include "nsIInputStream.h"
+#include "nsISupports.h"
+#include "nsLocalFile.h"
+#include "nsNetUtil.h"
+#include "nsNSSComponent.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"
+#include "ScopedNSSTypes.h"
+#include "secoidt.h"
+
+#if defined(XP_UNIX) && !defined(ANDROID)
+# include "nsSystemInfo.h"
+#endif
+
+#if defined(XP_WIN)
+# include "nsILocalFileWin.h"
+#elif defined(XP_MACOSX)
+# include "nsILocalFileMac.h"
+#endif
+
+#ifdef XP_UNIX
+# include "base/process_util.h"
+#endif
+
+#define REJECT_IF_INIT_PATH_FAILED(_file, _path, _promise) \
+ do { \
+ if (nsresult _rv = PathUtils::InitFileWithPath((_file), (_path)); \
+ NS_FAILED(_rv)) { \
+ (_promise)->MaybeRejectWithOperationError( \
+ FormatErrorMessage(_rv, "Could not parse path (%s)", \
+ NS_ConvertUTF16toUTF8(_path).get())); \
+ return; \
+ } \
+ } while (0)
+
+static constexpr auto SHUTDOWN_ERROR =
+ "IOUtils: Shutting down and refusing additional I/O tasks"_ns;
+
+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;
+}
+/**
+ * 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));
+}
+
+[[nodiscard]] 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);
+
+ if (aInternalFileInfo.mCreationTime.isSome()) {
+ info.mCreationTime.Construct(aInternalFileInfo.mCreationTime.ref());
+ }
+ info.mLastAccessed.Construct(aInternalFileInfo.mLastAccessed);
+ info.mLastModified.Construct(aInternalFileInfo.mLastModified);
+
+ info.mPermissions.Construct(aInternalFileInfo.mPermissions);
+
+ return ToJSValue(aCx, info, aValue);
+}
+
+template <typename T>
+static void ResolveJSPromise(Promise* aPromise, T&& aValue) {
+ if constexpr (std::is_same_v<T, Ok>) {
+ aPromise->MaybeResolveWithUndefined();
+ } else if constexpr (std::is_same_v<T, nsTArray<uint8_t>>) {
+ TypedArrayCreator<Uint8Array> array(aValue);
+ aPromise->MaybeResolve(array);
+ } else {
+ aPromise->MaybeResolve(std::forward<T>(aValue));
+ }
+}
+
+static void RejectJSPromise(Promise* aPromise, const IOUtils::IOError& aError) {
+ const auto& errMsg = aError.Message();
+
+ switch (aError.Code()) {
+ case NS_ERROR_FILE_UNRESOLVABLE_SYMLINK:
+ [[fallthrough]]; // to NS_ERROR_FILE_INVALID_PATH
+ case NS_ERROR_FILE_NOT_FOUND:
+ [[fallthrough]]; // to NS_ERROR_FILE_INVALID_PATH
+ case NS_ERROR_FILE_INVALID_PATH:
+ aPromise->MaybeRejectWithNotFoundError(errMsg.refOr("File not found"_ns));
+ break;
+ case NS_ERROR_FILE_IS_LOCKED:
+ [[fallthrough]]; // to NS_ERROR_FILE_ACCESS_DENIED
+ 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_NO_DEVICE_SPACE:
+ aPromise->MaybeRejectWithNotReadableError(
+ errMsg.refOr("Target device is full"_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:
+ [[fallthrough]]; // to NS_ERROR_FILE_DESTINATION_NOT_DIR
+ case NS_ERROR_FILE_DESTINATION_NOT_DIR:
+ aPromise->MaybeRejectWithInvalidAccessError(
+ errMsg.refOr("Target file is not a directory"_ns));
+ break;
+ case NS_ERROR_FILE_IS_DIRECTORY:
+ aPromise->MaybeRejectWithInvalidAccessError(
+ errMsg.refOr("Target file is a directory"_ns));
+ break;
+ case NS_ERROR_FILE_UNKNOWN_TYPE:
+ aPromise->MaybeRejectWithInvalidAccessError(
+ errMsg.refOr("Target file is of unknown type"_ns));
+ break;
+ case NS_ERROR_FILE_NAME_TOO_LONG:
+ aPromise->MaybeRejectWithOperationError(
+ errMsg.refOr("Target file path is too long"_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_DEVICE_FAILURE:
+ [[fallthrough]]; // to NS_ERROR_FILE_FS_CORRUPTED
+ case NS_ERROR_FILE_FS_CORRUPTED:
+ aPromise->MaybeRejectWithNotReadableError(
+ errMsg.refOr("Target file system may be corrupt or unavailable"_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:
+ [[fallthrough]]; // NS_ERROR_ILLEGAL_VALUE
+ case NS_ERROR_ILLEGAL_VALUE:
+ aPromise->MaybeRejectWithDataError(
+ errMsg.refOr("Argument is not allowed"_ns));
+ break;
+ case NS_ERROR_NOT_AVAILABLE:
+ aPromise->MaybeRejectWithNotFoundError(errMsg.refOr("Unavailable"_ns));
+ break;
+ case NS_ERROR_ABORT:
+ aPromise->MaybeRejectWithAbortError(errMsg.refOr("Operation aborted"_ns));
+ break;
+ default:
+ aPromise->MaybeRejectWithUnknownError(FormatErrorMessage(
+ aError.Code(), errMsg.refOr("Unexpected error"_ns).get()));
+ }
+}
+
+static void RejectShuttingDown(Promise* aPromise) {
+ RejectJSPromise(aPromise,
+ IOUtils::IOError(NS_ERROR_ABORT).WithMessage(SHUTDOWN_ERROR));
+}
+
+static bool AssertParentProcessWithCallerLocationImpl(GlobalObject& aGlobal,
+ nsCString& reason) {
+ if (MOZ_LIKELY(XRE_IsParentProcess())) {
+ return true;
+ }
+
+ AutoJSAPI jsapi;
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ MOZ_ALWAYS_TRUE(global);
+ MOZ_ALWAYS_TRUE(jsapi.Init(global));
+
+ JSContext* cx = jsapi.cx();
+
+ JS::AutoFilename scriptFilename;
+ uint32_t lineNo = 0;
+ JS::ColumnNumberOneOrigin colNo;
+
+ NS_ENSURE_TRUE(
+ JS::DescribeScriptedCaller(cx, &scriptFilename, &lineNo, &colNo), false);
+
+ NS_ENSURE_TRUE(scriptFilename.get(), false);
+
+ reason.AppendPrintf(" Called from %s:%d:%d.", scriptFilename.get(), lineNo,
+ colNo.oneOriginValue());
+ return false;
+}
+
+static void AssertParentProcessWithCallerLocation(GlobalObject& aGlobal) {
+ nsCString reason = "IOUtils can only be used in the parent process."_ns;
+ if (!AssertParentProcessWithCallerLocationImpl(aGlobal, reason)) {
+ MOZ_CRASH_UNSAFE_PRINTF("%s", reason.get());
+ }
+}
+
+// IOUtils implementation
+/* static */
+IOUtils::StateMutex IOUtils::sState{"IOUtils::sState"};
+
+/* static */
+template <typename Fn>
+already_AddRefed<Promise> IOUtils::WithPromiseAndState(GlobalObject& aGlobal,
+ ErrorResult& aError,
+ Fn aFn) {
+ AssertParentProcessWithCallerLocation(aGlobal);
+
+ RefPtr<Promise> promise = CreateJSPromise(aGlobal, aError);
+ if (!promise) {
+ return nullptr;
+ }
+
+ if (auto state = GetState()) {
+ aFn(promise, state.ref());
+ } else {
+ RejectShuttingDown(promise);
+ }
+ return promise.forget();
+}
+
+/* static */
+template <typename OkT, typename Fn>
+void IOUtils::DispatchAndResolve(IOUtils::EventQueue* aQueue, Promise* aPromise,
+ Fn aFunc) {
+ RefPtr<StrongWorkerRef> workerRef;
+ if (!NS_IsMainThread()) {
+ // We need to manually keep the worker alive until the promise returned by
+ // Dispatch() resolves or rejects.
+ workerRef = StrongWorkerRef::CreateForcibly(GetCurrentThreadWorkerPrivate(),
+ __func__);
+ }
+
+ if (RefPtr<IOPromise<OkT>> p = aQueue->Dispatch<OkT, Fn>(std::move(aFunc))) {
+ p->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [workerRef, promise = RefPtr(aPromise)](OkT&& ok) {
+ ResolveJSPromise(promise, std::forward<OkT>(ok));
+ },
+ [workerRef, promise = RefPtr(aPromise)](const IOError& err) {
+ RejectJSPromise(promise, err);
+ });
+ }
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::Read(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const ReadOptions& aOptions,
+ ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ 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;
+ }
+ toRead.emplace(aOptions.mMaxBytes.Value());
+ }
+
+ DispatchAndResolve<JsBuffer>(
+ state->mEventQueue, promise,
+ [file = std::move(file), offset = aOptions.mOffset, toRead,
+ decompress = aOptions.mDecompress]() {
+ return ReadSync(file, offset, toRead, decompress,
+ BufferKind::Uint8Array);
+ });
+ });
+}
+
+/* static */
+RefPtr<SyncReadFile> IOUtils::OpenFileForSyncReading(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ ErrorResult& aRv) {
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
+
+ // This API is only exposed to workers, so we should not be on the main
+ // thread here.
+ MOZ_RELEASE_ASSERT(!NS_IsMainThread());
+
+ nsCOMPtr<nsIFile> file = new nsLocalFile();
+ if (nsresult rv = PathUtils::InitFileWithPath(file, aPath); NS_FAILED(rv)) {
+ aRv.ThrowOperationError(FormatErrorMessage(
+ rv, "Could not parse path (%s)", NS_ConvertUTF16toUTF8(aPath).get()));
+ return nullptr;
+ }
+
+ RefPtr<nsFileRandomAccessStream> stream = new nsFileRandomAccessStream();
+ if (nsresult rv =
+ stream->Init(file, PR_RDONLY | nsIFile::OS_READAHEAD, 0666, 0);
+ NS_FAILED(rv)) {
+ aRv.ThrowOperationError(
+ FormatErrorMessage(rv, "Could not open the file at %s",
+ NS_ConvertUTF16toUTF8(aPath).get()));
+ return nullptr;
+ }
+
+ int64_t size = 0;
+ if (nsresult rv = stream->GetSize(&size); NS_FAILED(rv)) {
+ aRv.ThrowOperationError(FormatErrorMessage(
+ rv, "Could not get the stream size for the file at %s",
+ NS_ConvertUTF16toUTF8(aPath).get()));
+ return nullptr;
+ }
+
+ return new SyncReadFile(aGlobal.GetAsSupports(), std::move(stream), size);
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::ReadUTF8(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const ReadUTF8Options& aOptions,
+ ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ nsCOMPtr<nsIFile> file = new nsLocalFile();
+ REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
+
+ DispatchAndResolve<JsBuffer>(
+ state->mEventQueue, promise,
+ [file = std::move(file), decompress = aOptions.mDecompress]() {
+ return ReadUTF8Sync(file, decompress);
+ });
+ });
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::ReadJSON(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const ReadUTF8Options& aOptions,
+ ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ nsCOMPtr<nsIFile> file = new nsLocalFile();
+ REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
+
+ RefPtr<StrongWorkerRef> workerRef;
+ if (!NS_IsMainThread()) {
+ // We need to manually keep the worker alive until the promise
+ // returned by Dispatch() resolves or rejects.
+ workerRef = StrongWorkerRef::CreateForcibly(
+ GetCurrentThreadWorkerPrivate(), __func__);
+ }
+
+ state->mEventQueue
+ ->template Dispatch<JsBuffer>(
+ [file, decompress = aOptions.mDecompress]() {
+ return ReadUTF8Sync(file, decompress);
+ })
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [workerRef, promise = RefPtr{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);
+ },
+ [workerRef, promise = RefPtr{promise}](const IOError& aErr) {
+ RejectJSPromise(promise, aErr);
+ });
+ });
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::Write(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const Uint8Array& aData,
+ const WriteOptions& aOptions,
+ ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ nsCOMPtr<nsIFile> file = new nsLocalFile();
+ REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
+
+ Maybe<Buffer<uint8_t>> buf = aData.CreateFromData<Buffer<uint8_t>>();
+ if (buf.isNothing()) {
+ promise->MaybeRejectWithOperationError(
+ "Out of memory: Could not allocate buffer while writing to file");
+ return;
+ }
+
+ auto opts = InternalWriteOpts::FromBinding(aOptions);
+ if (opts.isErr()) {
+ RejectJSPromise(promise, opts.unwrapErr());
+ return;
+ }
+
+ DispatchAndResolve<uint32_t>(
+ state->mEventQueue, promise,
+ [file = std::move(file), buf = buf.extract(),
+ opts = opts.unwrap()]() { return WriteSync(file, buf, opts); });
+ });
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::WriteUTF8(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const nsACString& aString,
+ const WriteOptions& aOptions,
+ ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ 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;
+ }
+
+ DispatchAndResolve<uint32_t>(
+ state->mEventQueue, promise,
+ [file = std::move(file), str = nsCString(aString),
+ opts = opts.unwrap()]() {
+ return WriteSync(file, AsBytes(Span(str)), opts);
+ });
+ });
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::WriteJSON(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ JS::Handle<JS::Value> aValue,
+ const WriteOptions& aOptions,
+ ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ 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;
+ }
+
+ if (opts.inspect().mMode == WriteMode::Append ||
+ opts.inspect().mMode == WriteMode::AppendOrCreate) {
+ promise->MaybeRejectWithNotSupportedError(
+ "IOUtils.writeJSON does not support appending to files."_ns);
+ return;
+ }
+
+ JSContext* cx = aGlobal.Context();
+ JS::Rooted<JS::Value> rootedValue(cx, aValue);
+ nsString string;
+ if (!nsContentUtils::StringifyJSON(cx, aValue, string,
+ UndefinedIsNullStringLiteral)) {
+ 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;
+ }
+
+ DispatchAndResolve<uint32_t>(
+ state->mEventQueue, promise,
+ [file = std::move(file), string = std::move(string),
+ opts = opts.unwrap()]() -> Result<uint32_t, IOError> {
+ nsAutoCString utf8Str;
+ if (!CopyUTF16toUTF8(string, utf8Str, fallible)) {
+ return Err(IOError(NS_ERROR_OUT_OF_MEMORY));
+ }
+ return WriteSync(file, AsBytes(Span(utf8Str)), opts);
+ });
+ });
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::Move(GlobalObject& aGlobal,
+ const nsAString& aSourcePath,
+ const nsAString& aDestPath,
+ const MoveOptions& aOptions,
+ ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ 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);
+
+ DispatchAndResolve<Ok>(
+ state->mEventQueue, promise,
+ [sourceFile = std::move(sourceFile), destFile = std::move(destFile),
+ noOverwrite = aOptions.mNoOverwrite]() {
+ return MoveSync(sourceFile, destFile, noOverwrite);
+ });
+ });
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::Remove(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const RemoveOptions& aOptions,
+ ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ nsCOMPtr<nsIFile> file = new nsLocalFile();
+ REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
+
+ DispatchAndResolve<Ok>(
+ state->mEventQueue, promise,
+ [file = std::move(file), ignoreAbsent = aOptions.mIgnoreAbsent,
+ recursive = aOptions.mRecursive,
+ retryReadonly = aOptions.mRetryReadonly]() {
+ return RemoveSync(file, ignoreAbsent, recursive, retryReadonly);
+ });
+ });
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::MakeDirectory(
+ GlobalObject& aGlobal, const nsAString& aPath,
+ const MakeDirectoryOptions& aOptions, ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ nsCOMPtr<nsIFile> file = new nsLocalFile();
+ REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
+
+ DispatchAndResolve<Ok>(state->mEventQueue, promise,
+ [file = std::move(file),
+ createAncestors = aOptions.mCreateAncestors,
+ ignoreExisting = aOptions.mIgnoreExisting,
+ permissions = aOptions.mPermissions]() {
+ return MakeDirectorySync(file, createAncestors,
+ ignoreExisting,
+ permissions);
+ });
+ });
+}
+
+already_AddRefed<Promise> IOUtils::Stat(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ nsCOMPtr<nsIFile> file = new nsLocalFile();
+ REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
+
+ DispatchAndResolve<InternalFileInfo>(
+ state->mEventQueue, promise,
+ [file = std::move(file)]() { return StatSync(file); });
+ });
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::Copy(GlobalObject& aGlobal,
+ const nsAString& aSourcePath,
+ const nsAString& aDestPath,
+ const CopyOptions& aOptions,
+ ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ 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);
+
+ DispatchAndResolve<Ok>(
+ state->mEventQueue, promise,
+ [sourceFile = std::move(sourceFile), destFile = std::move(destFile),
+ noOverwrite = aOptions.mNoOverwrite,
+ recursive = aOptions.mRecursive]() {
+ return CopySync(sourceFile, destFile, noOverwrite, recursive);
+ });
+ });
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::SetAccessTime(
+ GlobalObject& aGlobal, const nsAString& aPath,
+ const Optional<int64_t>& aAccess, ErrorResult& aError) {
+ return SetTime(aGlobal, aPath, aAccess, &nsIFile::SetLastAccessedTime,
+ aError);
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::SetModificationTime(
+ GlobalObject& aGlobal, const nsAString& aPath,
+ const Optional<int64_t>& aModification, ErrorResult& aError) {
+ return SetTime(aGlobal, aPath, aModification, &nsIFile::SetLastModifiedTime,
+ aError);
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::SetTime(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const Optional<int64_t>& aNewTime,
+ IOUtils::SetTimeFn aSetTimeFn,
+ ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ nsCOMPtr<nsIFile> file = new nsLocalFile();
+ REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
+
+ int64_t newTime = aNewTime.WasPassed() ? aNewTime.Value()
+ : PR_Now() / PR_USEC_PER_MSEC;
+ DispatchAndResolve<int64_t>(
+ state->mEventQueue, promise,
+ [file = std::move(file), aSetTimeFn, newTime]() {
+ return SetTimeSync(file, aSetTimeFn, newTime);
+ });
+ });
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::GetChildren(
+ GlobalObject& aGlobal, const nsAString& aPath,
+ const GetChildrenOptions& aOptions, ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ nsCOMPtr<nsIFile> file = new nsLocalFile();
+ REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
+
+ DispatchAndResolve<nsTArray<nsString>>(
+ state->mEventQueue, promise,
+ [file = std::move(file), ignoreAbsent = aOptions.mIgnoreAbsent]() {
+ return GetChildrenSync(file, ignoreAbsent);
+ });
+ });
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::SetPermissions(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ uint32_t aPermissions,
+ const bool aHonorUmask,
+ ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+#if defined(XP_UNIX) && !defined(ANDROID)
+ if (aHonorUmask) {
+ aPermissions &= ~nsSystemInfo::gUserUmask;
+ }
+#endif
+
+ nsCOMPtr<nsIFile> file = new nsLocalFile();
+ REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
+
+ DispatchAndResolve<Ok>(
+ state->mEventQueue, promise,
+ [file = std::move(file), permissions = aPermissions]() {
+ return SetPermissionsSync(file, permissions);
+ });
+ });
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::Exists(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ nsCOMPtr<nsIFile> file = new nsLocalFile();
+ REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
+
+ DispatchAndResolve<bool>(
+ state->mEventQueue, promise,
+ [file = std::move(file)]() { return ExistsSync(file); });
+ });
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::CreateUniqueFile(GlobalObject& aGlobal,
+ const nsAString& aParent,
+ const nsAString& aPrefix,
+ const uint32_t aPermissions,
+ ErrorResult& aError) {
+ return CreateUnique(aGlobal, aParent, aPrefix, nsIFile::NORMAL_FILE_TYPE,
+ aPermissions, aError);
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::CreateUniqueDirectory(
+ GlobalObject& aGlobal, const nsAString& aParent, const nsAString& aPrefix,
+ const uint32_t aPermissions, ErrorResult& aError) {
+ return CreateUnique(aGlobal, aParent, aPrefix, nsIFile::DIRECTORY_TYPE,
+ aPermissions, aError);
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::CreateUnique(GlobalObject& aGlobal,
+ const nsAString& aParent,
+ const nsAString& aPrefix,
+ const uint32_t aFileType,
+ const uint32_t aPermissions,
+ ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ nsCOMPtr<nsIFile> file = new nsLocalFile();
+ REJECT_IF_INIT_PATH_FAILED(file, aParent, promise);
+
+ if (nsresult rv = file->Append(aPrefix); NS_FAILED(rv)) {
+ RejectJSPromise(promise,
+ IOError(rv).WithMessage(
+ "Could not append prefix `%s' to parent `%s'",
+ NS_ConvertUTF16toUTF8(aPrefix).get(),
+ file->HumanReadablePath().get()));
+ return;
+ }
+
+ DispatchAndResolve<nsString>(
+ state->mEventQueue, promise,
+ [file = std::move(file), aPermissions, aFileType]() {
+ return CreateUniqueSync(file, aFileType, aPermissions);
+ });
+ });
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::ComputeHexDigest(
+ GlobalObject& aGlobal, const nsAString& aPath,
+ const HashAlgorithm aAlgorithm, ErrorResult& aError) {
+ const bool nssInitialized = EnsureNSSInitializedChromeOrContent();
+
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ if (!nssInitialized) {
+ RejectJSPromise(promise,
+ IOError(NS_ERROR_UNEXPECTED)
+ .WithMessage("Could not initialize NSS"));
+ return;
+ }
+
+ nsCOMPtr<nsIFile> file = new nsLocalFile();
+ REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
+
+ DispatchAndResolve<nsCString>(state->mEventQueue, promise,
+ [file = std::move(file), aAlgorithm]() {
+ return ComputeHexDigestSync(file,
+ aAlgorithm);
+ });
+ });
+}
+
+#if defined(XP_WIN)
+
+/* static */
+already_AddRefed<Promise> IOUtils::GetWindowsAttributes(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ nsCOMPtr<nsIFile> file = new nsLocalFile();
+ REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
+
+ RefPtr<StrongWorkerRef> workerRef;
+ if (!NS_IsMainThread()) {
+ // We need to manually keep the worker alive until the promise
+ // returned by Dispatch() resolves or rejects.
+ workerRef = StrongWorkerRef::CreateForcibly(
+ GetCurrentThreadWorkerPrivate(), __func__);
+ }
+
+ state->mEventQueue
+ ->template Dispatch<uint32_t>([file = std::move(file)]() {
+ return GetWindowsAttributesSync(file);
+ })
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [workerRef, promise = RefPtr{promise}](const uint32_t aAttrs) {
+ WindowsFileAttributes attrs;
+
+ attrs.mReadOnly.Construct(aAttrs & FILE_ATTRIBUTE_READONLY);
+ attrs.mHidden.Construct(aAttrs & FILE_ATTRIBUTE_HIDDEN);
+ attrs.mSystem.Construct(aAttrs & FILE_ATTRIBUTE_SYSTEM);
+
+ promise->MaybeResolve(attrs);
+ },
+ [workerRef, promise = RefPtr{promise}](const IOError& aErr) {
+ RejectJSPromise(promise, aErr);
+ });
+ });
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::SetWindowsAttributes(
+ GlobalObject& aGlobal, const nsAString& aPath,
+ const WindowsFileAttributes& aAttrs, ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ nsCOMPtr<nsIFile> file = new nsLocalFile();
+ REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
+
+ uint32_t setAttrs = 0;
+ uint32_t clearAttrs = 0;
+
+ if (aAttrs.mReadOnly.WasPassed()) {
+ if (aAttrs.mReadOnly.Value()) {
+ setAttrs |= FILE_ATTRIBUTE_READONLY;
+ } else {
+ clearAttrs |= FILE_ATTRIBUTE_READONLY;
+ }
+ }
+
+ if (aAttrs.mHidden.WasPassed()) {
+ if (aAttrs.mHidden.Value()) {
+ setAttrs |= FILE_ATTRIBUTE_HIDDEN;
+ } else {
+ clearAttrs |= FILE_ATTRIBUTE_HIDDEN;
+ }
+ }
+
+ if (aAttrs.mSystem.WasPassed()) {
+ if (aAttrs.mSystem.Value()) {
+ setAttrs |= FILE_ATTRIBUTE_SYSTEM;
+ } else {
+ clearAttrs |= FILE_ATTRIBUTE_SYSTEM;
+ }
+ }
+
+ DispatchAndResolve<Ok>(
+ state->mEventQueue, promise,
+ [file = std::move(file), setAttrs, clearAttrs]() {
+ return SetWindowsAttributesSync(file, setAttrs, clearAttrs);
+ });
+ });
+}
+
+#elif defined(XP_MACOSX)
+
+/* static */
+already_AddRefed<Promise> IOUtils::HasMacXAttr(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const nsACString& aAttr,
+ ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ nsCOMPtr<nsIFile> file = new nsLocalFile();
+ REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
+
+ DispatchAndResolve<bool>(
+ state->mEventQueue, promise,
+ [file = std::move(file), attr = nsCString(aAttr)]() {
+ return HasMacXAttrSync(file, attr);
+ });
+ });
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::GetMacXAttr(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const nsACString& aAttr,
+ ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ nsCOMPtr<nsIFile> file = new nsLocalFile();
+ REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
+
+ DispatchAndResolve<nsTArray<uint8_t>>(
+ state->mEventQueue, promise,
+ [file = std::move(file), attr = nsCString(aAttr)]() {
+ return GetMacXAttrSync(file, attr);
+ });
+ });
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::SetMacXAttr(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const nsACString& aAttr,
+ const Uint8Array& aValue,
+ ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ nsCOMPtr<nsIFile> file = new nsLocalFile();
+ REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
+
+ nsTArray<uint8_t> value;
+
+ if (!aValue.AppendDataTo(value)) {
+ RejectJSPromise(
+ promise,
+ IOError(NS_ERROR_OUT_OF_MEMORY)
+ .WithMessage(
+ "Could not allocate buffer to set extended attribute"));
+ return;
+ }
+
+ DispatchAndResolve<Ok>(state->mEventQueue, promise,
+ [file = std::move(file), attr = nsCString(aAttr),
+ value = std::move(value)] {
+ return SetMacXAttrSync(file, attr, value);
+ });
+ });
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::DelMacXAttr(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const nsACString& aAttr,
+ ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ nsCOMPtr<nsIFile> file = new nsLocalFile();
+ REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
+
+ DispatchAndResolve<Ok>(
+ state->mEventQueue, promise,
+ [file = std::move(file), attr = nsCString(aAttr)] {
+ return DelMacXAttrSync(file, attr);
+ });
+ });
+}
+
+#endif
+
+/* static */
+already_AddRefed<Promise> IOUtils::GetFile(
+ GlobalObject& aGlobal, const Sequence<nsString>& aComponents,
+ ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ ErrorResult joinErr;
+ nsCOMPtr<nsIFile> file = PathUtils::Join(aComponents, joinErr);
+ if (joinErr.Failed()) {
+ promise->MaybeReject(std::move(joinErr));
+ return;
+ }
+
+ nsCOMPtr<nsIFile> parent;
+ if (nsresult rv = file->GetParent(getter_AddRefs(parent));
+ NS_FAILED(rv)) {
+ RejectJSPromise(promise, IOError(rv).WithMessage(
+ "Could not get parent directory"));
+ return;
+ }
+
+ state->mEventQueue
+ ->template Dispatch<Ok>([parent = std::move(parent)]() {
+ return MakeDirectorySync(parent, /* aCreateAncestors = */ true,
+ /* aIgnoreExisting = */ true, 0755);
+ })
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [file = std::move(file), promise = RefPtr(promise)](const Ok&) {
+ promise->MaybeResolve(file);
+ },
+ [promise = RefPtr(promise)](const IOError& err) {
+ RejectJSPromise(promise, err);
+ });
+ });
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::GetDirectory(
+ GlobalObject& aGlobal, const Sequence<nsString>& aComponents,
+ ErrorResult& aError) {
+ return WithPromiseAndState(
+ aGlobal, aError, [&](Promise* promise, auto& state) {
+ ErrorResult joinErr;
+ nsCOMPtr<nsIFile> dir = PathUtils::Join(aComponents, joinErr);
+ if (joinErr.Failed()) {
+ promise->MaybeReject(std::move(joinErr));
+ return;
+ }
+
+ state->mEventQueue
+ ->template Dispatch<Ok>([dir]() {
+ return MakeDirectorySync(dir, /* aCreateAncestors = */ true,
+ /* aIgnoreExisting = */ true, 0755);
+ })
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [dir, promise = RefPtr(promise)](const Ok&) {
+ promise->MaybeResolve(dir);
+ },
+ [promise = RefPtr(promise)](const IOError& err) {
+ RejectJSPromise(promise, err);
+ });
+ });
+}
+
+/* static */
+already_AddRefed<Promise> IOUtils::CreateJSPromise(GlobalObject& aGlobal,
+ ErrorResult& aError) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ RefPtr<Promise> promise = Promise::Create(global, aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+ MOZ_ASSERT(promise);
+ return do_AddRef(promise);
+}
+
+/* static */
+Result<IOUtils::JsBuffer, IOUtils::IOError> IOUtils::ReadSync(
+ nsIFile* aFile, const uint64_t aOffset, 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"));
+ }
+
+ if (aOffset > static_cast<uint64_t>(INT64_MAX)) {
+ return Err(IOError(NS_ERROR_ILLEGAL_INPUT)
+ .WithMessage("Requested offset is too large (%" PRIu64
+ " > %" PRId64 ")",
+ aOffset, INT64_MAX));
+ }
+
+ const int64_t offset = static_cast<int64_t>(aOffset);
+
+ RefPtr<nsFileRandomAccessStream> stream = new nsFileRandomAccessStream();
+ 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()));
+ }
+
+ uint32_t bufSize = 0;
+
+ if (aMaxBytes.isNothing()) {
+ // Limitation: We cannot read more than the maximum size of a TypedArray
+ // (UINT32_MAX bytes). Reject if we have been requested to
+ // perform too large of a read.
+
+ int64_t rawStreamSize = -1;
+ if (nsresult rv = stream->GetSize(&rawStreamSize); 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(rawStreamSize >= 0);
+
+ uint64_t streamSize = static_cast<uint64_t>(rawStreamSize);
+ if (aOffset >= streamSize) {
+ bufSize = 0;
+ } else {
+ if (streamSize - offset > static_cast<int64_t>(UINT32_MAX)) {
+ return Err(IOError(NS_ERROR_FILE_TOO_BIG)
+ .WithMessage(
+ "Could not read the file at %s with offset %" PRIu32
+ " because it is too large(size=%" PRIu64 " bytes)",
+ aFile->HumanReadablePath().get(), offset,
+ streamSize));
+ }
+
+ bufSize = static_cast<uint32_t>(streamSize - offset);
+ }
+ } else {
+ bufSize = aMaxBytes.value();
+ }
+
+ if (offset > 0) {
+ if (nsresult rv = stream->Seek(PR_SEEK_SET, offset); NS_FAILED(rv)) {
+ return Err(IOError(rv).WithMessage(
+ "Could not seek to position %" PRId64 " in file %s", offset,
+ aFile->HumanReadablePath().get()));
+ }
+ }
+
+ 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) {
+ // Read no more than INT32_MAX on each call to stream->Read, otherwise it
+ // returns an error.
+ uint32_t bytesToReadThisChunk =
+ std::min<uint32_t>(bufSize - totalRead, INT32_MAX);
+ uint32_t bytesRead = 0;
+ if (nsresult rv =
+ stream->Read(toRead.Elements(), bytesToReadThisChunk, &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, 0, 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 (exists && aOptions.mMode == WriteMode::Create) {
+ return Err(IOError(NS_ERROR_FILE_ALREADY_EXISTS)
+ .WithMessage("Refusing to overwrite the file at %s\n"
+ "Specify `mode: \"overwrite\"` 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)));
+
+ bool noOverwrite = aOptions.mMode == WriteMode::Create;
+
+ if (MoveSync(toMove, backupFile, noOverwrite).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;
+
+ switch (aOptions.mMode) {
+ case WriteMode::Overwrite:
+ flags |= PR_TRUNCATE | PR_CREATE_FILE;
+ break;
+
+ case WriteMode::Append:
+ flags |= PR_APPEND;
+ break;
+
+ case WriteMode::AppendOrCreate:
+ flags |= PR_APPEND | PR_CREATE_FILE;
+ break;
+
+ case WriteMode::Create:
+ flags |= PR_CREATE_FILE | PR_EXCL;
+ break;
+
+ default:
+ MOZ_CRASH("IOUtils: unknown write mode");
+ }
+
+ 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)) {
+ // Normalize platform-specific errors for opening a directory to an access
+ // denied error.
+ if (rv == nsresult::NS_ERROR_FILE_IS_DIRECTORY) {
+ rv = NS_ERROR_FILE_ACCESS_DENIED;
+ }
+ return Err(
+ IOError(rv).WithMessage("Could not open the file at %s for writing",
+ writeFile->HumanReadablePath().get()));
+ }
+
+ // nsFileRandomAccessStream::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) {
+ if (aOptions.mTmpFile) {
+ bool isDir = false;
+ if (nsresult rv = aFile->IsDirectory(&isDir);
+ NS_FAILED(rv) && !IsFileNotFound(rv)) {
+ return Err(IOError(rv).WithMessage("Could not stat the file at %s",
+ aFile->HumanReadablePath().get()));
+ }
+
+ // If we attempt to write to a directory *without* a temp file, we get a
+ // permission error.
+ //
+ // However, if we are writing to a temp file first, when we copy the
+ // temp file over the destination file, we actually end up copying it
+ // inside the directory, which is not what we want. In this case, we are
+ // just going to bail out early.
+ if (isDir) {
+ return Err(
+ IOError(NS_ERROR_FILE_ACCESS_DENIED)
+ .WithMessage("Could not open the file at %s for writing",
+ aFile->HumanReadablePath().get()));
+ }
+ }
+
+ if (MoveSync(writeFile, aFile, /* aNoOverwrite = */ 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)));
+
+ // We know `destName` is a file and therefore must have a parent directory.
+ MOZ_RELEASE_ASSERT(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,
+ bool aRetryReadonly) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ // Prevent an unused variable warning.
+ (void)aRetryReadonly;
+
+ 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()));
+ }
+
+#ifdef XP_WIN
+
+ if (rv == NS_ERROR_FILE_ACCESS_DENIED && aRetryReadonly) {
+ MOZ_TRY(SetWindowsAttributesSync(aFile, 0, FILE_ATTRIBUTE_READONLY));
+ return RemoveSync(aFile, aIgnoreAbsent, aRecursive,
+ /* aRetryReadonly = */ false);
+ }
+
+#endif
+
+ 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());
+
+ nsCOMPtr<nsIFile> parent;
+ MOZ_TRY(aFile->GetParent(getter_AddRefs(parent)));
+ if (!parent) {
+ // If we don't have a parent directory, we were called with a
+ // root directory. If the directory doesn't already exist (e.g., asking
+ // for a drive on Windows that does not exist), we will not be able to
+ // create it.
+ //
+ // Calling `nsLocalFile::Create()` on Windows can fail with
+ // `NS_ERROR_ACCESS_DENIED` trying to create a root directory, but we
+ // would rather the call succeed, so return early if the directory exists.
+ //
+ // Otherwise, we fall through to `nsiFile::Create()` and let it fail there
+ // instead.
+ bool exists = false;
+ MOZ_TRY(aFile->Exists(&exists));
+ if (exists) {
+ return Ok();
+ }
+ }
+
+ nsresult rv =
+ aFile->Create(nsIFile::DIRECTORY_TYPE, aMode, !aCreateAncestors);
+ 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 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));
+ }
+
+ PRTime lastAccessed = 0;
+ MOZ_TRY(aFile->GetLastAccessedTime(&lastAccessed));
+ info.mLastAccessed = static_cast<int64_t>(lastAccessed);
+
+ PRTime lastModified = 0;
+ MOZ_TRY(aFile->GetLastModifiedTime(&lastModified));
+ info.mLastModified = static_cast<int64_t>(lastModified);
+
+ MOZ_TRY(aFile->GetPermissions(&info.mPermissions));
+
+ return info;
+}
+
+/* static */
+Result<int64_t, IOUtils::IOError> IOUtils::SetTimeSync(
+ nsIFile* aFile, IOUtils::SetTimeFn aSetTimeFn, int64_t aNewTime) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ // 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 (aNewTime == 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 `setModificationTime` "
+ "with no arguments",
+ aFile->HumanReadablePath().get()));
+ }
+
+ nsresult rv = (aFile->*aSetTimeFn)(aNewTime);
+
+ if (NS_FAILED(rv)) {
+ IOError err(rv);
+ if (IsFileNotFound(rv)) {
+ return Err(
+ err.WithMessage("Could not set modification time of file(%s) "
+ "because it does not exist",
+ aFile->HumanReadablePath().get()));
+ }
+ return Err(err);
+ }
+ return aNewTime;
+}
+
+/* static */
+Result<nsTArray<nsString>, IOUtils::IOError> IOUtils::GetChildrenSync(
+ nsIFile* aFile, bool aIgnoreAbsent) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ nsTArray<nsString> children;
+ nsCOMPtr<nsIDirectoryEnumerator> iter;
+ nsresult rv = aFile->GetDirectoryEntries(getter_AddRefs(iter));
+ if (aIgnoreAbsent && IsFileNotFound(rv)) {
+ return children;
+ }
+ 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);
+ }
+
+ 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<nsString, IOUtils::IOError> IOUtils::CreateUniqueSync(
+ nsIFile* aFile, const uint32_t aFileType, const uint32_t aPermissions) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ if (nsresult rv = aFile->CreateUnique(aFileType, aPermissions);
+ NS_FAILED(rv)) {
+ return Err(IOError(rv).WithMessage("Could not create unique path"));
+ }
+
+ nsString path;
+ MOZ_ALWAYS_SUCCEEDS(aFile->GetPath(path));
+
+ return path;
+}
+
+/* static */
+Result<nsCString, IOUtils::IOError> IOUtils::ComputeHexDigestSync(
+ nsIFile* aFile, const HashAlgorithm aAlgorithm) {
+ static constexpr size_t BUFFER_SIZE = 8192;
+
+ SECOidTag alg;
+ switch (aAlgorithm) {
+ case HashAlgorithm::Sha256:
+ alg = SEC_OID_SHA256;
+ break;
+
+ case HashAlgorithm::Sha384:
+ alg = SEC_OID_SHA384;
+ break;
+
+ case HashAlgorithm::Sha512:
+ alg = SEC_OID_SHA512;
+ break;
+
+ default:
+ MOZ_RELEASE_ASSERT(false, "Unexpected HashAlgorithm");
+ }
+
+ Digest digest;
+ if (nsresult rv = digest.Begin(alg); NS_FAILED(rv)) {
+ return Err(IOError(rv).WithMessage("Could not hash file at %s",
+ aFile->HumanReadablePath().get()));
+ }
+
+ RefPtr<nsIInputStream> stream;
+ if (nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), aFile);
+ NS_FAILED(rv)) {
+ return Err(IOError(rv).WithMessage("Could not open the file at %s",
+ aFile->HumanReadablePath().get()));
+ }
+
+ char buffer[BUFFER_SIZE];
+ uint32_t read = 0;
+ for (;;) {
+ if (nsresult rv = stream->Read(buffer, BUFFER_SIZE, &read); NS_FAILED(rv)) {
+ return Err(IOError(rv).WithMessage(
+ "Encountered an unexpected error while reading file(%s)",
+ aFile->HumanReadablePath().get()));
+ }
+ if (read == 0) {
+ break;
+ }
+
+ if (nsresult rv =
+ digest.Update(reinterpret_cast<unsigned char*>(buffer), read);
+ NS_FAILED(rv)) {
+ return Err(IOError(rv).WithMessage("Could not hash file at %s",
+ aFile->HumanReadablePath().get()));
+ }
+ }
+
+ AutoTArray<uint8_t, SHA512_LENGTH> rawDigest;
+ if (nsresult rv = digest.End(rawDigest); NS_FAILED(rv)) {
+ return Err(IOError(rv).WithMessage("Could not hash file at %s",
+ aFile->HumanReadablePath().get()));
+ }
+
+ nsCString hexDigest;
+ if (!hexDigest.SetCapacity(2 * rawDigest.Length(), fallible)) {
+ return Err(IOError(NS_ERROR_OUT_OF_MEMORY));
+ }
+
+ const char HEX[] = "0123456789abcdef";
+ for (uint8_t b : rawDigest) {
+ hexDigest.Append(HEX[(b >> 4) & 0xF]);
+ hexDigest.Append(HEX[b & 0xF]);
+ }
+
+ return hexDigest;
+}
+
+#if defined(XP_WIN)
+
+Result<uint32_t, IOUtils::IOError> IOUtils::GetWindowsAttributesSync(
+ nsIFile* aFile) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ uint32_t attrs = 0;
+
+ nsCOMPtr<nsILocalFileWin> file = do_QueryInterface(aFile);
+ MOZ_ASSERT(file);
+
+ if (nsresult rv = file->GetWindowsFileAttributes(&attrs); NS_FAILED(rv)) {
+ return Err(IOError(rv).WithMessage(
+ "Could not get Windows file attributes for the file at `%s'",
+ aFile->HumanReadablePath().get()));
+ }
+ return attrs;
+}
+
+Result<Ok, IOUtils::IOError> IOUtils::SetWindowsAttributesSync(
+ nsIFile* aFile, const uint32_t aSetAttrs, const uint32_t aClearAttrs) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ nsCOMPtr<nsILocalFileWin> file = do_QueryInterface(aFile);
+ MOZ_ASSERT(file);
+
+ if (nsresult rv = file->SetWindowsFileAttributes(aSetAttrs, aClearAttrs);
+ NS_FAILED(rv)) {
+ return Err(IOError(rv).WithMessage(
+ "Could not set Windows file attributes for the file at `%s'",
+ aFile->HumanReadablePath().get()));
+ }
+
+ return Ok{};
+}
+
+#elif defined(XP_MACOSX)
+
+/* static */
+Result<bool, IOUtils::IOError> IOUtils::HasMacXAttrSync(
+ nsIFile* aFile, const nsCString& aAttr) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ nsCOMPtr<nsILocalFileMac> file = do_QueryInterface(aFile);
+ MOZ_ASSERT(file);
+
+ bool hasAttr = false;
+ if (nsresult rv = file->HasXAttr(aAttr, &hasAttr); NS_FAILED(rv)) {
+ return Err(IOError(rv).WithMessage(
+ "Could not read the extended attribute `%s' from the file `%s'",
+ aAttr.get(), aFile->HumanReadablePath().get()));
+ }
+
+ return hasAttr;
+}
+
+/* static */
+Result<nsTArray<uint8_t>, IOUtils::IOError> IOUtils::GetMacXAttrSync(
+ nsIFile* aFile, const nsCString& aAttr) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ nsCOMPtr<nsILocalFileMac> file = do_QueryInterface(aFile);
+ MOZ_ASSERT(file);
+
+ nsTArray<uint8_t> value;
+ if (nsresult rv = file->GetXAttr(aAttr, value); NS_FAILED(rv)) {
+ auto err = IOError(rv);
+
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ return Err(err.WithMessage(
+ "The file `%s' does not have an extended attribute `%s'",
+ aFile->HumanReadablePath().get(), aAttr.get()));
+ }
+
+ return Err(err.WithMessage(
+ "Could not read the extended attribute `%s' from the file `%s'",
+ aAttr.get(), aFile->HumanReadablePath().get()));
+ }
+
+ return value;
+}
+
+/* static */
+Result<Ok, IOUtils::IOError> IOUtils::SetMacXAttrSync(
+ nsIFile* aFile, const nsCString& aAttr, const nsTArray<uint8_t>& aValue) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ nsCOMPtr<nsILocalFileMac> file = do_QueryInterface(aFile);
+ MOZ_ASSERT(file);
+
+ if (nsresult rv = file->SetXAttr(aAttr, aValue); NS_FAILED(rv)) {
+ return Err(IOError(rv).WithMessage(
+ "Could not set extended attribute `%s' on file `%s'", aAttr.get(),
+ aFile->HumanReadablePath().get()));
+ }
+
+ return Ok{};
+}
+
+/* static */
+Result<Ok, IOUtils::IOError> IOUtils::DelMacXAttrSync(nsIFile* aFile,
+ const nsCString& aAttr) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ nsCOMPtr<nsILocalFileMac> file = do_QueryInterface(aFile);
+ MOZ_ASSERT(file);
+
+ if (nsresult rv = file->DelXAttr(aAttr); NS_FAILED(rv)) {
+ auto err = IOError(rv);
+
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ return Err(err.WithMessage(
+ "The file `%s' does not have an extended attribute `%s'",
+ aFile->HumanReadablePath().get(), aAttr.get()));
+ }
+
+ return Err(IOError(rv).WithMessage(
+ "Could not delete extended attribute `%s' on file `%s'", aAttr.get(),
+ aFile->HumanReadablePath().get()));
+ }
+
+ return Ok{};
+}
+
+#endif
+
+/* static */
+void IOUtils::GetProfileBeforeChange(GlobalObject& aGlobal,
+ JS::MutableHandle<JS::Value> aClient,
+ ErrorResult& aRv) {
+ return GetShutdownClient(aGlobal, aClient, aRv,
+ ShutdownPhase::ProfileBeforeChange);
+}
+
+/* static */
+void IOUtils::GetSendTelemetry(GlobalObject& aGlobal,
+ JS::MutableHandle<JS::Value> aClient,
+ ErrorResult& aRv) {
+ return GetShutdownClient(aGlobal, aClient, aRv, ShutdownPhase::SendTelemetry);
+}
+
+/**
+ * Assert that the given phase has a shutdown client exposed by IOUtils
+ *
+ * There is no shutdown client exposed for XpcomWillShutdown.
+ */
+static void AssertHasShutdownClient(const IOUtils::ShutdownPhase aPhase) {
+ MOZ_RELEASE_ASSERT(aPhase >= IOUtils::ShutdownPhase::ProfileBeforeChange &&
+ aPhase < IOUtils::ShutdownPhase::XpcomWillShutdown);
+}
+
+/* static */
+void IOUtils::GetShutdownClient(GlobalObject& aGlobal,
+ JS::MutableHandle<JS::Value> aClient,
+ ErrorResult& aRv,
+ const IOUtils::ShutdownPhase aPhase) {
+ MOZ_RELEASE_ASSERT(XRE_IsParentProcess());
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+ AssertHasShutdownClient(aPhase);
+
+ if (auto state = GetState()) {
+ MOZ_RELEASE_ASSERT(state.ref()->mBlockerStatus !=
+ ShutdownBlockerStatus::Uninitialized);
+
+ if (state.ref()->mBlockerStatus == ShutdownBlockerStatus::Failed) {
+ aRv.ThrowAbortError("IOUtils: could not register shutdown blockers");
+ return;
+ }
+
+ MOZ_RELEASE_ASSERT(state.ref()->mBlockerStatus ==
+ ShutdownBlockerStatus::Initialized);
+ auto result = state.ref()->mEventQueue->GetShutdownClient(aPhase);
+ if (result.isErr()) {
+ aRv.ThrowAbortError("IOUtils: could not get shutdown client");
+ return;
+ }
+
+ RefPtr<nsIAsyncShutdownClient> client = result.unwrap();
+ MOZ_RELEASE_ASSERT(client);
+ if (nsresult rv = client->GetJsclient(aClient); NS_FAILED(rv)) {
+ aRv.ThrowAbortError("IOUtils: Could not get shutdown jsclient");
+ }
+ return;
+ }
+
+ aRv.ThrowAbortError(
+ "IOUtils: profileBeforeChange phase has already finished");
+}
+
+/* sstatic */
+Maybe<IOUtils::StateMutex::AutoLock> IOUtils::GetState() {
+ auto state = sState.Lock();
+ if (state->mQueueStatus == EventQueueStatus::Shutdown) {
+ return Nothing{};
+ }
+
+ if (state->mQueueStatus == EventQueueStatus::Uninitialized) {
+ MOZ_RELEASE_ASSERT(!state->mEventQueue);
+ state->mEventQueue = new EventQueue();
+ state->mQueueStatus = EventQueueStatus::Initialized;
+
+ MOZ_RELEASE_ASSERT(state->mBlockerStatus ==
+ ShutdownBlockerStatus::Uninitialized);
+ }
+
+ if (NS_IsMainThread() &&
+ state->mBlockerStatus == ShutdownBlockerStatus::Uninitialized) {
+ state->SetShutdownHooks();
+ }
+
+ return Some(std::move(state));
+}
+
+IOUtils::EventQueue::EventQueue() {
+ MOZ_ALWAYS_SUCCEEDS(NS_CreateBackgroundTaskQueue(
+ "IOUtils::EventQueue", getter_AddRefs(mBackgroundEventTarget)));
+
+ MOZ_RELEASE_ASSERT(mBackgroundEventTarget);
+}
+
+void IOUtils::State::SetShutdownHooks() {
+ if (mBlockerStatus != ShutdownBlockerStatus::Uninitialized) {
+ return;
+ }
+
+ if (NS_WARN_IF(NS_FAILED(mEventQueue->SetShutdownHooks()))) {
+ mBlockerStatus = ShutdownBlockerStatus::Failed;
+ } else {
+ mBlockerStatus = ShutdownBlockerStatus::Initialized;
+ }
+
+ if (mBlockerStatus != ShutdownBlockerStatus::Initialized) {
+ NS_WARNING("IOUtils: could not register shutdown blockers.");
+ }
+}
+
+nsresult IOUtils::EventQueue::SetShutdownHooks() {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+
+ constexpr static auto STACK = u"IOUtils::EventQueue::SetShutdownHooks"_ns;
+ constexpr static auto FILE = NS_LITERAL_STRING_FROM_CSTRING(__FILE__);
+
+ nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdownService();
+ if (!svc) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsCOMPtr<nsIAsyncShutdownBlocker> profileBeforeChangeBlocker;
+
+ // Create a shutdown blocker for the profile-before-change phase.
+ {
+ profileBeforeChangeBlocker =
+ new IOUtilsShutdownBlocker(ShutdownPhase::ProfileBeforeChange);
+
+ nsCOMPtr<nsIAsyncShutdownClient> globalClient;
+ MOZ_TRY(svc->GetProfileBeforeChange(getter_AddRefs(globalClient)));
+ MOZ_RELEASE_ASSERT(globalClient);
+
+ MOZ_TRY(globalClient->AddBlocker(profileBeforeChangeBlocker, FILE, __LINE__,
+ STACK));
+ }
+
+ // Create the shutdown barrier for profile-before-change so that consumers can
+ // register shutdown blockers.
+ //
+ // The blocker we just created will wait for all clients registered on this
+ // barrier to finish.
+ {
+ nsCOMPtr<nsIAsyncShutdownBarrier> barrier;
+
+ // It is okay for this to fail. The created shutdown blocker won't await
+ // anything and shutdown will proceed.
+ MOZ_TRY(svc->MakeBarrier(
+ u"IOUtils: waiting for profileBeforeChange IO to complete"_ns,
+ getter_AddRefs(barrier)));
+ MOZ_RELEASE_ASSERT(barrier);
+
+ mBarriers[ShutdownPhase::ProfileBeforeChange] = std::move(barrier);
+ }
+
+ // Create a shutdown blocker for the profile-before-change-telemetry phase.
+ nsCOMPtr<nsIAsyncShutdownBlocker> sendTelemetryBlocker;
+ {
+ sendTelemetryBlocker =
+ new IOUtilsShutdownBlocker(ShutdownPhase::SendTelemetry);
+
+ nsCOMPtr<nsIAsyncShutdownClient> globalClient;
+ MOZ_TRY(svc->GetSendTelemetry(getter_AddRefs(globalClient)));
+ MOZ_RELEASE_ASSERT(globalClient);
+
+ MOZ_TRY(
+ globalClient->AddBlocker(sendTelemetryBlocker, FILE, __LINE__, STACK));
+ }
+
+ // Create the shutdown barrier for profile-before-change-telemetry so that
+ // consumers can register shutdown blockers.
+ //
+ // The blocker we just created will wait for all clients registered on this
+ // barrier to finish.
+ {
+ nsCOMPtr<nsIAsyncShutdownBarrier> barrier;
+
+ MOZ_TRY(svc->MakeBarrier(
+ u"IOUtils: waiting for sendTelemetry IO to complete"_ns,
+ getter_AddRefs(barrier)));
+ MOZ_RELEASE_ASSERT(barrier);
+
+ // Add a blocker on the previous shutdown phase.
+ nsCOMPtr<nsIAsyncShutdownClient> client;
+ MOZ_TRY(barrier->GetClient(getter_AddRefs(client)));
+
+ MOZ_TRY(
+ client->AddBlocker(profileBeforeChangeBlocker, FILE, __LINE__, STACK));
+
+ mBarriers[ShutdownPhase::SendTelemetry] = std::move(barrier);
+ }
+
+ // Create a shutdown blocker for the xpcom-will-shutdown phase.
+ {
+ nsCOMPtr<nsIAsyncShutdownClient> globalClient;
+ MOZ_TRY(svc->GetXpcomWillShutdown(getter_AddRefs(globalClient)));
+ MOZ_RELEASE_ASSERT(globalClient);
+
+ nsCOMPtr<nsIAsyncShutdownBlocker> blocker =
+ new IOUtilsShutdownBlocker(ShutdownPhase::XpcomWillShutdown);
+ MOZ_TRY(globalClient->AddBlocker(
+ blocker, FILE, __LINE__, u"IOUtils::EventQueue::SetShutdownHooks"_ns));
+ }
+
+ // Create a shutdown barrier for the xpcom-will-shutdown phase.
+ //
+ // The blocker we just created will wait for all clients registered on this
+ // barrier to finish.
+ //
+ // The only client registered on this barrier should be a blocker for the
+ // previous phase. This is to ensure that all shutdown IO happens when
+ // shutdown phases do not happen (e.g., in xpcshell tests where
+ // profile-before-change does not occur).
+ {
+ nsCOMPtr<nsIAsyncShutdownBarrier> barrier;
+
+ MOZ_TRY(svc->MakeBarrier(
+ u"IOUtils: waiting for xpcomWillShutdown IO to complete"_ns,
+ getter_AddRefs(barrier)));
+ MOZ_RELEASE_ASSERT(barrier);
+
+ // Add a blocker on the previous shutdown phase.
+ nsCOMPtr<nsIAsyncShutdownClient> client;
+ MOZ_TRY(barrier->GetClient(getter_AddRefs(client)));
+
+ client->AddBlocker(sendTelemetryBlocker, FILE, __LINE__,
+ u"IOUtils::EventQueue::SetShutdownHooks"_ns);
+
+ mBarriers[ShutdownPhase::XpcomWillShutdown] = std::move(barrier);
+ }
+
+ return NS_OK;
+}
+
+template <typename OkT, typename Fn>
+RefPtr<IOUtils::IOPromise<OkT>> IOUtils::EventQueue::Dispatch(Fn aFunc) {
+ MOZ_RELEASE_ASSERT(mBackgroundEventTarget);
+
+ auto promise =
+ MakeRefPtr<typename IOUtils::IOPromise<OkT>::Private>(__func__);
+ mBackgroundEventTarget->Dispatch(
+ NS_NewRunnableFunction("IOUtils::EventQueue::Dispatch",
+ [promise, func = std::move(aFunc)] {
+ Result<OkT, IOError> result = func();
+ if (result.isErr()) {
+ promise->Reject(result.unwrapErr(), __func__);
+ } else {
+ promise->Resolve(result.unwrap(), __func__);
+ }
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+ return promise;
+};
+
+Result<already_AddRefed<nsIAsyncShutdownBarrier>, nsresult>
+IOUtils::EventQueue::GetShutdownBarrier(const IOUtils::ShutdownPhase aPhase) {
+ if (!mBarriers[aPhase]) {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ }
+
+ return do_AddRef(mBarriers[aPhase]);
+}
+
+Result<already_AddRefed<nsIAsyncShutdownClient>, nsresult>
+IOUtils::EventQueue::GetShutdownClient(const IOUtils::ShutdownPhase aPhase) {
+ AssertHasShutdownClient(aPhase);
+
+ if (!mBarriers[aPhase]) {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ }
+
+ nsCOMPtr<nsIAsyncShutdownClient> client;
+ MOZ_TRY(mBarriers[aPhase]->GetClient(getter_AddRefs(client)));
+
+ return do_AddRef(client);
+}
+
+/* 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,
+ nsIAsyncShutdownCompletionCallback);
+
+NS_IMETHODIMP IOUtilsShutdownBlocker::GetName(nsAString& aName) {
+ aName = u"IOUtils Blocker ("_ns;
+ aName.Append(PHASE_NAMES[mPhase]);
+ aName.Append(')');
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP IOUtilsShutdownBlocker::BlockShutdown(
+ nsIAsyncShutdownClient* aBarrierClient) {
+ using EventQueueStatus = IOUtils::EventQueueStatus;
+ using ShutdownPhase = IOUtils::ShutdownPhase;
+
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIAsyncShutdownBarrier> barrier;
+
+ {
+ auto state = IOUtils::sState.Lock();
+ if (state->mQueueStatus == EventQueueStatus::Shutdown) {
+ // If the previous blockers have already run, then the event queue is
+ // already torn down and we have nothing to do.
+
+ MOZ_RELEASE_ASSERT(mPhase == ShutdownPhase::XpcomWillShutdown);
+ MOZ_RELEASE_ASSERT(!state->mEventQueue);
+
+ Unused << NS_WARN_IF(NS_FAILED(aBarrierClient->RemoveBlocker(this)));
+ mParentClient = nullptr;
+
+ return NS_OK;
+ }
+
+ MOZ_RELEASE_ASSERT(state->mEventQueue);
+
+ mParentClient = aBarrierClient;
+
+ barrier = state->mEventQueue->GetShutdownBarrier(mPhase).unwrapOr(nullptr);
+ }
+
+ // We cannot barrier->Wait() while holding the mutex because it will lead to
+ // deadlock.
+ if (!barrier || NS_WARN_IF(NS_FAILED(barrier->Wait(this)))) {
+ // If we don't have a barrier, we still need to flush the IOUtils event
+ // queue and disable task submission.
+ //
+ // Likewise, if waiting on the barrier failed, we are going to make our best
+ // attempt to clean up.
+ Unused << Done();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP IOUtilsShutdownBlocker::Done() {
+ using EventQueueStatus = IOUtils::EventQueueStatus;
+ using ShutdownPhase = IOUtils::ShutdownPhase;
+
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+
+ bool didFlush = false;
+
+ {
+ auto state = IOUtils::sState.Lock();
+
+ if (state->mEventQueue) {
+ MOZ_RELEASE_ASSERT(state->mQueueStatus == EventQueueStatus::Initialized);
+
+ // This method is called once we have served all shutdown clients. Now we
+ // flush the remaining IO queue. This ensures any straggling IO that was
+ // not part of the shutdown blocker finishes before we move to the next
+ // phase.
+ state->mEventQueue->Dispatch<Ok>([]() { return Ok{}; })
+ ->Then(GetMainThreadSerialEventTarget(), __func__,
+ [self = RefPtr(this)]() { self->OnFlush(); });
+
+ // And if we're the last shutdown phase to allow IO, disable the event
+ // queue to disallow further IO requests.
+ if (mPhase >= LAST_IO_PHASE) {
+ state->mQueueStatus = EventQueueStatus::Shutdown;
+ }
+
+ didFlush = true;
+ }
+ }
+
+ // If we have already shut down the event loop, then call OnFlush to stop
+ // blocking our parent shutdown client.
+ if (!didFlush) {
+ MOZ_RELEASE_ASSERT(mPhase == ShutdownPhase::XpcomWillShutdown);
+ OnFlush();
+ }
+
+ return NS_OK;
+}
+
+void IOUtilsShutdownBlocker::OnFlush() {
+ if (mParentClient) {
+ (void)NS_WARN_IF(NS_FAILED(mParentClient->RemoveBlocker(this)));
+ mParentClient = nullptr;
+
+ // If we are past the last shutdown phase that allows IO,
+ // we can shutdown the event queue here because no additional IO requests
+ // will be allowed (see |Done()|).
+ if (mPhase >= LAST_IO_PHASE) {
+ auto state = IOUtils::sState.Lock();
+ if (state->mEventQueue) {
+ state->mEventQueue = nullptr;
+ }
+ }
+ }
+}
+
+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.mMode = aOptions.mMode;
+
+ if (aOptions.mBackupFile.WasPassed()) {
+ opts.mBackupFile = new nsLocalFile();
+ if (nsresult rv = PathUtils::InitFileWithPath(opts.mBackupFile,
+ 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 = PathUtils::InitFileWithPath(opts.mTmpFile,
+ 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);
+ }
+
+ const char* ptr = aBuffer.mBuffer.get();
+ size_t length = aBuffer.mLength;
+
+ // Strip off a leading UTF-8 byte order marker (BOM) if found.
+ if (length >= 3 && Substring(ptr, 3) == "\xEF\xBB\xBF"_ns) {
+ ptr += 3;
+ length -= 3;
+ }
+
+ // If the string is encodable as Latin1, we need to deflate the string to a
+ // Latin1 string to account 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(ptr, length));
+}
+
+/* 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);
+ }
+
+ MOZ_RELEASE_ASSERT(aBuffer.mBuffer);
+ JS::Rooted<JSObject*> arrayBuffer(
+ aCx, JS::NewArrayBufferWithContents(aCx, aBuffer.mLength,
+ std::move(aBuffer.mBuffer)));
+
+ if (!arrayBuffer) {
+ // 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|.
+ return nullptr;
+ }
+
+ return JS_NewUint8ArrayWithBuffer(aCx, arrayBuffer, 0, aBuffer.mLength);
+}
+
+[[nodiscard]] 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;
+}
+
+// SyncReadFile
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(SyncReadFile)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(SyncReadFile)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SyncReadFile)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(SyncReadFile, mParent)
+
+SyncReadFile::SyncReadFile(nsISupports* aParent,
+ RefPtr<nsFileRandomAccessStream>&& aStream,
+ int64_t aSize)
+ : mParent(aParent), mStream(std::move(aStream)), mSize(aSize) {
+ MOZ_RELEASE_ASSERT(mSize >= 0);
+}
+
+SyncReadFile::~SyncReadFile() = default;
+
+JSObject* SyncReadFile::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return SyncReadFile_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void SyncReadFile::ReadBytesInto(const Uint8Array& aDestArray,
+ const int64_t aOffset, ErrorResult& aRv) {
+ if (!mStream) {
+ return aRv.ThrowOperationError("SyncReadFile is closed");
+ }
+
+ aDestArray.ProcessFixedData([&](const Span<uint8_t>& aData) {
+ auto rangeEnd = CheckedInt64(aOffset) + aData.Length();
+ if (!rangeEnd.isValid()) {
+ return aRv.ThrowOperationError("Requested range overflows i64");
+ }
+
+ if (rangeEnd.value() > mSize) {
+ return aRv.ThrowOperationError(
+ "Requested range overflows SyncReadFile size");
+ }
+
+ size_t readLen{aData.Length()};
+ if (readLen == 0) {
+ return;
+ }
+
+ if (nsresult rv = mStream->Seek(PR_SEEK_SET, aOffset); NS_FAILED(rv)) {
+ return aRv.ThrowOperationError(
+ FormatErrorMessage(rv, "Could not seek to position %lld", aOffset));
+ }
+
+ Span<char> toRead = AsWritableChars(aData);
+
+ size_t totalRead = 0;
+ while (totalRead != readLen) {
+ // Read no more than INT32_MAX on each call to mStream->Read,
+ // otherwise it returns an error.
+ uint32_t bytesToReadThisChunk =
+ std::min(readLen - totalRead, size_t(INT32_MAX));
+
+ uint32_t bytesRead = 0;
+ if (nsresult rv = mStream->Read(toRead.Elements(), bytesToReadThisChunk,
+ &bytesRead);
+ NS_FAILED(rv)) {
+ return aRv.ThrowOperationError(FormatErrorMessage(
+ rv, "Encountered an unexpected error while reading file stream"));
+ }
+ if (bytesRead == 0) {
+ return aRv.ThrowOperationError(
+ "Reading stopped before the entire array was filled");
+ }
+ totalRead += bytesRead;
+ toRead = toRead.From(bytesRead);
+ }
+ });
+}
+
+void SyncReadFile::Close() { mStream = nullptr; }
+
+#ifdef XP_UNIX
+namespace {
+
+static nsCString FromUnixString(const IOUtils::UnixString& aString) {
+ if (aString.IsUTF8String()) {
+ return aString.GetAsUTF8String();
+ }
+ if (aString.IsUint8Array()) {
+ nsCString data;
+ Unused << aString.GetAsUint8Array().AppendDataTo(data);
+ return data;
+ }
+ MOZ_CRASH("unreachable");
+}
+
+} // namespace
+
+// static
+uint32_t IOUtils::LaunchProcess(GlobalObject& aGlobal,
+ const Sequence<UnixString>& aArgv,
+ const LaunchOptions& aOptions,
+ ErrorResult& aRv) {
+ // The binding is worker-only, so should always be off-main-thread.
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ // This generally won't work in child processes due to sandboxing.
+ AssertParentProcessWithCallerLocation(aGlobal);
+
+ std::vector<std::string> argv;
+ base::LaunchOptions options;
+
+ for (const auto& arg : aArgv) {
+ argv.push_back(FromUnixString(arg).get());
+ }
+
+ size_t envLen = aOptions.mEnvironment.Length();
+ base::EnvironmentArray envp(new char*[envLen + 1]);
+ for (size_t i = 0; i < envLen; ++i) {
+ // EnvironmentArray is a UniquePtr instance which will `free`
+ // these strings.
+ envp[i] = strdup(FromUnixString(aOptions.mEnvironment[i]).get());
+ }
+ envp[envLen] = nullptr;
+ options.full_env = std::move(envp);
+
+ if (aOptions.mWorkdir.WasPassed()) {
+ options.workdir = FromUnixString(aOptions.mWorkdir.Value()).get();
+ }
+
+ if (aOptions.mFdMap.WasPassed()) {
+ for (const auto& fdItem : aOptions.mFdMap.Value()) {
+ options.fds_to_remap.push_back({fdItem.mSrc, fdItem.mDst});
+ }
+ }
+
+# ifdef XP_MACOSX
+ options.disclaim = aOptions.mDisclaim;
+# endif
+
+ base::ProcessHandle pid;
+ static_assert(sizeof(pid) <= sizeof(uint32_t),
+ "WebIDL long should be large enough for a pid");
+ Result<Ok, mozilla::ipc::LaunchError> err =
+ base::LaunchApp(argv, std::move(options), &pid);
+ if (err.isErr()) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return 0;
+ }
+
+ MOZ_ASSERT(pid >= 0);
+ return static_cast<uint32_t>(pid);
+}
+#endif // XP_UNIX
+
+} // namespace mozilla::dom
+
+#undef REJECT_IF_INIT_PATH_FAILED
diff --git a/dom/system/IOUtils.h b/dom/system/IOUtils.h
new file mode 100644
index 0000000000..82ea30eaa8
--- /dev/null
+++ b/dom/system/IOUtils.h
@@ -0,0 +1,925 @@
+/* -*- 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/StaticPtr.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/IOUtilsBinding.h"
+#include "mozilla/dom/TypedArray.h"
+#include "nsIAsyncShutdown.h"
+#include "nsIFile.h"
+#include "nsISerialEventTarget.h"
+#include "nsPrintfCString.h"
+#include "nsProxyRelease.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "prio.h"
+
+class nsFileRandomAccessStream;
+
+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;
+
+ enum class ShutdownPhase : uint8_t {
+ ProfileBeforeChange,
+ SendTelemetry,
+ XpcomWillShutdown,
+ Count,
+ };
+
+ template <typename T>
+ using PhaseArray =
+ EnumeratedArray<IOUtils::ShutdownPhase, IOUtils::ShutdownPhase::Count, T>;
+
+ static already_AddRefed<Promise> Read(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const ReadOptions& aOptions,
+ ErrorResult& aError);
+
+ static already_AddRefed<Promise> ReadUTF8(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const ReadUTF8Options& aOptions,
+ ErrorResult& aError);
+
+ static already_AddRefed<Promise> ReadJSON(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const ReadUTF8Options& aOptions,
+ ErrorResult& aError);
+
+ static already_AddRefed<Promise> Write(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const Uint8Array& aData,
+ const WriteOptions& aOptions,
+ ErrorResult& aError);
+
+ static already_AddRefed<Promise> WriteUTF8(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const nsACString& aString,
+ const WriteOptions& aOptions,
+ ErrorResult& aError);
+
+ static already_AddRefed<Promise> WriteJSON(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ JS::Handle<JS::Value> aValue,
+ const WriteOptions& aOptions,
+ ErrorResult& aError);
+
+ static already_AddRefed<Promise> Move(GlobalObject& aGlobal,
+ const nsAString& aSourcePath,
+ const nsAString& aDestPath,
+ const MoveOptions& aOptions,
+ ErrorResult& aError);
+
+ static already_AddRefed<Promise> Remove(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const RemoveOptions& aOptions,
+ ErrorResult& aError);
+
+ static already_AddRefed<Promise> MakeDirectory(
+ GlobalObject& aGlobal, const nsAString& aPath,
+ const MakeDirectoryOptions& aOptions, ErrorResult& aError);
+
+ static already_AddRefed<Promise> Stat(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ ErrorResult& aError);
+
+ static already_AddRefed<Promise> Copy(GlobalObject& aGlobal,
+ const nsAString& aSourcePath,
+ const nsAString& aDestPath,
+ const CopyOptions& aOptions,
+ ErrorResult& aError);
+
+ static already_AddRefed<Promise> SetAccessTime(
+ GlobalObject& aGlobal, const nsAString& aPath,
+ const Optional<int64_t>& aAccess, ErrorResult& aError);
+
+ static already_AddRefed<Promise> SetModificationTime(
+ GlobalObject& aGlobal, const nsAString& aPath,
+ const Optional<int64_t>& aModification, ErrorResult& aError);
+
+ private:
+ using SetTimeFn = decltype(&nsIFile::SetLastAccessedTime);
+
+ static_assert(
+ std::is_same_v<SetTimeFn, decltype(&nsIFile::SetLastModifiedTime)>);
+
+ static already_AddRefed<Promise> SetTime(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const Optional<int64_t>& aNewTime,
+ SetTimeFn aSetTimeFn,
+ ErrorResult& aError);
+
+ public:
+ static already_AddRefed<Promise> GetChildren(
+ GlobalObject& aGlobal, const nsAString& aPath,
+ const GetChildrenOptions& aOptions, ErrorResult& aError);
+
+ static already_AddRefed<Promise> SetPermissions(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ uint32_t aPermissions,
+ const bool aHonorUmask,
+ ErrorResult& aError);
+
+ static already_AddRefed<Promise> Exists(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ ErrorResult& aError);
+
+ static already_AddRefed<Promise> CreateUniqueFile(GlobalObject& aGlobal,
+ const nsAString& aParent,
+ const nsAString& aPrefix,
+ const uint32_t aPermissions,
+ ErrorResult& aError);
+ static already_AddRefed<Promise> CreateUniqueDirectory(
+ GlobalObject& aGlobal, const nsAString& aParent, const nsAString& aPrefix,
+ const uint32_t aPermissions, ErrorResult& aError);
+
+ private:
+ /**
+ * A helper method for CreateUniqueFile and CreateUniqueDirectory.
+ */
+ static already_AddRefed<Promise> CreateUnique(GlobalObject& aGlobal,
+ const nsAString& aParent,
+ const nsAString& aPrefix,
+ const uint32_t aFileType,
+ const uint32_t aPermissions,
+ ErrorResult& aError);
+
+ public:
+ static already_AddRefed<Promise> ComputeHexDigest(
+ GlobalObject& aGlobal, const nsAString& aPath,
+ const HashAlgorithm aAlgorithm, ErrorResult& aError);
+
+#if defined(XP_WIN)
+ static already_AddRefed<Promise> GetWindowsAttributes(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ ErrorResult& aError);
+
+ static already_AddRefed<Promise> SetWindowsAttributes(
+ GlobalObject& aGlobal, const nsAString& aPath,
+ const mozilla::dom::WindowsFileAttributes& aAttrs, ErrorResult& aError);
+#elif defined(XP_MACOSX)
+ static already_AddRefed<Promise> HasMacXAttr(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const nsACString& aAttr,
+ ErrorResult& aError);
+ static already_AddRefed<Promise> GetMacXAttr(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const nsACString& aAttr,
+ ErrorResult& aError);
+ static already_AddRefed<Promise> SetMacXAttr(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const nsACString& aAttr,
+ const Uint8Array& aValue,
+ ErrorResult& aError);
+ static already_AddRefed<Promise> DelMacXAttr(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const nsACString& aAttr,
+ ErrorResult& aError);
+#endif
+
+#ifdef XP_UNIX
+ using UnixString = OwningUTF8StringOrUint8Array;
+ static uint32_t LaunchProcess(GlobalObject& aGlobal,
+ const Sequence<UnixString>& aArgv,
+ const LaunchOptions& aOptions,
+ ErrorResult& aRv);
+#endif
+
+ static already_AddRefed<Promise> GetFile(
+ GlobalObject& aGlobal, const Sequence<nsString>& aComponents,
+ ErrorResult& aError);
+
+ static already_AddRefed<Promise> GetDirectory(
+ GlobalObject& aGlobal, const Sequence<nsString>& aComponents,
+ ErrorResult& aError);
+
+ static void GetProfileBeforeChange(GlobalObject& aGlobal,
+ JS::MutableHandle<JS::Value>,
+ ErrorResult& aRv);
+
+ static void GetSendTelemetry(GlobalObject& aGlobal,
+ JS::MutableHandle<JS::Value>, ErrorResult& aRv);
+
+ static RefPtr<SyncReadFile> OpenFileForSyncReading(GlobalObject& aGlobal,
+ const nsAString& aPath,
+ ErrorResult& aRv);
+
+ 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;
+ class EventQueue;
+ class State;
+
+ template <typename Fn>
+ static already_AddRefed<Promise> WithPromiseAndState(GlobalObject& aGlobal,
+ ErrorResult& aError,
+ Fn aFn);
+
+ /**
+ * Dispatch a task on the event queue and resolve or reject the associated
+ * promise based on the result.
+ *
+ * NB: If the calling thread is a worker, this function takes care of keepting
+ * it alive until the |IOPromise| can complete.
+ *
+ * @param aPromise The promise corresponding to the task running on the event
+ * queue.
+ * @param aFunc The task to run.
+ */
+ template <typename OkT, typename Fn>
+ static void DispatchAndResolve(EventQueue* aQueue, Promise* aPromise,
+ Fn aFunc);
+
+ /**
+ * Creates a new JS Promise.
+ *
+ * @return The new promise, or |nullptr| on failure.
+ */
+ static already_AddRefed<Promise> CreateJSPromise(GlobalObject& aGlobal,
+ ErrorResult& aError);
+
+ // Allow conversion of |InternalFileInfo| with |ToJSValue|.
+ friend bool ToJSValue(JSContext* aCx,
+ const InternalFileInfo& aInternalFileInfo,
+ JS::MutableHandle<JS::Value> aValue);
+
+ /**
+ * Attempts to read the entire file at |aPath| into a buffer.
+ *
+ * @param aFile The location of the file.
+ * @param aOffset The offset to start reading from.
+ * @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 uint64_t aOffset,
+ 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 aNoOverWrite 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.
+ * @param aRetryReadonly Retry a delete that failed with a NotAllowedError by
+ * first removing the readonly attribute. Only has an
+ * effect on Windows.
+ *
+ * @return Ok if the file was removed successfully, or an error.
+ */
+ static Result<Ok, IOError> RemoveSync(nsIFile* aFile, bool aIgnoreAbsent,
+ bool aRecursive, bool aRetryReadonly);
+
+ /**
+ * 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 access or modification time of the file at
+ * |aFile|.
+ *
+ * @param aFile The location of the file.
+ * @param SetTimeFn A member function pointer to either
+ * nsIFile::SetLastAccessedTime or
+ * nsIFile::SetLastModifiedTime.
+ * @param aNewTime Some value in milliseconds since Epoch.
+ *
+ * @return Timestamp of the file if the operation was successful, or an error.
+ */
+ static Result<int64_t, IOError> SetTimeSync(nsIFile* aFile,
+ SetTimeFn aSetTimeFn,
+ int64_t aNewTime);
+
+ /**
+ * 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, bool aIgnoreAbsent);
+
+ /**
+ * 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);
+
+ /**
+ * Create a file or directory with a unique path.
+ *
+ * @param aFile The location of the file or directory (including prefix)
+ * @param aFileType One of |nsIFile::NORMAL_FILE_TYPE| or
+ * |nsIFile::DIRECTORY_TYPE|.
+ * @param aperms The permissions to create the file or directory with.
+ *
+ * @return A unique path.
+ */
+ static Result<nsString, IOError> CreateUniqueSync(
+ nsIFile* aFile, const uint32_t aFileType, const uint32_t aPermissions);
+
+ /**
+ * Compute the hash of a file.
+ *
+ * @param aFile The file to hash.
+ * @param aAlgorithm The hashing algorithm to use.
+ *
+ * @return The hash of the file, as a hex digest.
+ */
+ static Result<nsCString, IOError> ComputeHexDigestSync(
+ nsIFile* aFile, const HashAlgorithm aAlgorithm);
+
+#if defined(XP_WIN)
+ /**
+ * Return the Windows-specific attributes of the file.
+ *
+ * @param aFile The location of the file.
+ *
+ * @return The Windows-specific attributes of the file.
+ */
+ static Result<uint32_t, IOError> GetWindowsAttributesSync(nsIFile* aFile);
+
+ /**
+ * Set the Windows-specific attributes of the file.
+ *
+ * @param aFile The location of the file.
+ * @param aAttrs The attributes to set on the file.
+ *
+ * @return |Ok| if the attributes were successfully set, or an error.
+ */
+ static Result<Ok, IOError> SetWindowsAttributesSync(
+ nsIFile* aFile, const uint32_t aSetAttrs, const uint32_t aClearAttrs);
+#elif defined(XP_MACOSX)
+ static Result<bool, IOError> HasMacXAttrSync(nsIFile* aFile,
+ const nsCString& aAttr);
+ static Result<nsTArray<uint8_t>, IOError> GetMacXAttrSync(
+ nsIFile* aFile, const nsCString& aAttr);
+ static Result<Ok, IOError> SetMacXAttrSync(nsIFile* aFile,
+ const nsCString& aAttr,
+ const nsTArray<uint8_t>& aValue);
+ static Result<Ok, IOError> DelMacXAttrSync(nsIFile* aFile,
+ const nsCString& aAttr);
+#endif
+
+ static void GetShutdownClient(GlobalObject& aGlobal,
+ JS::MutableHandle<JS::Value> aClient,
+ ErrorResult& aRv, const ShutdownPhase aPhase);
+
+ enum class EventQueueStatus {
+ Uninitialized,
+ Initialized,
+ Shutdown,
+ };
+
+ enum class ShutdownBlockerStatus {
+ Uninitialized,
+ Initialized,
+ Failed,
+ };
+
+ /**
+ * Internal IOUtils state.
+ */
+ class State {
+ public:
+ StaticAutoPtr<EventQueue> mEventQueue;
+ EventQueueStatus mQueueStatus = EventQueueStatus::Uninitialized;
+ ShutdownBlockerStatus mBlockerStatus = ShutdownBlockerStatus::Uninitialized;
+
+ /**
+ * Set up shutdown hooks to free our internals at shutdown.
+ *
+ * NB: Must be called on main thread.
+ */
+ void SetShutdownHooks();
+ };
+
+ using StateMutex = StaticDataMutex<State>;
+
+ /**
+ * Lock the state mutex and return a handle. If shutdown has not yet
+ * finished, the internals will be constructed if necessary.
+ *
+ * @returns A handle to the internal state, which can be used to retrieve the
+ * event queue.
+ * If |Some| is returned, |mEventQueue| is guaranteed to be
+ * initialized. If shutdown has finished, |Nothing| is returned.
+ */
+ static Maybe<StateMutex::AutoLock> GetState();
+
+ static StateMutex sState;
+};
+
+/**
+ * The IOUtils event queue.
+ */
+class IOUtils::EventQueue final {
+ friend void IOUtils::State::SetShutdownHooks();
+
+ public:
+ EventQueue();
+
+ EventQueue(const EventQueue&) = delete;
+ EventQueue(EventQueue&&) = delete;
+ EventQueue& operator=(const EventQueue&) = delete;
+ EventQueue& operator=(EventQueue&&) = delete;
+
+ /**
+ * Dispatch a task on the event queue.
+ *
+ * NB: If using this directly from |IOUtils| instead of
+ * |IOUtils::DispatchAndResolve| *and* the calling thread is a worker, you
+ * *must* take care to keep the worker thread alive until the |IOPromise|
+ * resolves or rejects. See the implementation of
+ * |IOUtils::DispatchAndResolve| or |IOUtils::GetWindowsAttributes| for an
+ * example.
+ *
+ * @param aFunc The task to dispatch on the event queue.
+ *
+ * @return A promise that resolves to the task's return value or rejects with
+ * an error.
+ */
+ template <typename OkT, typename Fn>
+ RefPtr<IOPromise<OkT>> Dispatch(Fn aFunc);
+
+ Result<already_AddRefed<nsIAsyncShutdownBarrier>, nsresult>
+ GetShutdownBarrier(const ShutdownPhase aPhase);
+ Result<already_AddRefed<nsIAsyncShutdownClient>, nsresult> GetShutdownClient(
+ const ShutdownPhase aPhase);
+
+ private:
+ nsresult SetShutdownHooks();
+
+ nsCOMPtr<nsISerialEventTarget> mBackgroundEventTarget;
+ IOUtils::PhaseArray<nsCOMPtr<nsIAsyncShutdownBarrier>> mBarriers;
+};
+
+/**
+ * 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;
+ Maybe<PRTime> mCreationTime; // In ms since epoch.
+ PRTime mLastAccessed = 0; // In ms since epoch.
+ PRTime mLastModified = 0; // In ms since epoch.
+ 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;
+ WriteMode mMode;
+ bool mFlush = 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 nsIAsyncShutdownCompletionCallback {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIASYNCSHUTDOWNBLOCKER
+ NS_DECL_NSIASYNCSHUTDOWNCOMPLETIONCALLBACK
+
+ explicit IOUtilsShutdownBlocker(const IOUtils::ShutdownPhase aPhase)
+ : mPhase(aPhase) {}
+
+ private:
+ virtual ~IOUtilsShutdownBlocker() = default;
+
+ /**
+ * Called on the main thread after the event queue has been flushed.
+ */
+ void OnFlush();
+
+ static constexpr IOUtils::PhaseArray<const char16_t*> PHASE_NAMES{
+ u"profile-before-change",
+ u"profile-before-change-telemetry",
+ u"xpcom-will-shutdown",
+ };
+
+ // The last shutdown phase before we should shut down the event loop.
+ static constexpr auto LAST_IO_PHASE = IOUtils::ShutdownPhase::SendTelemetry;
+
+ IOUtils::ShutdownPhase mPhase;
+ nsCOMPtr<nsIAsyncShutdownClient> mParentClient;
+};
+
+/**
+ * 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 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);
+};
+
+class SyncReadFile : public nsISupports, public nsWrapperCache {
+ public:
+ SyncReadFile(nsISupports* aParent, RefPtr<nsFileRandomAccessStream>&& aStream,
+ int64_t aSize);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(SyncReadFile)
+
+ nsISupports* GetParentObject() const { return mParent; }
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ int64_t Size() const { return mSize; }
+ void ReadBytesInto(const Uint8Array&, const int64_t, ErrorResult& aRv);
+ void Close();
+
+ private:
+ virtual ~SyncReadFile();
+
+ nsCOMPtr<nsISupports> mParent;
+ RefPtr<nsFileRandomAccessStream> mStream;
+ int64_t mSize = 0;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
diff --git a/dom/system/NetworkGeolocationProvider.sys.mjs b/dom/system/NetworkGeolocationProvider.sys.mjs
new file mode 100644
index 0000000000..1bee69a282
--- /dev/null
+++ b/dom/system/NetworkGeolocationProvider.sys.mjs
@@ -0,0 +1,504 @@
+/* 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ LocationHelper: "resource://gre/modules/LocationHelper.sys.mjs",
+ clearTimeout: "resource://gre/modules/Timer.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+// GeolocationPositionError has no interface object, so we can't use that here.
+const POSITION_UNAVAILABLE = 2;
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "gLoggingEnabled",
+ "geo.provider.network.logging.enabled",
+ false
+);
+
+function LOG(aMsg) {
+ if (lazy.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"]),
+};
+
+export 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
+ );
+
+ this.wifiService = null;
+ this.timer = null;
+ this.started = false;
+}
+
+NetworkGeolocationProvider.prototype = {
+ classID: Components.ID("{77DA64D3-7458-4920-9491-86CC9914F904}"),
+ name: "NetworkGeolocationProvider",
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIGeolocationProvider",
+ "nsIWifiListener",
+ "nsITimerCallback",
+ "nsIObserver",
+ "nsINamed",
+ ]),
+ 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, false);
+ }
+
+ 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) {
+ // Mochitest wants to check this value
+ if (Services.prefs.getBoolPref("geo.provider.testing")) {
+ Services.obs.notifyObservers(
+ null,
+ "testing-geolocation-high-accuracy",
+ enable
+ );
+ }
+ },
+
+ onChange(accessPoints) {
+ // we got some wifi data, rearm the timer.
+ this.resetTimer();
+
+ let wifiData = null;
+ if (accessPoints) {
+ wifiData = lazy.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);
+ console.error(err);
+ if (err.name == "AbortError") {
+ this.onStatus(true, "xhr-timeout");
+ } else {
+ this.onStatus(true, "xhr-error");
+ }
+ }
+ },
+
+ 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 = lazy.setTimeout(
+ () => fetchController.abort(),
+ Services.prefs.getIntPref("geo.provider.network.timeout")
+ );
+
+ let req = await fetch(url, fetchOpts);
+ lazy.clearTimeout(timeoutId);
+ let result = req.json();
+ return result;
+ },
+};
diff --git a/dom/system/PathUtils.cpp b/dom/system/PathUtils.cpp
new file mode 100644
index 0000000000..24d7b648db
--- /dev/null
+++ b/dom/system/PathUtils.cpp
@@ -0,0 +1,635 @@
+/* -*- 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/ResultExtensions.h"
+#include "mozilla/Span.h"
+#include "mozilla/Try.h"
+#include "mozilla/dom/DOMParser.h"
+#include "mozilla/dom/PathUtilsBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsCOMPtr.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsIFile.h"
+#include "nsIGlobalObject.h"
+#include "nsLocalFile.h"
+#include "nsNetUtil.h"
+#include "nsString.h"
+#include "nsURLHelper.h"
+#include "xpcpublic.h"
+
+namespace mozilla::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 COLON = ": "_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(COLON);
+ 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;
+ }
+}
+
+static bool DoWindowsPathCheck() {
+#ifdef XP_WIN
+# ifdef DEBUG
+ return true;
+# else // DEBUG
+ return xpc::IsInAutomation();
+# endif // DEBUG
+#else // XP_WIN
+ return false;
+#endif // XP_WIN
+}
+
+/* static */
+nsresult PathUtils::InitFileWithPath(nsIFile* aFile, const nsAString& aPath) {
+ if (DoWindowsPathCheck()) {
+ MOZ_RELEASE_ASSERT(!aPath.Contains(u'/'),
+ "Windows paths cannot include forward slashes");
+ }
+
+ MOZ_ASSERT(aFile);
+ return aFile->InitWithPath(aPath);
+}
+
+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 = InitFileWithPath(path, 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,
+ const int32_t aDepth, nsString& aResult,
+ ErrorResult& aErr) {
+ if (aPath.IsEmpty()) {
+ aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH);
+ return;
+ }
+
+ nsCOMPtr<nsIFile> path = new nsLocalFile();
+ if (nsresult rv = InitFileWithPath(path, aPath); NS_FAILED(rv)) {
+ ThrowError(aErr, rv, ERROR_INITIALIZE_PATH);
+ return;
+ }
+
+ if (aDepth <= 0) {
+ aErr.ThrowNotSupportedError("A depth of at least 1 is required");
+ return;
+ }
+
+ nsCOMPtr<nsIFile> parent;
+ for (int32_t i = 0; path && i < aDepth; i++) {
+ if (nsresult rv = path->GetParent(getter_AddRefs(parent)); NS_FAILED(rv)) {
+ ThrowError(aErr, rv, ERROR_GET_PARENT);
+ return;
+ }
+ path = parent;
+ }
+
+ if (parent) {
+ MOZ_ALWAYS_SUCCEEDS(parent->GetPath(aResult));
+ } else {
+ aResult = VoidString();
+ }
+}
+
+void PathUtils::Join(const GlobalObject&, const Sequence<nsString>& aComponents,
+ nsString& aResult, ErrorResult& aErr) {
+ nsCOMPtr<nsIFile> path = Join(Span(aComponents), aErr);
+ if (aErr.Failed()) {
+ return;
+ }
+
+ MOZ_ALWAYS_SUCCEEDS(path->GetPath(aResult));
+}
+
+already_AddRefed<nsIFile> PathUtils::Join(
+ const Span<const nsString>& aComponents, ErrorResult& aErr) {
+ if (aComponents.IsEmpty() || aComponents[0].IsEmpty()) {
+ aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH);
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIFile> path = new nsLocalFile();
+ if (nsresult rv = InitFileWithPath(path, aComponents[0]); NS_FAILED(rv)) {
+ ThrowError(aErr, rv, ERROR_INITIALIZE_PATH);
+ return nullptr;
+ }
+
+ 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 nullptr;
+ }
+ }
+
+ return path.forget();
+}
+
+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 = InitFileWithPath(path, 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::ToExtendedWindowsPath(const GlobalObject&,
+ const nsAString& aPath, nsString& aResult,
+ ErrorResult& aErr) {
+#ifndef XP_WIN
+ aErr.ThrowNotAllowedError("Operation is windows specific"_ns);
+ return;
+#else
+ if (aPath.IsEmpty()) {
+ aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH);
+ return;
+ }
+
+ const nsAString& str1 = Substring(aPath, 1, 1);
+ const nsAString& str2 = Substring(aPath, 2, aPath.Length() - 2);
+
+ bool isUNC = aPath.Length() >= 2 &&
+ (aPath.First() == '\\' || aPath.First() == '/') &&
+ (str1.EqualsLiteral("\\") || str1.EqualsLiteral("/"));
+
+ constexpr auto pathPrefix = u"\\\\?\\"_ns;
+ const nsAString& uncPath = pathPrefix + u"UNC\\"_ns + str2;
+ const nsAString& normalPath = pathPrefix + aPath;
+
+ nsCOMPtr<nsIFile> path = new nsLocalFile();
+ if (nsresult rv = InitFileWithPath(path, isUNC ? uncPath : normalPath);
+ NS_FAILED(rv)) {
+ ThrowError(aErr, rv, ERROR_INITIALIZE_PATH);
+ return;
+ }
+
+ MOZ_ALWAYS_SUCCEEDS(path->GetPath(aResult));
+#endif
+}
+
+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 = InitFileWithPath(path, 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 = InitFileWithPath(path, 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::SplitRelative(const GlobalObject& aGlobal,
+ const nsAString& aPath,
+ const SplitRelativeOptions& aOptions,
+ nsTArray<nsString>& aResult, ErrorResult& aErr) {
+ if (aPath.IsEmpty()) {
+ aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH);
+ return;
+ }
+
+ if (DoWindowsPathCheck()) {
+ MOZ_RELEASE_ASSERT(!aPath.Contains(u'/'),
+ "Windows paths cannot include forward slashes");
+ }
+
+ if (IsAbsolute(aGlobal, aPath)) {
+ aErr.ThrowNotAllowedError(
+ "PathUtils.splitRelative requires a relative path"_ns);
+ return;
+ }
+
+#ifdef XP_WIN
+ constexpr auto SEPARATOR = u'\\';
+#else
+ constexpr auto SEPARATOR = u'/';
+#endif
+
+ constexpr auto PARENT = u".."_ns;
+ constexpr auto CURRENT = u"."_ns;
+
+ for (const nsAString& pathComponent :
+ nsCharSeparatedTokenizerTemplate<NS_TokenizerIgnoreNothing>{aPath,
+ SEPARATOR}
+ .ToRange()) {
+ if (!aOptions.mAllowEmpty && pathComponent.IsEmpty()) {
+ aErr.ThrowNotAllowedError(
+ "PathUtils.splitRelative: Empty directory components (\"\") not "
+ "allowed by options");
+ return;
+ }
+
+ if (!aOptions.mAllowParentDir && pathComponent == PARENT) {
+ aErr.ThrowNotAllowedError(
+ "PathUtils.splitRelative: Parent directory components (\"..\") not "
+ "allowed by options");
+ return;
+ }
+
+ if (!aOptions.mAllowCurrentDir && pathComponent == CURRENT) {
+ aErr.ThrowNotAllowedError(
+ "PathUtils.splitRelative: Current directory components (\".\") not "
+ "allowed by options");
+ return;
+ }
+
+ aResult.AppendElement(pathComponent);
+ }
+}
+
+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 = InitFileWithPath(path, aPath); NS_FAILED(rv)) {
+ ThrowError(aErr, rv, ERROR_INITIALIZE_PATH);
+ return;
+ }
+
+ if (nsresult rv = net_GetURLSpecFromActualFile(path, aResult);
+ NS_FAILED(rv)) {
+ ThrowError(aErr, rv, "Could not retrieve URI spec"_ns);
+ return;
+ }
+}
+
+bool PathUtils::IsAbsolute(const GlobalObject&, const nsAString& aPath) {
+ nsCOMPtr<nsIFile> path = new nsLocalFile();
+ nsresult rv = InitFileWithPath(path, aPath);
+ return NS_SUCCEEDED(rv);
+}
+
+void PathUtils::GetProfileDirSync(const GlobalObject&, nsString& aResult,
+ ErrorResult& aErr) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ auto guard = sDirCache.Lock();
+ DirectoryCache::Ensure(guard.ref())
+ .GetDirectorySync(aResult, aErr, DirectoryCache::Directory::Profile);
+}
+void PathUtils::GetLocalProfileDirSync(const GlobalObject&, nsString& aResult,
+ ErrorResult& aErr) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ auto guard = sDirCache.Lock();
+ DirectoryCache::Ensure(guard.ref())
+ .GetDirectorySync(aResult, aErr, DirectoryCache::Directory::LocalProfile);
+}
+void PathUtils::GetTempDirSync(const GlobalObject&, nsString& aResult,
+ ErrorResult& aErr) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ auto guard = sDirCache.Lock();
+ DirectoryCache::Ensure(guard.ref())
+ .GetDirectorySync(aResult, aErr, DirectoryCache::Directory::Temp);
+}
+
+void PathUtils::GetXulLibraryPathSync(const GlobalObject&, nsString& aResult,
+ ErrorResult& aErr) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ auto guard = sDirCache.Lock();
+ DirectoryCache::Ensure(guard.ref())
+ .GetDirectorySync(aResult, aErr, DirectoryCache::Directory::XulLibrary);
+}
+
+already_AddRefed<Promise> PathUtils::GetProfileDirAsync(
+ const GlobalObject& aGlobal, ErrorResult& aErr) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ auto guard = sDirCache.Lock();
+ return DirectoryCache::Ensure(guard.ref())
+ .GetDirectoryAsync(aGlobal, aErr, DirectoryCache::Directory::Profile);
+}
+
+already_AddRefed<Promise> PathUtils::GetLocalProfileDirAsync(
+ const GlobalObject& aGlobal, ErrorResult& aErr) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ auto guard = sDirCache.Lock();
+ return DirectoryCache::Ensure(guard.ref())
+ .GetDirectoryAsync(aGlobal, aErr,
+ DirectoryCache::Directory::LocalProfile);
+}
+
+already_AddRefed<Promise> PathUtils::GetTempDirAsync(
+ const GlobalObject& aGlobal, ErrorResult& aErr) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ auto guard = sDirCache.Lock();
+ return DirectoryCache::Ensure(guard.ref())
+ .GetDirectoryAsync(aGlobal, aErr, DirectoryCache::Directory::Temp);
+}
+
+already_AddRefed<Promise> PathUtils::GetXulLibraryPathAsync(
+ const GlobalObject& aGlobal, ErrorResult& aErr) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ auto guard = sDirCache.Lock();
+ return DirectoryCache::Ensure(guard.ref())
+ .GetDirectoryAsync(aGlobal, aErr, DirectoryCache::Directory::XulLibrary);
+}
+
+PathUtils::DirectoryCache::DirectoryCache() {
+ for (auto& dir : mDirectories) {
+ dir.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();
+}
+
+void PathUtils::DirectoryCache::GetDirectorySync(
+ nsString& aResult, ErrorResult& aErr, const Directory aRequestedDir) {
+ MOZ_RELEASE_ASSERT(aRequestedDir < Directory::Count);
+
+ if (nsresult rv = PopulateDirectoriesImpl(aRequestedDir); NS_FAILED(rv)) {
+ nsAutoCStringN<32> errorName;
+ GetErrorName(rv, errorName);
+
+ nsAutoCStringN<256> msg;
+ msg.Append("Could not retrieve directory "_ns);
+ msg.Append(kDirectoryNames[aRequestedDir]);
+ msg.Append(COLON);
+ msg.Append(errorName);
+
+ aErr.ThrowUnknownError(msg);
+ return;
+ }
+
+ aResult = mDirectories[aRequestedDir];
+}
+
+already_AddRefed<Promise> PathUtils::DirectoryCache::GetDirectoryAsync(
+ 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) {
+ MOZ_RELEASE_ASSERT(aRequestedDir < Directory::Count);
+ MOZ_RELEASE_ASSERT(!mDirectories[aRequestedDir].IsVoid());
+ aPromise->MaybeResolve(mDirectories[aRequestedDir]);
+}
+
+already_AddRefed<PathUtils::DirectoryCache::PopulateDirectoriesPromise>
+PathUtils::DirectoryCache::PopulateDirectories(
+ const PathUtils::DirectoryCache::Directory aRequestedDir) {
+ MOZ_RELEASE_ASSERT(aRequestedDir < Directory::Count);
+
+ // If we have already resolved the requested directory, we can return
+ // immediately.
+ // Otherwise, if 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 (!mDirectories[aRequestedDir].IsVoid()) {
+ return nullptr;
+ }
+ if (!mPromises[aRequestedDir].IsEmpty()) {
+ return mPromises[aRequestedDir].Ensure(__func__);
+ }
+
+ RefPtr<PopulateDirectoriesPromise> promise =
+ mPromises[aRequestedDir].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) {
+ MOZ_RELEASE_ASSERT(aRequestedDir < Directory::Count);
+
+ if (NS_SUCCEEDED(aRv)) {
+ mPromises[aRequestedDir].Resolve(Ok{}, __func__);
+ } else {
+ mPromises[aRequestedDir].Reject(aRv, __func__);
+ }
+}
+
+nsresult PathUtils::DirectoryCache::PopulateDirectoriesImpl(
+ const PathUtils::DirectoryCache::Directory aRequestedDir) {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+ MOZ_RELEASE_ASSERT(aRequestedDir < Directory::Count);
+
+ if (!mDirectories[aRequestedDir].IsVoid()) {
+ // In between when this promise was dispatched to the main thread and now,
+ // the directory cache has had this entry populated (via the
+ // on-main-thread sync method).
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIFile> path;
+
+ MOZ_TRY(NS_GetSpecialDirectory(kDirectoryNames[aRequestedDir],
+ getter_AddRefs(path)));
+ MOZ_TRY(path->GetPath(mDirectories[aRequestedDir]));
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/system/PathUtils.h b/dom/system/PathUtils.h
new file mode 100644
index 0000000000..ff01ddfc1e
--- /dev/null
+++ b/dom/system/PathUtils.h
@@ -0,0 +1,260 @@
+/* -*- 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/EnumeratedArray.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/Result.h"
+#include "mozilla/dom/PathUtilsBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class PathUtils final {
+ public:
+ /**
+ * Initialize the given nsIFile with the given path.
+ *
+ * This is equivalent to calling nsIFile::InitWithPath() with the caveat that
+ * on Windows debug or during Windows CI tests, we will crash if the path
+ * contains a forward slash.
+ *
+ * @param aFile The file to initialize.
+ * @param aPath The path to initialize the file with.
+ *
+ * @return The result of calling nsIFile::InitWithPath.
+ */
+ static nsresult InitFileWithPath(nsIFile* aFile, const nsAString& aPath);
+
+ static void Filename(const GlobalObject&, const nsAString& aPath,
+ nsString& aResult, ErrorResult& aErr);
+
+ static void Parent(const GlobalObject&, const nsAString& aPath,
+ const int32_t aDepth, nsString& aResult,
+ ErrorResult& aErr);
+
+ static void Join(const GlobalObject&, const Sequence<nsString>& aComponents,
+ nsString& aResult, ErrorResult& aErr);
+
+ /**
+ * Join a sequence of path components and return an nsIFile with the resulting
+ * path.
+ *
+ * @param aComponents A sequence of path components. The first component must
+ * be an absolute path.
+ * @param aErr The error result, if any.
+ *
+ * @return An nsIFile with the resulting path, if there were no errors.
+ * Otherwise, nullptr is returned.
+ */
+ static already_AddRefed<nsIFile> Join(const Span<const nsString>& aComponents,
+ ErrorResult& aErr);
+
+ static void JoinRelative(const GlobalObject&, const nsAString& aBasePath,
+ const nsAString& aRelativePath, nsString& aResult,
+ ErrorResult& aErr);
+
+ static void ToExtendedWindowsPath(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 SplitRelative(const GlobalObject& aGlobal, const nsAString& aPath,
+ const SplitRelativeOptions& aOptions,
+ nsTArray<nsString>& aResult, ErrorResult& aErr);
+
+ static void ToFileURI(const GlobalObject&, const nsAString& aPath,
+ nsCString& aResult, ErrorResult& aErr);
+
+ static bool IsAbsolute(const GlobalObject&, const nsAString& aPath);
+
+ static void GetProfileDirSync(const GlobalObject&, nsString& aResult,
+ ErrorResult& aErr);
+ static void GetLocalProfileDirSync(const GlobalObject&, nsString& aResult,
+ ErrorResult& aErr);
+ static void GetTempDirSync(const GlobalObject&, nsString& aResult,
+ ErrorResult& aErr);
+ static void GetXulLibraryPathSync(const GlobalObject&, nsString& aResult,
+ ErrorResult& aErr);
+
+ static already_AddRefed<Promise> GetProfileDirAsync(
+ const GlobalObject& aGlobal, ErrorResult& aErr);
+ static already_AddRefed<Promise> GetLocalProfileDirAsync(
+ const GlobalObject& aGlobal, ErrorResult& aErr);
+ static already_AddRefed<Promise> GetTempDirAsync(const GlobalObject& aGlobal,
+ ErrorResult& aErr);
+ static already_AddRefed<Promise> GetXulLibraryPathAsync(
+ 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 |GetDirectorySync| or
+ * |GetDirectoryAsync|.
+ */
+ enum class Directory {
+ /**
+ * The user's profile directory.
+ */
+ Profile,
+ /**
+ * The user's local profile directory.
+ */
+ LocalProfile,
+ /**
+ * The OS temporary directory.
+ */
+ Temp,
+ /**
+ * The libxul path.
+ */
+ XulLibrary,
+ /**
+ * The number of Directory entries.
+ */
+ Count,
+ };
+
+ 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);
+
+ void GetDirectorySync(nsString& aResult, ErrorResult& aErr,
+ const Directory aRequestedDir);
+
+ /**
+ * 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> GetDirectoryAsync(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);
+
+ template <typename T>
+ using DirectoryArray = EnumeratedArray<Directory, Directory::Count, T>;
+
+ DirectoryArray<nsString> mDirectories;
+ DirectoryArray<MozPromiseHolder<PopulateDirectoriesPromise>> mPromises;
+
+ static constexpr DirectoryArray<const char*> kDirectoryNames{
+ NS_APP_USER_PROFILE_50_DIR,
+ NS_APP_USER_PROFILE_LOCAL_50_DIR,
+ NS_OS_TEMP_DIR,
+ NS_XPCOM_LIBRARY_FILE,
+ };
+};
+
+} // 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..8861c30298
--- /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::EnableLocationUpdates(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::EnableLocationUpdates(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..82bcb2cb6f
--- /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',
+ ],
+ 'esModule': 'resource://gre/modules/NetworkGeolocationProvider.sys.mjs',
+ 'constructor': 'NetworkGeolocationProvider',
+ },
+]
diff --git a/dom/system/linux/GeoclueLocationProvider.cpp b/dom/system/linux/GeoclueLocationProvider.cpp
new file mode 100644
index 0000000000..c9f5ef5dac
--- /dev/null
+++ b/dom/system/linux/GeoclueLocationProvider.cpp
@@ -0,0 +1,1060 @@
+/*
+ * 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/.
+ *
+ * Author: Maciej S. Szmigiero <mail@maciej.szmigiero.name>
+ */
+
+#include "GeoclueLocationProvider.h"
+
+#include <gio/gio.h>
+#include <glib.h>
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/GRefPtr.h"
+#include "mozilla/GUniquePtr.h"
+#include "mozilla/Logging.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticPrefs_geo.h"
+#include "mozilla/UniquePtrExtensions.h"
+#include "mozilla/WeakPtr.h"
+#include "mozilla/XREAppData.h"
+#include "mozilla/dom/GeolocationPosition.h"
+#include "mozilla/dom/GeolocationPositionErrorBinding.h"
+#include "MLSFallback.h"
+#include "nsAppRunner.h"
+#include "nsCOMPtr.h"
+#include "nsIDOMGeoPosition.h"
+#include "nsINamed.h"
+#include "nsITimer.h"
+#include "nsStringFwd.h"
+#include "prtime.h"
+
+namespace mozilla::dom {
+
+static LazyLogModule gGCLocationLog("GeoclueLocation");
+
+#define GCL_LOG(level, ...) \
+ MOZ_LOG(gGCLocationLog, mozilla::LogLevel::level, (__VA_ARGS__))
+
+static const char* const kGeoclueBusName = "org.freedesktop.GeoClue2";
+static const char* const kGCManagerPath = "/org/freedesktop/GeoClue2/Manager";
+static const char* const kGCManagerInterface =
+ "org.freedesktop.GeoClue2.Manager";
+static const char* const kGCClientInterface = "org.freedesktop.GeoClue2.Client";
+static const char* const kGCLocationInterface =
+ "org.freedesktop.GeoClue2.Location";
+static const char* const kDBPropertySetMethod =
+ "org.freedesktop.DBus.Properties.Set";
+
+/*
+ * Minimum altitude reported as valid (in meters),
+ * https://en.wikipedia.org/wiki/List_of_places_on_land_with_elevations_below_sea_level
+ * says that lowest land in the world is at -430 m, so let's use -500 m here.
+ */
+static const double kGCMinAlt = -500;
+
+/*
+ * Matches "enum GClueAccuracyLevel" values, see:
+ * https://www.freedesktop.org/software/geoclue/docs/geoclue-gclue-enums.html#GClueAccuracyLevel
+ */
+enum class GCAccuracyLevel {
+ None = 0,
+ Country = 1,
+ City = 4,
+ Neighborhood = 5,
+ Street = 6,
+ Exact = 8,
+};
+
+/*
+ * Whether to reuse D-Bus proxies between uses of this provider.
+ * Usually a good thing, can be disabled for debug purposes.
+ */
+static const bool kGCReuseDBusProxy = true;
+
+class GCLocProviderPriv final : public nsIGeolocationProvider,
+ public SupportsWeakPtr {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIGEOLOCATIONPROVIDER
+
+ GCLocProviderPriv();
+
+ void UpdateLastPosition();
+
+ private:
+ class LocationTimerCallback final : public nsITimerCallback, public nsINamed {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSITIMERCALLBACK
+
+ explicit LocationTimerCallback(GCLocProviderPriv* aParent)
+ : mParent(aParent) {}
+
+ NS_IMETHOD GetName(nsACString& aName) override {
+ aName.AssignLiteral("GCLocProvider::LocationTimerCallback");
+ return NS_OK;
+ }
+
+ private:
+ ~LocationTimerCallback() = default;
+ WeakPtr<GCLocProviderPriv> mParent;
+ };
+
+ enum class Accuracy { Unset, Low, High };
+ // States:
+ // Uninit: The default / initial state, with no client proxy yet.
+ // Initing: Takes care of establishing the client connection (GetClient /
+ // ConnectClient / SetDesktopID).
+ // SettingAccuracy: Does SetAccuracy operation, knows it should just go idle
+ // after finishing it.
+ // SettingAccuracyForStart: Does SetAccuracy operation, knows it then needs
+ // to do a Start operation after finishing it.
+ // Idle: Fully initialized, but not running state (quiescent).
+ // Starting: Starts the client by calling the Start D-Bus method.
+ // Started: Normal running state.
+ // Stopping: Stops the client by calling the Stop D-Bus method, knows it
+ // should just go idle after finishing it.
+ // StoppingForRestart: Stops the client by calling the Stop D-Bus method as
+ // a part of a Stop -> Start sequence (with possibly
+ // an accuracy update between these method calls).
+ //
+ // Valid state transitions are:
+ // (any state) -> Uninit: Transition when a D-Bus call failed or
+ // provided invalid data.
+ //
+ // Watch() startup path:
+ // Uninit -> Initing: Transition after getting the very first Watch()
+ // request
+ // or any such request while not having the client proxy.
+ // Initing -> SettingAccuracyForStart: Transition after getting a successful
+ // SetDesktopID response.
+ // SettingAccuracyForStart -> Starting: Transition after getting a
+ // successful
+ // SetAccuracy response.
+ // Idle -> Starting: Transition after getting a Watch() request while in
+ // fully
+ // initialized, but not running state.
+ // SettingAccuracy -> SettingAccuracyForStart: Transition after getting a
+ // Watch()
+ // request in the middle of
+ // setting accuracy during idle
+ // status.
+ // Stopping -> StoppingForRestart: Transition after getting a Watch()
+ // request
+ // in the middle of doing a Stop D-Bus call
+ // for idle status.
+ // StoppingForRestart -> Starting: Transition after getting a successful
+ // Stop response as a part of a Stop ->
+ // Start sequence while the previously set
+ // accuracy is still correct.
+ // StoppingForRestart -> SettingAccuracyForStart: Transition after getting
+ // a successful Stop response
+ // as a part of a Stop ->
+ // Start sequence but the set
+ // accuracy needs updating.
+ // Starting -> Started: Transition after getting a successful Start
+ // response.
+ //
+ // Shutdown() path:
+ // (any state) -> Uninit: Transition when not reusing the client proxy for
+ // any reason.
+ // Started -> Stopping: Transition from normal running state when reusing
+ // the client proxy.
+ // SettingAccuracyForStart -> SettingAccuracy: Transition when doing
+ // a shutdown in the middle of
+ // setting accuracy for a start
+ // when reusing the client
+ // proxy.
+ // SettingAccuracy -> Idle: Transition after getting a successful
+ // SetAccuracy
+ // response.
+ // StoppingForRestart -> Stopping: Transition when doing shutdown
+ // in the middle of a Stop -> Start sequence
+ // when reusing the client proxy.
+ // Stopping -> Idle: Transition after getting a successful Stop response.
+ //
+ // SetHighAccuracy() path:
+ // Started -> StoppingForRestart: Transition when accuracy needs updating
+ // on a running client.
+ // (the rest of the flow in StoppingForRestart state is the same as when
+ // being in this state in the Watch() startup path)
+ enum class ClientState {
+ Uninit,
+ Initing,
+ SettingAccuracy,
+ SettingAccuracyForStart,
+ Idle,
+ Starting,
+ Started,
+ Stopping,
+ StoppingForRestart
+ };
+
+ ~GCLocProviderPriv();
+
+ static bool AlwaysHighAccuracy();
+
+ void SetState(ClientState aNewState, const char* aNewStateStr);
+
+ void Update(nsIDOMGeoPosition* aPosition);
+ MOZ_CAN_RUN_SCRIPT void NotifyError(int aError);
+ MOZ_CAN_RUN_SCRIPT void DBusProxyError(const GError* aGError,
+ bool aResetManager = false);
+
+ MOZ_CAN_RUN_SCRIPT static void GetClientResponse(GDBusProxy* aProxy,
+ GAsyncResult* aResult,
+ gpointer aUserData);
+ void ConnectClient(const gchar* aClientPath);
+ MOZ_CAN_RUN_SCRIPT static void ConnectClientResponse(GObject* aObject,
+ GAsyncResult* aResult,
+ gpointer aUserData);
+ void SetDesktopID();
+ MOZ_CAN_RUN_SCRIPT static void SetDesktopIDResponse(GDBusProxy* aProxy,
+ GAsyncResult* aResult,
+ gpointer aUserData);
+ void SetAccuracy();
+ MOZ_CAN_RUN_SCRIPT static void SetAccuracyResponse(GDBusProxy* aProxy,
+ GAsyncResult* aResult,
+ gpointer aUserData);
+ void StartClient();
+ MOZ_CAN_RUN_SCRIPT static void StartClientResponse(GDBusProxy* aProxy,
+ GAsyncResult* aResult,
+ gpointer aUserData);
+ void StopClient(bool aForRestart);
+ MOZ_CAN_RUN_SCRIPT static void StopClientResponse(GDBusProxy* aProxy,
+ GAsyncResult* aResult,
+ gpointer aUserData);
+ void StopClientNoWait();
+ void MaybeRestartForAccuracy();
+
+ MOZ_CAN_RUN_SCRIPT static void GCManagerOwnerNotify(GObject* aObject,
+ GParamSpec* aPSpec,
+ gpointer aUserData);
+
+ static void GCClientSignal(GDBusProxy* aProxy, gchar* aSenderName,
+ gchar* aSignalName, GVariant* aParameters,
+ gpointer aUserData);
+ void ConnectLocation(const gchar* aLocationPath);
+ static bool GetLocationProperty(GDBusProxy* aProxyLocation,
+ const gchar* aName, double* aOut);
+ static void ConnectLocationResponse(GObject* aObject, GAsyncResult* aResult,
+ gpointer aUserData);
+
+ void SetLocationTimer();
+ void StopLocationTimer();
+
+ bool InDBusCall();
+ bool InDBusStoppingCall();
+ bool InDBusStoppedCall();
+
+ void DeleteManager();
+ void DoShutdown(bool aDeleteClient, bool aDeleteManager);
+ void DoShutdownClearCallback(bool aDestroying);
+
+ nsresult FallbackToMLS();
+ void StopMLSFallback();
+
+ void WatchStart();
+
+ Accuracy mAccuracyWanted = Accuracy::Unset;
+ Accuracy mAccuracySet = Accuracy::Unset;
+ RefPtr<GDBusProxy> mProxyManager;
+ RefPtr<GDBusProxy> mProxyClient;
+ RefPtr<GCancellable> mCancellable;
+ nsCOMPtr<nsIGeolocationUpdate> mCallback;
+ ClientState mClientState = ClientState::Uninit;
+ RefPtr<nsIDOMGeoPosition> mLastPosition;
+ RefPtr<nsITimer> mLocationTimer;
+ RefPtr<MLSFallback> mMLSFallback;
+};
+
+//
+// GCLocProviderPriv
+//
+
+#define GCLP_SETSTATE(this, state) this->SetState(ClientState::state, #state)
+
+GCLocProviderPriv::GCLocProviderPriv() {
+ if (AlwaysHighAccuracy()) {
+ mAccuracyWanted = Accuracy::High;
+ } else {
+ mAccuracyWanted = Accuracy::Low;
+ }
+}
+
+GCLocProviderPriv::~GCLocProviderPriv() { DoShutdownClearCallback(true); }
+
+bool GCLocProviderPriv::AlwaysHighAccuracy() {
+ return StaticPrefs::geo_provider_geoclue_always_high_accuracy();
+}
+
+void GCLocProviderPriv::SetState(ClientState aNewState,
+ const char* aNewStateStr) {
+ if (mClientState == aNewState) {
+ return;
+ }
+
+ GCL_LOG(Debug, "changing state to %s", aNewStateStr);
+ mClientState = aNewState;
+}
+
+void GCLocProviderPriv::Update(nsIDOMGeoPosition* aPosition) {
+ if (!mCallback) {
+ return;
+ }
+
+ mCallback->Update(aPosition);
+}
+
+void GCLocProviderPriv::UpdateLastPosition() {
+ MOZ_DIAGNOSTIC_ASSERT(mLastPosition, "No last position to update");
+ StopLocationTimer();
+ Update(mLastPosition);
+}
+
+nsresult GCLocProviderPriv::FallbackToMLS() {
+ GCL_LOG(Debug, "trying to fall back to MLS");
+ StopMLSFallback();
+
+ RefPtr fallback = new MLSFallback(0);
+ MOZ_TRY(fallback->Startup(mCallback));
+
+ GCL_LOG(Debug, "Started up MLS fallback");
+ mMLSFallback = std::move(fallback);
+ return NS_OK;
+}
+
+void GCLocProviderPriv::StopMLSFallback() {
+ if (!mMLSFallback) {
+ return;
+ }
+ GCL_LOG(Debug, "Clearing MLS fallback");
+ if (mMLSFallback) {
+ mMLSFallback->Shutdown();
+ mMLSFallback = nullptr;
+ }
+}
+
+void GCLocProviderPriv::NotifyError(int aError) {
+ if (!mCallback) {
+ return;
+ }
+
+ // We errored out, try to fall back to MLS.
+ if (NS_SUCCEEDED(FallbackToMLS())) {
+ return;
+ }
+
+ nsCOMPtr callback = mCallback;
+ callback->NotifyError(aError);
+}
+
+void GCLocProviderPriv::DBusProxyError(const GError* aGError,
+ bool aResetManager) {
+ // that G_DBUS_ERROR below is actually a function call, not a constant
+ GQuark gdbusDomain = G_DBUS_ERROR;
+ int error = GeolocationPositionError_Binding::POSITION_UNAVAILABLE;
+ if (aGError) {
+ if (g_error_matches(aGError, gdbusDomain, G_DBUS_ERROR_TIMEOUT) ||
+ g_error_matches(aGError, gdbusDomain, G_DBUS_ERROR_TIMED_OUT)) {
+ error = GeolocationPositionError_Binding::TIMEOUT;
+ } else if (g_error_matches(aGError, gdbusDomain,
+ G_DBUS_ERROR_LIMITS_EXCEEDED) ||
+ g_error_matches(aGError, gdbusDomain,
+ G_DBUS_ERROR_ACCESS_DENIED) ||
+ g_error_matches(aGError, gdbusDomain,
+ G_DBUS_ERROR_AUTH_FAILED)) {
+ error = GeolocationPositionError_Binding::PERMISSION_DENIED;
+ }
+ }
+
+ DoShutdown(true, aResetManager);
+ NotifyError(error);
+}
+
+void GCLocProviderPriv::GetClientResponse(GDBusProxy* aProxy,
+ GAsyncResult* aResult,
+ gpointer aUserData) {
+ GUniquePtr<GError> error;
+ RefPtr<GVariant> variant = dont_AddRef(
+ g_dbus_proxy_call_finish(aProxy, aResult, getter_Transfers(error)));
+ if (!variant) {
+ // if cancelled |self| might no longer be there
+ if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ GCL_LOG(Error, "Failed to get client: %s\n", error->message);
+ RefPtr self = static_cast<GCLocProviderPriv*>(aUserData);
+ self->DBusProxyError(error.get(), true);
+ }
+ return;
+ }
+
+ RefPtr self = static_cast<GCLocProviderPriv*>(aUserData);
+ MOZ_DIAGNOSTIC_ASSERT(self->mClientState == ClientState::Initing,
+ "Client in a wrong state");
+
+ auto signalError = MakeScopeExit([&]() MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ self->DBusProxyError(nullptr, true);
+ });
+
+ if (!g_variant_is_of_type(variant, G_VARIANT_TYPE_TUPLE)) {
+ GCL_LOG(Error, "Unexpected get client call return type: %s\n",
+ g_variant_get_type_string(variant));
+ return;
+ }
+
+ if (g_variant_n_children(variant) < 1) {
+ GCL_LOG(Error,
+ "Not enough params in get client call return: %" G_GSIZE_FORMAT
+ "\n",
+ g_variant_n_children(variant));
+ return;
+ }
+
+ variant = dont_AddRef(g_variant_get_child_value(variant, 0));
+ if (!g_variant_is_of_type(variant, G_VARIANT_TYPE_OBJECT_PATH)) {
+ GCL_LOG(Error, "Unexpected get client call return type inside tuple: %s\n",
+ g_variant_get_type_string(variant));
+ return;
+ }
+
+ const gchar* clientPath = g_variant_get_string(variant, nullptr);
+ GCL_LOG(Debug, "Client path: %s\n", clientPath);
+
+ signalError.release();
+ self->ConnectClient(clientPath);
+}
+
+void GCLocProviderPriv::ConnectClient(const gchar* aClientPath) {
+ MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Initing,
+ "Client in a wrong state");
+ MOZ_ASSERT(mCancellable, "Watch() wasn't successfully called");
+ g_dbus_proxy_new_for_bus(
+ G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, nullptr, kGeoclueBusName,
+ aClientPath, kGCClientInterface, mCancellable,
+ reinterpret_cast<GAsyncReadyCallback>(ConnectClientResponse), this);
+}
+
+void GCLocProviderPriv::ConnectClientResponse(GObject* aObject,
+ GAsyncResult* aResult,
+ gpointer aUserData) {
+ GUniquePtr<GError> error;
+ RefPtr<GDBusProxy> proxyClient =
+ dont_AddRef(g_dbus_proxy_new_finish(aResult, getter_Transfers(error)));
+ if (!proxyClient) {
+ // if cancelled |self| might no longer be there
+ if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ GCL_LOG(Error, "Failed to connect to client: %s\n", error->message);
+ RefPtr self = static_cast<GCLocProviderPriv*>(aUserData);
+ self->DBusProxyError(error.get());
+ }
+ return;
+ }
+ RefPtr self = static_cast<GCLocProviderPriv*>(aUserData);
+ self->mProxyClient = std::move(proxyClient);
+
+ MOZ_DIAGNOSTIC_ASSERT(self->mClientState == ClientState::Initing,
+ "Client in a wrong state");
+
+ GCL_LOG(Info, "Client interface connected\n");
+
+ g_signal_connect(self->mProxyClient, "g-signal", G_CALLBACK(GCClientSignal),
+ self);
+ self->SetDesktopID();
+}
+
+void GCLocProviderPriv::SetDesktopID() {
+ MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Initing,
+ "Client in a wrong state");
+ MOZ_DIAGNOSTIC_ASSERT(mProxyClient && mCancellable,
+ "Watch() wasn't successfully called");
+
+ nsAutoCString appName;
+ gAppData->GetDBusAppName(appName);
+ g_dbus_proxy_call(mProxyClient, kDBPropertySetMethod,
+ g_variant_new("(ssv)", kGCClientInterface, "DesktopId",
+ g_variant_new_string(appName.get())),
+ G_DBUS_CALL_FLAGS_NONE, -1, mCancellable,
+ reinterpret_cast<GAsyncReadyCallback>(SetDesktopIDResponse),
+ this);
+}
+
+void GCLocProviderPriv::SetDesktopIDResponse(GDBusProxy* aProxy,
+ GAsyncResult* aResult,
+ gpointer aUserData) {
+ GUniquePtr<GError> error;
+
+ RefPtr<GVariant> variant = dont_AddRef(
+ g_dbus_proxy_call_finish(aProxy, aResult, getter_Transfers(error)));
+ if (!variant) {
+ // if cancelled |self| might no longer be there
+ if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ GCL_LOG(Error, "Failed to set DesktopId: %s\n", error->message);
+ RefPtr self = static_cast<GCLocProviderPriv*>(aUserData);
+ self->DBusProxyError(error.get());
+ }
+ return;
+ }
+
+ RefPtr self = static_cast<GCLocProviderPriv*>(aUserData);
+ MOZ_DIAGNOSTIC_ASSERT(self->mClientState == ClientState::Initing,
+ "Client in a wrong state");
+
+ GCLP_SETSTATE(self, Idle);
+ self->SetAccuracy();
+}
+
+void GCLocProviderPriv::SetAccuracy() {
+ MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Idle,
+ "Client in a wrong state");
+ MOZ_DIAGNOSTIC_ASSERT(mProxyClient && mCancellable,
+ "Watch() wasn't successfully called");
+ MOZ_ASSERT(mAccuracyWanted != Accuracy::Unset, "Invalid accuracy");
+
+ guint32 accuracy;
+ if (mAccuracyWanted == Accuracy::High) {
+ accuracy = (guint32)GCAccuracyLevel::Exact;
+ } else {
+ accuracy = (guint32)GCAccuracyLevel::City;
+ }
+
+ mAccuracySet = mAccuracyWanted;
+ GCLP_SETSTATE(this, SettingAccuracyForStart);
+ g_dbus_proxy_call(
+ mProxyClient, kDBPropertySetMethod,
+ g_variant_new("(ssv)", kGCClientInterface, "RequestedAccuracyLevel",
+ g_variant_new_uint32(accuracy)),
+ G_DBUS_CALL_FLAGS_NONE, -1, mCancellable,
+ reinterpret_cast<GAsyncReadyCallback>(SetAccuracyResponse), this);
+}
+
+void GCLocProviderPriv::SetAccuracyResponse(GDBusProxy* aProxy,
+ GAsyncResult* aResult,
+ gpointer aUserData) {
+ GUniquePtr<GError> error;
+ RefPtr<GVariant> variant = dont_AddRef(
+ g_dbus_proxy_call_finish(aProxy, aResult, getter_Transfers(error)));
+ if (!variant) {
+ // if cancelled |self| might no longer be there
+ if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ GCL_LOG(Error, "Failed to set requested accuracy level: %s\n",
+ error->message);
+ RefPtr self = static_cast<GCLocProviderPriv*>(aUserData);
+ self->DBusProxyError(error.get());
+ }
+ return;
+ }
+
+ RefPtr self = static_cast<GCLocProviderPriv*>(aUserData);
+ MOZ_DIAGNOSTIC_ASSERT(
+ self->mClientState == ClientState::SettingAccuracyForStart ||
+ self->mClientState == ClientState::SettingAccuracy,
+ "Client in a wrong state");
+ bool wantStart = self->mClientState == ClientState::SettingAccuracyForStart;
+ GCLP_SETSTATE(self, Idle);
+
+ if (wantStart) {
+ self->StartClient();
+ }
+}
+
+void GCLocProviderPriv::StartClient() {
+ MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Idle,
+ "Client in a wrong state");
+ MOZ_DIAGNOSTIC_ASSERT(mProxyClient && mCancellable,
+ "Watch() wasn't successfully called");
+ GCLP_SETSTATE(this, Starting);
+ g_dbus_proxy_call(
+ mProxyClient, "Start", nullptr, G_DBUS_CALL_FLAGS_NONE, -1, mCancellable,
+ reinterpret_cast<GAsyncReadyCallback>(StartClientResponse), this);
+}
+
+void GCLocProviderPriv::StartClientResponse(GDBusProxy* aProxy,
+ GAsyncResult* aResult,
+ gpointer aUserData) {
+ GUniquePtr<GError> error;
+
+ RefPtr<GVariant> variant = dont_AddRef(
+ g_dbus_proxy_call_finish(aProxy, aResult, getter_Transfers(error)));
+ if (!variant) {
+ // if cancelled |self| might no longer be there
+ if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ GCL_LOG(Error, "Failed to start client: %s\n", error->message);
+ RefPtr self = static_cast<GCLocProviderPriv*>(aUserData);
+ /*
+ * A workaround for
+ * https://gitlab.freedesktop.org/geoclue/geoclue/-/issues/143 We need to
+ * get a new client instance once the agent finally connects to the
+ * Geoclue service, otherwise every Start request on the old client
+ * interface will be denied. We need to reconnect to the Manager interface
+ * to achieve this since otherwise GetClient call will simply return the
+ * old client instance.
+ */
+ bool resetManager = g_error_matches(error.get(), G_DBUS_ERROR,
+ G_DBUS_ERROR_ACCESS_DENIED);
+ self->DBusProxyError(error.get(), resetManager);
+ }
+ return;
+ }
+
+ RefPtr self = static_cast<GCLocProviderPriv*>(aUserData);
+ MOZ_DIAGNOSTIC_ASSERT(self->mClientState == ClientState::Starting,
+ "Client in a wrong state");
+ GCLP_SETSTATE(self, Started);
+ self->MaybeRestartForAccuracy();
+}
+
+void GCLocProviderPriv::StopClient(bool aForRestart) {
+ MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Started,
+ "Client in a wrong state");
+ MOZ_DIAGNOSTIC_ASSERT(mProxyClient && mCancellable,
+ "Watch() wasn't successfully called");
+
+ if (aForRestart) {
+ GCLP_SETSTATE(this, StoppingForRestart);
+ } else {
+ GCLP_SETSTATE(this, Stopping);
+ }
+
+ g_dbus_proxy_call(
+ mProxyClient, "Stop", nullptr, G_DBUS_CALL_FLAGS_NONE, -1, mCancellable,
+ reinterpret_cast<GAsyncReadyCallback>(StopClientResponse), this);
+}
+
+void GCLocProviderPriv::StopClientResponse(GDBusProxy* aProxy,
+ GAsyncResult* aResult,
+ gpointer aUserData) {
+ GUniquePtr<GError> error;
+ RefPtr<GVariant> variant = dont_AddRef(
+ g_dbus_proxy_call_finish(aProxy, aResult, getter_Transfers(error)));
+ if (!variant) {
+ // if cancelled |self| might no longer be there
+ if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ GCL_LOG(Error, "Failed to stop client: %s\n", error->message);
+ RefPtr self = static_cast<GCLocProviderPriv*>(aUserData);
+ self->DBusProxyError(error.get());
+ }
+ return;
+ }
+
+ RefPtr self = static_cast<GCLocProviderPriv*>(aUserData);
+ MOZ_DIAGNOSTIC_ASSERT(self->InDBusStoppingCall(), "Client in a wrong state");
+ bool wantRestart = self->mClientState == ClientState::StoppingForRestart;
+ GCLP_SETSTATE(self, Idle);
+
+ if (!wantRestart) {
+ return;
+ }
+
+ if (self->mAccuracyWanted != self->mAccuracySet) {
+ self->SetAccuracy();
+ } else {
+ self->StartClient();
+ }
+}
+
+void GCLocProviderPriv::StopClientNoWait() {
+ MOZ_DIAGNOSTIC_ASSERT(mProxyClient, "Watch() wasn't successfully called");
+ g_dbus_proxy_call(mProxyClient, "Stop", nullptr, G_DBUS_CALL_FLAGS_NONE, -1,
+ nullptr, nullptr, nullptr);
+}
+
+void GCLocProviderPriv::MaybeRestartForAccuracy() {
+ if (mAccuracyWanted == mAccuracySet) {
+ return;
+ }
+
+ if (mClientState != ClientState::Started) {
+ return;
+ }
+
+ // Setting a new accuracy requires restarting the client
+ StopClient(true);
+}
+
+void GCLocProviderPriv::GCManagerOwnerNotify(GObject* aObject,
+ GParamSpec* aPSpec,
+ gpointer aUserData) {
+ RefPtr self = static_cast<GCLocProviderPriv*>(aUserData);
+ GUniquePtr<gchar> managerOwner(
+ g_dbus_proxy_get_name_owner(self->mProxyManager));
+ if (!managerOwner) {
+ GCL_LOG(Info, "The Manager interface has lost its owner\n");
+ self->DBusProxyError(nullptr, true);
+ }
+}
+
+void GCLocProviderPriv::GCClientSignal(GDBusProxy* aProxy, gchar* aSenderName,
+ gchar* aSignalName,
+ GVariant* aParameters,
+ gpointer aUserData) {
+ if (g_strcmp0(aSignalName, "LocationUpdated")) {
+ return;
+ }
+
+ if (!g_variant_is_of_type(aParameters, G_VARIANT_TYPE_TUPLE)) {
+ GCL_LOG(Error, "Unexpected location updated signal params type: %s\n",
+ g_variant_get_type_string(aParameters));
+ return;
+ }
+
+ if (g_variant_n_children(aParameters) < 2) {
+ GCL_LOG(Error,
+ "Not enough params in location updated signal: %" G_GSIZE_FORMAT
+ "\n",
+ g_variant_n_children(aParameters));
+ return;
+ }
+
+ RefPtr<GVariant> variant =
+ dont_AddRef(g_variant_get_child_value(aParameters, 1));
+ if (!g_variant_is_of_type(variant, G_VARIANT_TYPE_OBJECT_PATH)) {
+ GCL_LOG(Error,
+ "Unexpected location updated signal new location path type: %s\n",
+ g_variant_get_type_string(variant));
+ return;
+ }
+
+ RefPtr self = static_cast<GCLocProviderPriv*>(aUserData);
+ const gchar* locationPath = g_variant_get_string(variant, nullptr);
+ GCL_LOG(Verbose, "New location path: %s\n", locationPath);
+ self->ConnectLocation(locationPath);
+}
+
+void GCLocProviderPriv::ConnectLocation(const gchar* aLocationPath) {
+ MOZ_ASSERT(mCancellable, "Startup() wasn't successfully called");
+ g_dbus_proxy_new_for_bus(
+ G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, nullptr, kGeoclueBusName,
+ aLocationPath, kGCLocationInterface, mCancellable,
+ reinterpret_cast<GAsyncReadyCallback>(ConnectLocationResponse), this);
+}
+
+bool GCLocProviderPriv::GetLocationProperty(GDBusProxy* aProxyLocation,
+ const gchar* aName, double* aOut) {
+ RefPtr<GVariant> property =
+ dont_AddRef(g_dbus_proxy_get_cached_property(aProxyLocation, aName));
+ if (!g_variant_is_of_type(property, G_VARIANT_TYPE_DOUBLE)) {
+ GCL_LOG(Error, "Unexpected location property %s type: %s\n", aName,
+ g_variant_get_type_string(property));
+ return false;
+ }
+
+ *aOut = g_variant_get_double(property);
+ return true;
+}
+
+void GCLocProviderPriv::ConnectLocationResponse(GObject* aObject,
+ GAsyncResult* aResult,
+ gpointer aUserData) {
+ GUniquePtr<GError> error;
+ RefPtr<GDBusProxy> proxyLocation =
+ dont_AddRef(g_dbus_proxy_new_finish(aResult, getter_Transfers(error)));
+ if (!proxyLocation) {
+ if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ GCL_LOG(Warning, "Failed to connect to location: %s\n", error->message);
+ }
+ return;
+ }
+
+ RefPtr self = static_cast<GCLocProviderPriv*>(aUserData);
+ /*
+ * nsGeoPositionCoords will convert NaNs to null for optional properties of
+ * the JavaScript Coordinates object.
+ */
+ double lat = UnspecifiedNaN<double>();
+ double lon = UnspecifiedNaN<double>();
+ double alt = UnspecifiedNaN<double>();
+ double hError = UnspecifiedNaN<double>();
+ const double vError = UnspecifiedNaN<double>();
+ double heading = UnspecifiedNaN<double>();
+ double speed = UnspecifiedNaN<double>();
+ struct {
+ const gchar* name;
+ double* out;
+ } props[] = {
+ {"Latitude", &lat}, {"Longitude", &lon}, {"Altitude", &alt},
+ {"Accuracy", &hError}, {"Heading", &heading}, {"Speed", &speed},
+ };
+
+ for (auto& prop : props) {
+ if (!GetLocationProperty(proxyLocation, prop.name, prop.out)) {
+ return;
+ }
+ }
+
+ if (alt < kGCMinAlt) {
+ alt = UnspecifiedNaN<double>();
+ }
+ if (speed < 0) {
+ speed = UnspecifiedNaN<double>();
+ }
+ if (heading < 0 || std::isnan(speed) || speed == 0) {
+ heading = UnspecifiedNaN<double>();
+ }
+
+ GCL_LOG(Info, "New location: %f %f +-%fm @ %gm; hdg %f spd %fm/s\n", lat, lon,
+ hError, alt, heading, speed);
+
+ self->mLastPosition =
+ new nsGeoPosition(lat, lon, alt, hError, vError, heading, speed,
+ PR_Now() / PR_USEC_PER_MSEC);
+ self->UpdateLastPosition();
+}
+
+void GCLocProviderPriv::SetLocationTimer() {
+ MOZ_DIAGNOSTIC_ASSERT(mLastPosition, "no last position to report");
+
+ StopLocationTimer();
+
+ RefPtr<LocationTimerCallback> timerCallback = new LocationTimerCallback(this);
+ NS_NewTimerWithCallback(getter_AddRefs(mLocationTimer), timerCallback, 1000,
+ nsITimer::TYPE_ONE_SHOT);
+}
+
+void GCLocProviderPriv::StopLocationTimer() {
+ if (!mLocationTimer) {
+ return;
+ }
+
+ mLocationTimer->Cancel();
+ mLocationTimer = nullptr;
+}
+
+// Did we made some D-Bus call and are still waiting for its response?
+bool GCLocProviderPriv::InDBusCall() {
+ return mClientState == ClientState::Initing ||
+ mClientState == ClientState::SettingAccuracy ||
+ mClientState == ClientState::SettingAccuracyForStart ||
+ mClientState == ClientState::Starting ||
+ mClientState == ClientState::Stopping ||
+ mClientState == ClientState::StoppingForRestart;
+}
+
+bool GCLocProviderPriv::InDBusStoppingCall() {
+ return mClientState == ClientState::Stopping ||
+ mClientState == ClientState::StoppingForRestart;
+}
+
+/*
+ * Did we made some D-Bus call while stopped and
+ * are still waiting for its response?
+ */
+bool GCLocProviderPriv::InDBusStoppedCall() {
+ return mClientState == ClientState::SettingAccuracy ||
+ mClientState == ClientState::SettingAccuracyForStart;
+}
+
+void GCLocProviderPriv::DeleteManager() {
+ if (!mProxyManager) {
+ return;
+ }
+
+ g_signal_handlers_disconnect_matched(mProxyManager, G_SIGNAL_MATCH_DATA, 0, 0,
+ nullptr, nullptr, this);
+ mProxyManager = nullptr;
+}
+
+void GCLocProviderPriv::DoShutdown(bool aDeleteClient, bool aDeleteManager) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ !aDeleteManager || aDeleteClient,
+ "deleting manager proxy requires deleting client one, too");
+
+ // Invalidate the cached last position
+ StopLocationTimer();
+ mLastPosition = nullptr;
+
+ /*
+ * Do we need to delete the D-Bus proxy (or proxies)?
+ * Either because that's what our caller wanted, or because we are set to
+ * never reuse them, or because we are in a middle of some D-Bus call while
+ * having the service running (and so not being able to issue an immediate
+ * Stop call).
+ */
+ if (aDeleteClient || !kGCReuseDBusProxy ||
+ (InDBusCall() && !InDBusStoppingCall() && !InDBusStoppedCall())) {
+ if (mClientState == ClientState::Started) {
+ StopClientNoWait();
+ GCLP_SETSTATE(this, Idle);
+ }
+ if (mProxyClient) {
+ g_signal_handlers_disconnect_matched(mProxyClient, G_SIGNAL_MATCH_DATA, 0,
+ 0, nullptr, nullptr, this);
+ }
+ if (mCancellable) {
+ g_cancellable_cancel(mCancellable);
+ mCancellable = nullptr;
+ }
+ mProxyClient = nullptr;
+
+ if (aDeleteManager || !kGCReuseDBusProxy) {
+ DeleteManager();
+ }
+
+ GCLP_SETSTATE(this, Uninit);
+ } else if (mClientState == ClientState::Started) {
+ StopClient(false);
+ } else if (mClientState == ClientState::SettingAccuracyForStart) {
+ GCLP_SETSTATE(this, SettingAccuracy);
+ } else if (mClientState == ClientState::StoppingForRestart) {
+ GCLP_SETSTATE(this, Stopping);
+ }
+}
+
+void GCLocProviderPriv::DoShutdownClearCallback(bool aDestroying) {
+ mCallback = nullptr;
+ StopMLSFallback();
+ DoShutdown(aDestroying, aDestroying);
+}
+
+NS_IMPL_ISUPPORTS(GCLocProviderPriv, nsIGeolocationProvider)
+
+// nsIGeolocationProvider
+//
+
+/*
+ * The Startup() method should only succeed if Geoclue is available on D-Bus
+ * so it can be used for determining whether to continue with this geolocation
+ * provider in Geolocation.cpp
+ */
+NS_IMETHODIMP
+GCLocProviderPriv::Startup() {
+ if (mProxyManager) {
+ return NS_OK;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Uninit,
+ "Client in a initialized state but no manager");
+
+ GUniquePtr<GError> error;
+ mProxyManager = dont_AddRef(g_dbus_proxy_new_for_bus_sync(
+ G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, nullptr, kGeoclueBusName,
+ kGCManagerPath, kGCManagerInterface, nullptr, getter_Transfers(error)));
+ if (!mProxyManager) {
+ GCL_LOG(Info, "Cannot connect to the Manager interface: %s\n",
+ error->message);
+ return NS_ERROR_FAILURE;
+ }
+
+ g_signal_connect(mProxyManager, "notify::g-name-owner",
+ G_CALLBACK(GCManagerOwnerNotify), this);
+
+ GUniquePtr<gchar> managerOwner(g_dbus_proxy_get_name_owner(mProxyManager));
+ if (!managerOwner) {
+ GCL_LOG(Info, "The Manager interface has no owner\n");
+ DeleteManager();
+ return NS_ERROR_FAILURE;
+ }
+
+ GCL_LOG(Info, "Manager interface connected successfully\n");
+
+ return NS_OK;
+}
+
+void GCLocProviderPriv::WatchStart() {
+ if (mClientState == ClientState::Idle) {
+ StartClient();
+ } else if (mClientState == ClientState::Started) {
+ if (mLastPosition && !mLocationTimer) {
+ GCL_LOG(Verbose,
+ "Will report the existing location if new one doesn't come up\n");
+ SetLocationTimer();
+ }
+ } else if (mClientState == ClientState::SettingAccuracy) {
+ GCLP_SETSTATE(this, SettingAccuracyForStart);
+ } else if (mClientState == ClientState::Stopping) {
+ GCLP_SETSTATE(this, StoppingForRestart);
+ }
+}
+
+NS_IMETHODIMP
+GCLocProviderPriv::Watch(nsIGeolocationUpdate* aCallback) {
+ mCallback = aCallback;
+
+ if (!mCancellable) {
+ mCancellable = dont_AddRef(g_cancellable_new());
+ }
+
+ if (mClientState != ClientState::Uninit) {
+ WatchStart();
+ return NS_OK;
+ }
+
+ if (!mProxyManager) {
+ GCL_LOG(Debug, "watch request falling back to MLS");
+ return FallbackToMLS();
+ }
+
+ StopMLSFallback();
+
+ GCLP_SETSTATE(this, Initing);
+ g_dbus_proxy_call(mProxyManager, "GetClient", nullptr, G_DBUS_CALL_FLAGS_NONE,
+ -1, mCancellable,
+ reinterpret_cast<GAsyncReadyCallback>(GetClientResponse),
+ this);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GCLocProviderPriv::Shutdown() {
+ DoShutdownClearCallback(false);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GCLocProviderPriv::SetHighAccuracy(bool aHigh) {
+ GCL_LOG(Verbose, "Want %s accuracy\n", aHigh ? "high" : "low");
+ if (!aHigh && AlwaysHighAccuracy()) {
+ GCL_LOG(Verbose, "Forcing high accuracy due to pref\n");
+ aHigh = true;
+ }
+
+ mAccuracyWanted = aHigh ? Accuracy::High : Accuracy::Low;
+ MaybeRestartForAccuracy();
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(GCLocProviderPriv::LocationTimerCallback, nsITimerCallback,
+ nsINamed)
+
+NS_IMETHODIMP
+GCLocProviderPriv::LocationTimerCallback::Notify(nsITimer* aTimer) {
+ if (mParent) {
+ RefPtr<GCLocProviderPriv> parent(mParent);
+ parent->UpdateLastPosition();
+ }
+
+ return NS_OK;
+}
+
+GeoclueLocationProvider::GeoclueLocationProvider() {
+ mPriv = new GCLocProviderPriv;
+}
+
+// nsISupports
+//
+
+NS_IMPL_ISUPPORTS(GeoclueLocationProvider, nsIGeolocationProvider)
+
+// nsIGeolocationProvider
+//
+
+NS_IMETHODIMP
+GeoclueLocationProvider::Startup() { return mPriv->Startup(); }
+
+NS_IMETHODIMP
+GeoclueLocationProvider::Watch(nsIGeolocationUpdate* aCallback) {
+ return mPriv->Watch(aCallback);
+}
+
+NS_IMETHODIMP
+GeoclueLocationProvider::Shutdown() { return mPriv->Shutdown(); }
+
+NS_IMETHODIMP
+GeoclueLocationProvider::SetHighAccuracy(bool aHigh) {
+ return mPriv->SetHighAccuracy(aHigh);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/system/linux/GeoclueLocationProvider.h b/dom/system/linux/GeoclueLocationProvider.h
new file mode 100644
index 0000000000..908cd25e37
--- /dev/null
+++ b/dom/system/linux/GeoclueLocationProvider.h
@@ -0,0 +1,32 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#ifndef GeoclueLocationProvider_h
+#define GeoclueLocationProvider_h
+
+#include "mozilla/RefPtr.h"
+#include "nsIGeolocationProvider.h"
+
+namespace mozilla::dom {
+
+class GCLocProviderPriv;
+
+class GeoclueLocationProvider final : public nsIGeolocationProvider {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIGEOLOCATIONPROVIDER
+
+ GeoclueLocationProvider();
+
+ private:
+ ~GeoclueLocationProvider() = default;
+
+ RefPtr<GCLocProviderPriv> mPriv;
+};
+
+} // namespace mozilla::dom
+
+#endif /* GeoclueLocationProvider_h */
diff --git a/dom/system/linux/GpsdLocationProvider.cpp b/dom/system/linux/GpsdLocationProvider.cpp
new file mode 100644
index 0000000000..34cd23c453
--- /dev/null
+++ b/dom/system/linux/GpsdLocationProvider.cpp
@@ -0,0 +1,446 @@
+/* -*- 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"
+#include "prtime.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)
+ : Runnable("GpsdU"),
+ 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)
+ : Runnable("GpsdNE"),
+ 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)
+ : Runnable("GpsdP"),
+ mLocationProvider(aLocationProvider),
+ mRunning(true) {
+ MOZ_ASSERT(mLocationProvider);
+ }
+
+ static bool IsSupported() {
+ return GPSD_API_MAJOR_VERSION >= 5 && GPSD_API_MAJOR_VERSION <= 12;
+ }
+
+ bool IsRunning() const { return mRunning; }
+
+ void StopRunning() { mRunning = false; }
+
+ // nsIRunnable
+ //
+
+ NS_IMETHOD Run() override {
+ int err;
+
+ switch (GPSD_API_MAJOR_VERSION) {
+ case 5 ... 12:
+ 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 && GPSD_API_MAJOR_VERSION <= 12
+ 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 */
+ }
+
+# if GPSD_API_MAJOR_VERSION >= 7
+ res = gps_read(&gpsData, nullptr, 0);
+# else
+
+ res = gps_read(&gpsData);
+# endif
+
+ if (res < 0) {
+ err = ErrnoToError(errno);
+ break;
+ } else if (!res) {
+ continue; /* no data available */
+ }
+
+# if GPSD_API_MAJOR_VERSION < 10
+ if (gpsData.status == STATUS_NO_FIX) {
+ continue;
+ }
+# endif
+
+ switch (gpsData.fix.mode) {
+ case MODE_3D:
+ double galt;
+
+# if GPSD_API_MAJOR_VERSION >= 9
+ galt = gpsData.fix.altMSL;
+# else
+ galt = gpsData.fix.altitude;
+# endif
+ if (!std::isnan(galt)) {
+ alt = galt;
+ }
+ [[fallthrough]];
+ case MODE_2D:
+ if (!std::isnan(gpsData.fix.latitude)) {
+ lat = gpsData.fix.latitude;
+ }
+ if (!std::isnan(gpsData.fix.longitude)) {
+ lon = gpsData.fix.longitude;
+ }
+ if (!std::isnan(gpsData.fix.epx) && !std::isnan(gpsData.fix.epy)) {
+ hError = std::max(gpsData.fix.epx, gpsData.fix.epy);
+ } else if (!std::isnan(gpsData.fix.epx)) {
+ hError = gpsData.fix.epx;
+ } else if (!std::isnan(gpsData.fix.epy)) {
+ hError = gpsData.fix.epy;
+ }
+ if (!std::isnan(gpsData.fix.epv)) {
+ vError = gpsData.fix.epv;
+ }
+ if (!std::isnan(gpsData.fix.track)) {
+ heading = gpsData.fix.track;
+ }
+ if (!std::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>("GpsdLP", 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",
+ 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/PortalLocationProvider.cpp b/dom/system/linux/PortalLocationProvider.cpp
new file mode 100644
index 0000000000..6ebb1854dc
--- /dev/null
+++ b/dom/system/linux/PortalLocationProvider.cpp
@@ -0,0 +1,351 @@
+/* -*- 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 "PortalLocationProvider.h"
+#include "MLSFallback.h"
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/Logging.h"
+#include "mozilla/dom/GeolocationPositionErrorBinding.h"
+#include "GeolocationPosition.h"
+#include "prtime.h"
+#include "mozilla/GUniquePtr.h"
+#include "mozilla/UniquePtrExtensions.h"
+#include "mozilla/XREAppData.h"
+
+#include <gio/gio.h>
+#include <glib-object.h>
+
+extern const mozilla::XREAppData* gAppData;
+
+namespace mozilla::dom {
+
+#ifdef MOZ_LOGGING
+static LazyLogModule sPortalLog("Portal");
+# define LOG_PORTAL(...) MOZ_LOG(sPortalLog, LogLevel::Debug, (__VA_ARGS__))
+#else
+# define LOG_PORTAL(...)
+#endif /* MOZ_LOGGING */
+
+const char kDesktopBusName[] = "org.freedesktop.portal.Desktop";
+const char kSessionInterfaceName[] = "org.freedesktop.portal.Session";
+
+/**
+ * |MLSGeolocationUpdate| provides a fallback if Portal is not supported.
+ */
+class PortalLocationProvider::MLSGeolocationUpdate final
+ : public nsIGeolocationUpdate {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIGEOLOCATIONUPDATE
+
+ explicit MLSGeolocationUpdate(nsIGeolocationUpdate* aCallback);
+
+ protected:
+ ~MLSGeolocationUpdate() = default;
+
+ private:
+ const nsCOMPtr<nsIGeolocationUpdate> mCallback;
+};
+
+PortalLocationProvider::MLSGeolocationUpdate::MLSGeolocationUpdate(
+ nsIGeolocationUpdate* aCallback)
+ : mCallback(aCallback) {
+ MOZ_ASSERT(mCallback);
+}
+
+NS_IMPL_ISUPPORTS(PortalLocationProvider::MLSGeolocationUpdate,
+ nsIGeolocationUpdate);
+
+// nsIGeolocationUpdate
+//
+
+NS_IMETHODIMP
+PortalLocationProvider::MLSGeolocationUpdate::Update(
+ nsIDOMGeoPosition* aPosition) {
+ nsCOMPtr<nsIDOMGeoPositionCoords> coords;
+ aPosition->GetCoords(getter_AddRefs(coords));
+ if (!coords) {
+ return NS_ERROR_FAILURE;
+ }
+ LOG_PORTAL("MLS is updating position\n");
+ return mCallback->Update(aPosition);
+}
+
+NS_IMETHODIMP
+PortalLocationProvider::MLSGeolocationUpdate::NotifyError(uint16_t aError) {
+ nsCOMPtr<nsIGeolocationUpdate> callback(mCallback);
+ return callback->NotifyError(aError);
+}
+
+//
+// PortalLocationProvider
+//
+
+PortalLocationProvider::PortalLocationProvider() = default;
+
+PortalLocationProvider::~PortalLocationProvider() {
+ if (mDBUSLocationProxy || mRefreshTimer || mMLSProvider) {
+ NS_WARNING(
+ "PortalLocationProvider: Shutdown() had not been called before "
+ "destructor.");
+ Shutdown();
+ }
+}
+
+void PortalLocationProvider::Update(nsIDOMGeoPosition* aPosition) {
+ if (!mCallback) {
+ return; // not initialized or already shut down
+ }
+
+ if (mMLSProvider) {
+ LOG_PORTAL(
+ "Update from location portal received: Cancelling fallback MLS "
+ "provider\n");
+ mMLSProvider->Shutdown();
+ mMLSProvider = nullptr;
+ }
+
+ LOG_PORTAL("Send updated location to the callback %p", mCallback.get());
+ mCallback->Update(aPosition);
+
+ aPosition->GetCoords(getter_AddRefs(mLastGeoPositionCoords));
+ // Schedule sending repetitive updates because we don't get more until
+ // position is changed from portal. That would lead to timeout on the
+ // Firefox side.
+ SetRefreshTimer(5000);
+}
+
+void PortalLocationProvider::NotifyError(int aError) {
+ LOG_PORTAL("*****NotifyError %d\n", aError);
+ if (!mCallback) {
+ return; // not initialized or already shut down
+ }
+
+ if (!mMLSProvider) {
+ /* With Portal failed, we restart MLS. It will be canceled once we
+ * get another location from Portal. Start it immediately.
+ */
+ mMLSProvider = MakeAndAddRef<MLSFallback>(0);
+ mMLSProvider->Startup(new MLSGeolocationUpdate(mCallback));
+ }
+
+ nsCOMPtr<nsIGeolocationUpdate> callback(mCallback);
+ callback->NotifyError(aError);
+}
+
+NS_IMPL_ISUPPORTS(PortalLocationProvider, nsIGeolocationProvider)
+
+static void location_updated_signal_cb(GDBusProxy* proxy, gchar* sender_name,
+ gchar* signal_name, GVariant* parameters,
+ gpointer user_data) {
+ LOG_PORTAL("Signal: %s received from: %s\n", sender_name, signal_name);
+
+ if (g_strcmp0(signal_name, "LocationUpdated")) {
+ LOG_PORTAL("Unexpected signal %s received", signal_name);
+ return;
+ }
+
+ auto* locationProvider = static_cast<PortalLocationProvider*>(user_data);
+ RefPtr<GVariant> response_data;
+ gchar* session_handle;
+ g_variant_get(parameters, "(o@a{sv})", &session_handle,
+ response_data.StartAssignment());
+ if (!response_data) {
+ LOG_PORTAL("Missing response data from portal\n");
+ locationProvider->NotifyError(
+ GeolocationPositionError_Binding::POSITION_UNAVAILABLE);
+ return;
+ }
+ LOG_PORTAL("Session handle: %s Response data: %s\n", session_handle,
+ GUniquePtr<gchar>(g_variant_print(response_data, TRUE)).get());
+ g_free(session_handle);
+
+ double lat = 0;
+ double lon = 0;
+ if (!g_variant_lookup(response_data, "Latitude", "d", &lat) ||
+ !g_variant_lookup(response_data, "Longitude", "d", &lon)) {
+ locationProvider->NotifyError(
+ GeolocationPositionError_Binding::POSITION_UNAVAILABLE);
+ return;
+ }
+
+ double alt = UnspecifiedNaN<double>();
+ g_variant_lookup(response_data, "Altitude", "d", &alt);
+ double vError = 0;
+ double hError = UnspecifiedNaN<double>();
+ g_variant_lookup(response_data, "Accuracy", "d", &hError);
+ double heading = UnspecifiedNaN<double>();
+ g_variant_lookup(response_data, "Heading", "d", &heading);
+ double speed = UnspecifiedNaN<double>();
+ g_variant_lookup(response_data, "Speed", "d", &speed);
+
+ locationProvider->Update(new nsGeoPosition(lat, lon, alt, hError, vError,
+ heading, speed,
+ PR_Now() / PR_USEC_PER_MSEC));
+}
+
+NS_IMETHODIMP
+PortalLocationProvider::Startup() {
+ LOG_PORTAL("Starting location portal");
+ if (mDBUSLocationProxy) {
+ LOG_PORTAL("Proxy already started.\n");
+ return NS_OK;
+ }
+
+ // Create dbus proxy for the Location portal
+ GUniquePtr<GError> error;
+ mDBUSLocationProxy = dont_AddRef(g_dbus_proxy_new_for_bus_sync(
+ G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE,
+ nullptr, /* GDBusInterfaceInfo */
+ kDesktopBusName, "/org/freedesktop/portal/desktop",
+ "org.freedesktop.portal.Location", nullptr, /* GCancellable */
+ getter_Transfers(error)));
+ if (!mDBUSLocationProxy) {
+ g_printerr("Error creating location dbus proxy: %s\n", error->message);
+ return NS_OK; // fallback to MLS
+ }
+
+ // Listen to signals which will be send to us with the location data
+ mDBUSSignalHandler =
+ g_signal_connect(mDBUSLocationProxy, "g-signal",
+ G_CALLBACK(location_updated_signal_cb), this);
+
+ // Call CreateSession of the location portal
+ GVariantBuilder builder;
+
+ nsAutoCString appName;
+ gAppData->GetDBusAppName(appName);
+ g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
+ g_variant_builder_add(&builder, "{sv}", "session_handle_token",
+ g_variant_new_string(appName.get()));
+
+ RefPtr<GVariant> result = dont_AddRef(g_dbus_proxy_call_sync(
+ mDBUSLocationProxy, "CreateSession", g_variant_new("(a{sv})", &builder),
+ G_DBUS_CALL_FLAGS_NONE, -1, nullptr, getter_Transfers(error)));
+
+ g_variant_builder_clear(&builder);
+
+ if (!result) {
+ g_printerr("Error calling CreateSession method: %s\n", error->message);
+ return NS_OK; // fallback to MLS
+ }
+
+ // Start to listen to the location changes
+ g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
+
+ // TODO Use wayland:handle as described in
+ // https://flatpak.github.io/xdg-desktop-portal/#parent_window
+ const gchar* parent_window = "";
+ gchar* portalSession;
+ g_variant_get_child(result, 0, "o", &portalSession);
+ mPortalSession.reset(portalSession);
+
+ result = g_dbus_proxy_call_sync(
+ mDBUSLocationProxy, "Start",
+ g_variant_new("(osa{sv})", mPortalSession.get(), parent_window, &builder),
+ G_DBUS_CALL_FLAGS_NONE, -1, nullptr, getter_Transfers(error));
+
+ g_variant_builder_clear(&builder);
+
+ if (!result) {
+ g_printerr("Error calling Start method: %s\n", error->message);
+ return NS_OK; // fallback to MLS
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PortalLocationProvider::Watch(nsIGeolocationUpdate* aCallback) {
+ mCallback = aCallback;
+
+ if (mLastGeoPositionCoords) {
+ // We cannot immediately call the Update there becase the window is not
+ // yet ready for that.
+ LOG_PORTAL(
+ "Update location in 1ms because we have the valid coords cached.");
+ SetRefreshTimer(1);
+ return NS_OK;
+ }
+
+ /* The MLS fallback will kick in after 12 seconds if portal
+ * doesn't provide location information within time. Once we
+ * see the first message from portal, the fallback will be
+ * disabled in |Update|.
+ */
+ mMLSProvider = MakeAndAddRef<MLSFallback>(12000);
+ mMLSProvider->Startup(new MLSGeolocationUpdate(aCallback));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP PortalLocationProvider::GetName(nsACString& aName) {
+ aName.AssignLiteral("PortalLocationProvider");
+ return NS_OK;
+}
+
+void PortalLocationProvider::SetRefreshTimer(int aDelay) {
+ LOG_PORTAL("SetRefreshTimer for %p to %d ms\n", this, aDelay);
+ if (!mRefreshTimer) {
+ NS_NewTimerWithCallback(getter_AddRefs(mRefreshTimer), this, aDelay,
+ nsITimer::TYPE_ONE_SHOT);
+ } else {
+ mRefreshTimer->Cancel();
+ mRefreshTimer->InitWithCallback(this, aDelay, nsITimer::TYPE_ONE_SHOT);
+ }
+}
+
+NS_IMETHODIMP
+PortalLocationProvider::Notify(nsITimer* timer) {
+ // We need to reschedule the timer because we won't get any update
+ // from portal until the location is changed. That would cause
+ // watchPosition to fail with TIMEOUT error.
+ SetRefreshTimer(5000);
+ if (mLastGeoPositionCoords) {
+ LOG_PORTAL("Update location callback with latest coords.");
+ mCallback->Update(
+ new nsGeoPosition(mLastGeoPositionCoords, PR_Now() / PR_USEC_PER_MSEC));
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PortalLocationProvider::Shutdown() {
+ LOG_PORTAL("Shutdown location provider");
+ if (mRefreshTimer) {
+ mRefreshTimer->Cancel();
+ mRefreshTimer = nullptr;
+ }
+ mLastGeoPositionCoords = nullptr;
+ if (mDBUSLocationProxy) {
+ g_signal_handler_disconnect(mDBUSLocationProxy, mDBUSSignalHandler);
+ LOG_PORTAL("calling Close method to the session interface...\n");
+ RefPtr<GDBusMessage> message = dont_AddRef(g_dbus_message_new_method_call(
+ kDesktopBusName, mPortalSession.get(), kSessionInterfaceName, "Close"));
+ mPortalSession = nullptr;
+ if (message) {
+ GUniquePtr<GError> error;
+ GDBusConnection* connection =
+ g_dbus_proxy_get_connection(mDBUSLocationProxy);
+ g_dbus_connection_send_message(
+ connection, message, G_DBUS_SEND_MESSAGE_FLAGS_NONE,
+ /*out_serial=*/nullptr, getter_Transfers(error));
+ if (error) {
+ g_printerr("Failed to close the session: %s\n", error->message);
+ }
+ }
+ mDBUSLocationProxy = nullptr;
+ }
+ if (mMLSProvider) {
+ mMLSProvider->Shutdown();
+ mMLSProvider = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PortalLocationProvider::SetHighAccuracy(bool aHigh) { return NS_OK; }
+
+} // namespace mozilla::dom
diff --git a/dom/system/linux/PortalLocationProvider.h b/dom/system/linux/PortalLocationProvider.h
new file mode 100644
index 0000000000..e7ead0ab5c
--- /dev/null
+++ b/dom/system/linux/PortalLocationProvider.h
@@ -0,0 +1,54 @@
+/* -*- 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 PortalLocationProvider_h
+#define PortalLocationProvider_h
+
+#include "nsCOMPtr.h"
+#include "mozilla/GRefPtr.h"
+#include "mozilla/GUniquePtr.h"
+#include "Geolocation.h"
+#include "nsIGeolocationProvider.h"
+#include <gio/gio.h>
+
+class MLSFallback;
+
+namespace mozilla::dom {
+
+class PortalLocationProvider final : public nsIGeolocationProvider,
+ public nsITimerCallback,
+ public nsINamed {
+ class MLSGeolocationUpdate;
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIGEOLOCATIONPROVIDER
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSINAMED
+
+ PortalLocationProvider();
+
+ void Update(nsIDOMGeoPosition* aPosition);
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ void NotifyError(int aError);
+
+ private:
+ ~PortalLocationProvider();
+ void SetRefreshTimer(int aDelay);
+
+ RefPtr<GDBusProxy> mDBUSLocationProxy;
+ gulong mDBUSSignalHandler = 0;
+
+ GUniquePtr<gchar> mPortalSession;
+ nsCOMPtr<nsIGeolocationUpdate> mCallback;
+ RefPtr<MLSFallback> mMLSProvider;
+ nsCOMPtr<nsIDOMGeoPositionCoords> mLastGeoPositionCoords;
+ nsCOMPtr<nsITimer> mRefreshTimer;
+};
+
+} // namespace mozilla::dom
+
+#endif /* GpsLocationProvider_h */
diff --git a/dom/system/linux/moz.build b/dom/system/linux/moz.build
new file mode 100644
index 0000000000..9e7d6ef31d
--- /dev/null
+++ b/dom/system/linux/moz.build
@@ -0,0 +1,23 @@
+# -*- 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"]
+
+
+if CONFIG["MOZ_ENABLE_DBUS"]:
+ SOURCES += ["GeoclueLocationProvider.cpp"]
+ SOURCES += ["PortalLocationProvider.cpp"]
+ LOCAL_INCLUDES += ["/dom/geolocation"]
+ CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+
+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..2e9fee320b
--- /dev/null
+++ b/dom/system/mac/CoreLocationLocationProvider.mm
@@ -0,0 +1,253 @@
+/* -*- 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 "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());
+
+ // 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..82e9edf1ac
--- /dev/null
+++ b/dom/system/mac/nsOSPermissionRequest.mm
@@ -0,0 +1,65 @@
+/* -*- 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 "nsCocoaUtils.h"
+
+using namespace mozilla;
+
+using mozilla::dom::Promise;
+
+NS_IMETHODIMP
+nsOSPermissionRequest::GetAudioCapturePermissionState(uint16_t* aAudio) {
+ MOZ_ASSERT(aAudio);
+ return nsCocoaUtils::GetAudioCapturePermissionState(*aAudio);
+}
+
+NS_IMETHODIMP
+nsOSPermissionRequest::GetVideoCapturePermissionState(uint16_t* aVideo) {
+ MOZ_ASSERT(aVideo);
+ return nsCocoaUtils::GetVideoCapturePermissionState(*aVideo);
+}
+
+NS_IMETHODIMP
+nsOSPermissionRequest::GetScreenCapturePermissionState(uint16_t* aScreen) {
+ MOZ_ASSERT(aScreen);
+ return nsCocoaUtils::GetScreenCapturePermissionState(*aScreen);
+}
+
+NS_IMETHODIMP
+nsOSPermissionRequest::RequestVideoCapturePermission(JSContext* aCx,
+ Promise** 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) {
+ 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() {
+ return nsCocoaUtils::MaybeRequestScreenCapturePermission();
+}
diff --git a/dom/system/moz.build b/dom/system/moz.build
new file mode 100644
index 0000000000..e702d44183
--- /dev/null
+++ b/dom/system/moz.build
@@ -0,0 +1,113 @@
+# -*- 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("*ocationProvider*"):
+ BUG_COMPONENT = ("Core", "DOM: Geolocation")
+
+with Files("windows/*LocationProvider*"):
+ BUG_COMPONENT = ("Core", "DOM: Geolocation")
+
+with Files("IOUtils*"):
+ BUG_COMPONENT = ("Toolkit", "IOUtils and PathUtils")
+
+with Files("PathUtils*"):
+ BUG_COMPONENT = ("Toolkit", "IOUtils and PathUtils")
+
+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.toml"):
+ BUG_COMPONENT = ("Toolkit", "IOUtils and PathUtils")
+
+with Files("tests/*constants*"):
+ BUG_COMPONENT = ("Toolkit", "IOUtils and PathUtils")
+
+with Files("tests/ioutils/**"):
+ BUG_COMPONENT = ("Toolkit", "IOUtils and PathUtils")
+
+with Files("tests/mochitest.toml"):
+ BUG_COMPONENT = ("Core", "DOM: Device Interfaces")
+
+with Files("test/*pathutils*"):
+ BUG_COMPONENT = ("Toolkit", "IOUtils and PathUtils")
+
+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 += [
+ "nsIOSPermissionRequest.idl",
+]
+
+XPIDL_MODULE = "dom_system"
+
+EXPORTS += [
+ "nsDeviceSensors.h",
+ "nsOSPermissionRequestBase.h",
+]
+
+EXPORTS.mozilla.dom += [
+ "IOUtils.h",
+ "PathUtils.h",
+]
+
+UNIFIED_SOURCES += [
+ "IOUtils.cpp",
+ "nsDeviceSensors.cpp",
+ "nsOSPermissionRequestBase.cpp",
+ "PathUtils.cpp",
+]
+
+EXTRA_JS_MODULES += [
+ "NetworkGeolocationProvider.sys.mjs",
+]
+
+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.toml", "tests/ioutils/chrome.toml"]
+MOCHITEST_MANIFESTS += ["tests/mochitest.toml"]
diff --git a/dom/system/nsDeviceSensors.cpp b/dom/system/nsDeviceSensors.cpp
new file mode 100644
index 0000000000..c7fc67b52c
--- /dev/null
+++ b/dom/system/nsDeviceSensors.cpp
@@ -0,0 +1,557 @@
+/* -*- 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 "nsGlobalWindowInner.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/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) {
+ MaybeFireDOMUserProximityEvent(target, x, 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::MaybeFireDOMUserProximityEvent(
+ mozilla::dom::EventTarget* aTarget, double aValue, double aMax) {
+ 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"deviceorientationabsolute"_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;
+ }
+ 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;
+ }
+ 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;
+ }
+ 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;
+ }
+ if (doc) {
+ doc->WarnOnceAbout(DeprecatedOperations::eAmbientLightEvent, true);
+ }
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Device sensor type not recognised");
+ return false;
+ }
+
+ if (!window) {
+ return true;
+ }
+ return !nsGlobalWindowInner::Cast(window)->ShouldResistFingerprinting(
+ RFPTarget::DeviceSensors);
+}
diff --git a/dom/system/nsDeviceSensors.h b/dom/system/nsDeviceSensors.h
new file mode 100644
index 0000000000..dd079088e6
--- /dev/null
+++ b/dom/system/nsDeviceSensors.h
@@ -0,0 +1,72 @@
+/* -*- 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"
+
+class nsIDOMWindow;
+
+namespace mozilla::dom {
+class Document;
+class EventTarget;
+} // namespace mozilla::dom
+
+class nsDeviceSensors : public nsIDeviceSensors,
+ public mozilla::hal::ISensorObserver {
+ using DeviceAccelerationInit = mozilla::dom::DeviceAccelerationInit;
+ using DeviceRotationRateInit = mozilla::dom::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 MaybeFireDOMUserProximityEvent(mozilla::dom::EventTarget* aTarget,
+ double aValue, 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/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..e1037b6047
--- /dev/null
+++ b/dom/system/nsOSPermissionRequestBase.h
@@ -0,0 +1,38 @@
+/* -*- 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::dom {
+class Promise;
+} // namespace mozilla::dom
+
+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/chrome.toml b/dom/system/tests/chrome.toml
new file mode 100644
index 0000000000..87f782b465
--- /dev/null
+++ b/dom/system/tests/chrome.toml
@@ -0,0 +1,6 @@
+[DEFAULT]
+
+["test_pathutils.html"]
+
+["test_pathutils_worker.xhtml"]
+support-files = ["pathutils_worker.js"]
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.toml b/dom/system/tests/ioutils/chrome.toml
new file mode 100644
index 0000000000..16434b5bb6
--- /dev/null
+++ b/dom/system/tests/ioutils/chrome.toml
@@ -0,0 +1,39 @@
+[DEFAULT]
+support-files = [
+ "file_ioutils_test_fixtures.js",
+ "file_ioutils_worker.js",
+]
+
+["test_ioutils.html"]
+
+["test_ioutils_compute_hex_digest.html"]
+
+["test_ioutils_copy_move.html"]
+
+["test_ioutils_create_unique.html"]
+
+["test_ioutils_dir_iteration.html"]
+
+["test_ioutils_getfile.html"]
+
+["test_ioutils_mac_xattr.html"]
+skip-if = ["(os != 'mac')"]
+
+["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_set_permissions.html"]
+
+["test_ioutils_stat_set_modification_time.html"]
+
+["test_ioutils_windows_file_attributes.html"]
+skip-if = ["(os != 'win')"]
+
+["test_ioutils_worker.xhtml"]
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..e367eb4d99
--- /dev/null
+++ b/dom/system/tests/ioutils/file_ioutils_worker.js
@@ -0,0 +1,219 @@
+// Any copyright is dedicated to the Public Domain.
+// - http://creativecommons.org/publicdomain/zero/1.0/
+
+// Portions of this file are originally from narwhal.js (http://narwhaljs.org)
+// Copyright (c) 2009 Thomas Robinson <280north.com>
+// MIT license: http://opensource.org/licenses/MIT
+
+/* eslint-env worker */
+
+"use strict";
+
+/* import-globals-from /testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js */
+importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js");
+
+importScripts("file_ioutils_test_fixtures.js");
+
+self.onmessage = async function (msg) {
+ const tmpDir = await PathUtils.getTempDir();
+
+ // 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 = 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.
+ let fileContents = await IOUtils.read(tmpFileName);
+ ok(
+ _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(
+ _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 = PathUtils.join(tmpDir, "test_move_file_src.tmp");
+ const dest = PathUtils.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 = PathUtils.join(tmpDir, "test_ioutils_orig.tmp");
+ const destFileName = PathUtils.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 = PathUtils.join(tmpDir, "test_make_dir.tmp.d");
+ await IOUtils.makeDirectory(dir);
+ const stat = await IOUtils.stat(dir);
+ is(
+ stat.type,
+ "directory",
+ "IOUtils::makeDirectory can make a new directory from a worker"
+ );
+
+ await cleanup(dir);
+ }
+};
+
+// This is copied from the ObjectUtils module, as it is difficult to translate
+// file_ioutils_test_fixtures.js into a ES module and have it used in non-module
+// contexts.
+
+// ... Start of previously MIT-licensed code.
+// This deepEqual implementation is originally from narwhal.js (http://narwhaljs.org)
+// Copyright (c) 2009 Thomas Robinson <280north.com>
+// MIT license: http://opensource.org/licenses/MIT
+
+function _deepEqual(a, b) {
+ // The numbering below refers to sections in the CommonJS spec.
+
+ // 7.1 All identical values are equivalent, as determined by ===.
+ if (a === b) {
+ return true;
+ // 7.2 If the b value is a Date object, the a value is
+ // equivalent if it is also a Date object that refers to the same time.
+ }
+ let aIsDate = instanceOf(a, "Date");
+ let bIsDate = instanceOf(b, "Date");
+ if (aIsDate || bIsDate) {
+ if (!aIsDate || !bIsDate) {
+ return false;
+ }
+ if (isNaN(a.getTime()) && isNaN(b.getTime())) {
+ return true;
+ }
+ return a.getTime() === b.getTime();
+ // 7.3 If the b value is a RegExp object, the a value is
+ // equivalent if it is also a RegExp object with the same source and
+ // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`).
+ }
+ let aIsRegExp = instanceOf(a, "RegExp");
+ let bIsRegExp = instanceOf(b, "RegExp");
+ if (aIsRegExp || bIsRegExp) {
+ return (
+ aIsRegExp &&
+ bIsRegExp &&
+ a.source === b.source &&
+ a.global === b.global &&
+ a.multiline === b.multiline &&
+ a.lastIndex === b.lastIndex &&
+ a.ignoreCase === b.ignoreCase
+ );
+ // 7.4 Other pairs that do not both pass typeof value == "object",
+ // equivalence is determined by ==.
+ }
+ if (typeof a != "object" || typeof b != "object") {
+ return a == b;
+ }
+ // 7.5 For all other Object pairs, including Array objects, equivalence is
+ // determined by having the same number of owned properties (as verified
+ // with Object.prototype.hasOwnProperty.call), the same set of keys
+ // (although not necessarily the same order), equivalent values for every
+ // corresponding key, and an identical 'prototype' property. Note: this
+ // accounts for both named and indexed properties on Arrays.
+ return objEquiv(a, b);
+}
+
+function instanceOf(object, type) {
+ return Object.prototype.toString.call(object) == "[object " + type + "]";
+}
+
+function isUndefinedOrNull(value) {
+ return value === null || value === undefined;
+}
+
+function isArguments(object) {
+ return instanceOf(object, "Arguments");
+}
+
+function objEquiv(a, b) {
+ if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) {
+ return false;
+ }
+ // An identical 'prototype' property.
+ if ((a.prototype || undefined) != (b.prototype || undefined)) {
+ return false;
+ }
+ // Object.keys may be broken through screwy arguments passing. Converting to
+ // an array solves the problem.
+ if (isArguments(a)) {
+ if (!isArguments(b)) {
+ return false;
+ }
+ a = Array.prototype.slice.call(a);
+ b = Array.prototype.slice.call(b);
+ return _deepEqual(a, b);
+ }
+ let ka, kb;
+ try {
+ ka = Object.keys(a);
+ kb = Object.keys(b);
+ } catch (e) {
+ // Happens when one is a string literal and the other isn't
+ return false;
+ }
+ // Having the same number of owned properties (keys incorporates
+ // hasOwnProperty)
+ if (ka.length != kb.length) {
+ return false;
+ }
+ // The same set of keys (although not necessarily the same order),
+ ka.sort();
+ kb.sort();
+ // Equivalent values for every corresponding key, and possibly expensive deep
+ // test
+ for (let key of ka) {
+ if (!_deepEqual(a[key], b[key])) {
+ return false;
+ }
+ }
+ return true;
+}
+
+// ... End of previously MIT-licensed code.
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_compute_hex_digest.html b/dom/system/tests/ioutils/test_ioutils_compute_hex_digest.html
new file mode 100644
index 0000000000..7a98f83a1f
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_compute_hex_digest.html
@@ -0,0 +1,55 @@
+
+<!-- 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.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ add_task(async function test_computeHexDigest() {
+ const tempDir = PathUtils.join(PathUtils.tempDir, "ioutils-test-compute-hex-digest.tmp.d");
+ await createDir(tempDir);
+
+ const path = PathUtils.join(tempDir, "file");
+ await IOUtils.writeUTF8(path, "hello world\n");
+
+ const DIGESTS = [
+ "a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447",
+ "6b3b69ff0a404f28d75e98a066d3fc64fffd9940870cc68bece28545b9a75086b343d7a1366838083e4b8f3ca6fd3c80",
+ "db3974a97f2407b7cae1ae637c0030687a11913274d578492558e39c16c017de84eacdc8c62fe34ee4e12b4b1428817f09b6a2760c3f8a664ceae94d2434a593",
+ ];
+ const ALGORITHMS = ["sha256", "sha384", "sha512"];
+
+ for (let i = 0; i < ALGORITHMS.length; i++) {
+ const alg = ALGORITHMS[i];
+ const expected = DIGESTS[i];
+
+ Assert.equal(
+ await IOUtils.computeHexDigest(path, alg),
+ expected,
+ `IOUtils.hashFile() has expected value for ${alg}`);
+ }
+
+ await cleanup(tempDir);
+ });
+ </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..408bb82f39
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_copy_move.html
@@ -0,0 +1,360 @@
+<!-- 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.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ add_task(async function test_move_relative_path() {
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "test_ioutils_move_src.tmp");
+ const destFileName = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "test_move_to_dir.tmp");
+ const destDir = PathUtils.join(PathUtils.tempDir, "test_move_to_dir.tmp.d");
+ const dest = PathUtils.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(PathUtils.join(destDir, PathUtils.filename(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 = PathUtils.join(PathUtils.tempDir, "test_move_dir.tmp.d");
+ const destDir = PathUtils.join(PathUtils.tempDir, "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(PathUtils.join(srcDir, "file.tmp"), "foo");
+ // Test.
+ await IOUtils.move(srcDir, destDir);
+ const destFile = PathUtils.join(destDir, PathUtils.filename(srcDir), "file.tmp");
+ ok(
+ !await IOUtils.exists(srcDir)
+ && await dirExists(destDir)
+ && await dirExists(PathUtils.join(destDir, PathUtils.filename(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 = PathUtils.join(PathUtils.tempDir, "not_exists_src.tmp");
+ const notExistsDest = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "test_move_failures_file_dest.tmp");
+ const srcDir = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "test_ioutils_orig.tmp");
+ const destFileName = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "test_copy_file_to_dir.tmp");
+ const destDir = PathUtils.join(PathUtils.tempDir, "test_copy_file_to_dir.tmp.d");
+ const dest = PathUtils.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(PathUtils.join(destDir, PathUtils.filename(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 = PathUtils.join(PathUtils.tempDir, "test_copy_dir.tmp.d");
+ const destDir = PathUtils.join(PathUtils.tempDir, "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(PathUtils.join(srcDir, "file.tmp"), "foo");
+ // Test.
+ await IOUtils.copy(srcDir, destDir, { recursive: true });
+ const destFile = PathUtils.join(destDir, PathUtils.filename(srcDir), "file.tmp");
+ ok(
+ await dirExists(srcDir)
+ && await dirExists(destDir)
+ && await dirExists(PathUtils.join(destDir, PathUtils.filename(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 = PathUtils.join(PathUtils.tempDir, "test_copy_not_exists_src.tmp");
+ const notExistsDest = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "test_copy_failures_file_dest.tmp");
+ const srcDir = PathUtils.join(PathUtils.tempDir, "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_create_unique.html b/dom/system/tests/ioutils/test_ioutils_create_unique.html
new file mode 100644
index 0000000000..be7ab23697
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_create_unique.html
@@ -0,0 +1,86 @@
+<!-- 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";
+
+ function octalFormat(n) {
+ let s = n.toString(8);
+ while (s.length < 3) {
+ s = `0${s}`;
+ }
+ return `0o${s}`;
+ }
+
+ async function check(method, path, prefix, type, perms) {
+ const filename = PathUtils.filename(path);
+
+ ok(filename.startsWith(prefix), `IOUtils.${method} uses the prefix`);
+ ok(await IOUtils.exists(path), `IOUtils.${method} creates a file`);
+
+ const stat = await IOUtils.stat(path);
+ is(stat.type, type, `IOUtils.${method} creates a "${type}" file`);
+
+ is(
+ octalFormat(stat.permissions),
+ octalFormat(perms),
+ `IOUtils.${method} creates a file with the correct permissions`
+ );
+ }
+
+ add_task(async function test_createUnique() {
+ const tempDir = PathUtils.join(
+ PathUtils.tempDir,
+ "test_createUnique.tmp.d"
+ );
+
+ const filesToChmod = [];
+
+ SimpleTest.registerCleanupFunction(async function test_createUnique_cleanup() {
+ for (const file of filesToChmod) {
+ if (await IOUtils.exists(file)) {
+ await IOUtils.setPermissions(file, 0o666);
+ }
+ }
+
+ await IOUtils.remove(tempDir, { recursive: true });
+ });
+
+ const isWindows = Services.appinfo.OS === "WINNT";
+
+ info("Creating a unique directory")
+ const dir = await IOUtils.createUniqueDirectory(tempDir, "unique-dir", 0o600);
+ await check("createUniqueDirectory", dir, "unique-dir", "directory", isWindows ? 0o666 : 0o600);
+
+ info("Creating a unique directory with the same prefix")
+ const dir2 = await IOUtils.createUniqueDirectory(tempDir, "unique-dir", 0o700);
+ await check("createUniqueDirectory", dir2, "unique-dir", "directory", isWindows ? 0o666 : 0o700);
+ ok(dir !== dir2, "IOUtils.createUniqueDirectory creates unique paths");
+
+ info("Creating a unique file");
+ const file = await IOUtils.createUniqueFile(tempDir, "unique-file", 0o641);
+ await check("createUniqueFile", file, "unique-file", "regular", isWindows ? 0o666 : 0o641);
+
+ info("Creating a unique file with the same prefix");
+ const file2 = await IOUtils.createUniqueFile(tempDir, "unique-file", 0o400);
+ filesToChmod.push(file2);
+ await check("createUniqueFile", file2, "unique-file", "regular", isWindows ? 0o444 : 0o400);
+ });
+ </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..54168235b0
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_dir_iteration.html
@@ -0,0 +1,96 @@
+<!-- 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.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ add_task(async function iterate_dir_failure() {
+ let notExists = PathUtils.join(PathUtils.tempDir, '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 = PathUtils.join(PathUtils.tempDir, '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 = PathUtils.join(PathUtils.tempDir, 'iterator.tmp.d');
+ let child1 = PathUtils.join(root, 'child1.tmp');
+ let child2 = PathUtils.join(root, 'child2.tmp');
+ let grandchild = PathUtils.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 = PathUtils.join(PathUtils.tempDir, '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);
+ });
+
+ add_task(async function iterate_ignore_missing_dir() {
+ info("Try to get the children of a missing file with ignoreAbsent");
+
+ const notExists = PathUtils.join(PathUtils.tempDir, "does_not_exist_dir.tmp.d");
+
+ is(
+ (await IOUtils.getChildren(notExists, { ignoreAbsent: true })).length,
+ 0,
+ "IOUtils::getChildren returns an empty array when called with ignoreAbsent on a missing file"
+ );
+ ok(!await fileExists(notExists), `Expected ${notExists} not to exist`);
+ });
+ </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_getfile.html b/dom/system/tests/ioutils/test_ioutils_getfile.html
new file mode 100644
index 0000000000..077f0b5c1c
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_getfile.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>
+ "use strict";
+
+ const TEST_PATH = PathUtils.join(PathUtils.tempDir, "test-ioutils-getfile");
+
+ add_task(async function test_getFile() {
+ const expectedPath = PathUtils.join(TEST_PATH, "foo", "bar", "baz", "get-file.txt");
+ const parentPath = PathUtils.parent(expectedPath);
+
+ ok(!(await IOUtils.exists(parentPath)), "Parent directory should not exist");
+
+ const file = await IOUtils.getFile(TEST_PATH, "foo", "bar", "baz", "get-file.txt");
+ const path = file.path;
+
+ is(path, expectedPath, "Should have the correct path");
+ ok(await IOUtils.exists(parentPath), "Parent directory should be created");
+ ok(!(await IOUtils.exists(path)), "File should not be created");
+
+ await IOUtils.remove(TEST_PATH, { recursive: true });
+ });
+
+ add_task(async function test_getFile_exists() {
+ const expectedPath = PathUtils.join(TEST_PATH, "foo", "bar", "baz", "get-file-exists.txt");
+
+ await IOUtils.makeDirectory(PathUtils.parent(expectedPath));
+ await IOUtils.writeUTF8(expectedPath, "hello world");
+
+ const file = await IOUtils.getFile(TEST_PATH, "foo", "bar", "baz", "get-file-exists.txt");
+ is(file.path, expectedPath, "Should have the correct path");
+ is(await IOUtils.readUTF8(file.path), "hello world", "Contents should be unchanged");
+
+ await IOUtils.remove(TEST_PATH, { recursive: true });
+ });
+
+ add_task(async function test_getDirectory() {
+ const expectedPath = PathUtils.join(TEST_PATH, "qux", "quux", "corge");
+
+ ok(!(await IOUtils.exists(PathUtils.parent(expectedPath))), "Parent directory should not exist");
+
+ const file = await IOUtils.getDirectory(TEST_PATH, "qux", "quux", "corge");
+
+ is(file.path, expectedPath, "Should have the correct path");
+ ok(await IOUtils.exists(expectedPath), "Directory should be created");
+
+ const info = await IOUtils.stat(expectedPath);
+ is(info.type, "directory", "Should create a directory");
+
+ await IOUtils.remove(TEST_PATH, { recursive: true });
+ });
+
+ add_task(async function test_getDirectory_exists() {
+ const expectedPath = PathUtils.join(TEST_PATH, "qux", "quux", "corge");
+
+ await IOUtils.makeDirectory(expectedPath);
+
+ const file = await IOUtils.getDirectory(TEST_PATH, "qux", "quux", "corge");
+ is(file.path, expectedPath, "Should have the correct path");
+ ok(await IOUtils.exists(expectedPath), "Directory should still exist");
+
+ const info = await IOUtils.stat(expectedPath);
+ is(info.type, "directory", "Should still be a directory");
+
+ await IOUtils.remove(TEST_PATH, { recursive: true });
+ });
+ </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_mac_xattr.html b/dom/system/tests/ioutils/test_ioutils_mac_xattr.html
new file mode 100644
index 0000000000..6af9b2e6f8
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_mac_xattr.html
@@ -0,0 +1,91 @@
+<!-- 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.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ const ATTR = "bogus.attr";
+ const VALUE = new TextEncoder().encode("bogus");
+
+ add_task(async function test_macXAttr() {
+ const tmpDir = PathUtils.join(PathUtils.tempDir, "ioutils-macos-xattr.tmp.d");
+
+ await createDir(tmpDir);
+
+ const path = PathUtils.join(tmpDir, "file.tmp");
+ ok(!await IOUtils.exists(path), "File should not exist");
+ await IOUtils.writeUTF8(path, "");
+
+ ok(
+ !await IOUtils.hasMacXAttr(path, ATTR),
+ "File does not have an extended attribute at creation"
+ );
+
+ info("Testing getting an attribute that does not exist");
+ await Assert.rejects(
+ IOUtils.getMacXAttr(path, ATTR),
+ /NotFoundError: The file `.+' does not have an extended attribute/,
+ "IOUtils::getMacXAttr rejects when the attribute does not exist"
+ );
+
+ info("Testing setting an attribute");
+ await IOUtils.setMacXAttr(path, ATTR, VALUE);
+ ok(
+ await IOUtils.hasMacXAttr(path, ATTR),
+ "File has extended attribute after setting"
+ );
+
+ {
+ info("Testing getting an attribute")
+ const value = await IOUtils.getMacXAttr(path, ATTR);
+ Assert.deepEqual(
+ Array.from(value),
+ Array.from(VALUE),
+ "Attribute value should match"
+ );
+ }
+
+ info("Testing removing an attribute");
+ await IOUtils.delMacXAttr(path, ATTR);
+ await Assert.rejects(
+ IOUtils.getMacXAttr(path, ATTR),
+ /NotFoundError: The file `.+' does not have an extended attribute/,
+ "IOUtils::delMacXAttr removes the attribute"
+ );
+
+ ok(
+ !await IOUtils.hasMacXAttr(path, ATTR),
+ "File does not have extended attribute after removing"
+ );
+
+ info("Testing removing an attribute that does not exist");
+ await Assert.rejects(
+ IOUtils.delMacXAttr(path, ATTR),
+ /NotFoundError: The file `.+' does not have an extended attribute/,
+ "IOUtils::delMacXAttr rejects when the attribute does not exist"
+ );
+
+ await cleanup(tmpDir);
+ });
+ </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..6827b24cc6
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_mkdir.html
@@ -0,0 +1,135 @@
+<!-- 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.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ add_task(async function test_make_directory() {
+ info("Test creating a new directory");
+ const newDirectoryName = PathUtils.join(PathUtils.tempDir, "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(PathUtils.tempDir, "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 .*/,
+ "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 notADirFileName = PathUtils.join(PathUtils.tempDir, "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 newDir = PathUtils.join(PathUtils.tempDir, "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);
+ });
+
+ add_task(async function test_make_directory_root() {
+ if (Services.appinfo.OS === "WINNT") {
+ // We don't actually know the root drive, but we can find the root drive
+ // of the profile directory.
+ let current = PathUtils.profileDir;
+ let parent = PathUtils.parent(current);
+ while (parent !== null) {
+ current = parent;
+ parent = PathUtils.parent(current);
+ }
+ // `current` will now be a valid root directory.
+ ok(await IOUtils.exists(current), "Root directory should exist");
+
+ const DRIVE_RE = /^[A-Za-z]:$/;
+ ok(
+ current.startsWith("\\\\") || DRIVE_RE.test(current),
+ `Root directory (${current}) should be a UNC path or drive`,
+ );
+ await IOUtils.makeDirectory(current, {createAncestors: false});
+ } else {
+ ok(await IOUtils.exists("/"), "Root directory should exist");
+ await IOUtils.makeDirectory("/", {createAncestors: false});
+ }
+ });
+ </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..2243eb1eda
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_read_write.html
@@ -0,0 +1,524 @@
+<!-- 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.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+ const { ObjectUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/ObjectUtils.sys.mjs"
+ );
+
+ add_task(async function test_read_failure() {
+ const doesNotExist = PathUtils.join(PathUtils.tempDir, "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() {
+ // Make a new file, and try to write to it with overwrites disabled.
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "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, {
+ mode: "create",
+ }),
+ /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,
+ { mode: "overwrite" }
+ );
+ 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");
+
+ let fileContents = new TextEncoder().encode("Original file contents");
+ let destFileName = PathUtils.join(PathUtils.tempDir, "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");
+
+ let fileContents = new TextEncoder().encode("Original file contents");
+ let destFileName = PathUtils.join(PathUtils.tempDir, "test_write_with_backup_and_tmp_options.tmp");
+ let backupFileName = destFileName + ".backup";
+ let tmpFileName = PathUtils.join(PathUtils.tempDir, "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");
+ ok(await fileExists(destFileName), `Expected ${destFileName} to exist`);
+ 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"
+ );
+
+ info("Test backup with tmp and backup file options, existing destination and backup");
+ newFileContents = new TextEncoder().encode("Updated new file contents");
+ ok(await fileExists(destFileName), `Expected ${destFileName} to exist`);
+ ok(await fileExists(backupFileName), `Expected ${backupFileName} to exist`);
+ bytesWritten =
+ await IOUtils.write(destFileName, newFileContents, {
+ backupFile: backupFileName,
+ tmpPath: tmpFileName,
+ });
+
+ ok(!await fileExists(tmpFileName), "IOUtils::write cleans up the tmpFile");
+ ok(
+ await fileHasTextContents(backupFileName, "New file contents"),
+ "IOUtils::write can create a backup if the target file exists"
+ );
+ ok(
+ await fileHasTextContents(destFileName, "Updated 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 tmpFileName = PathUtils.join(PathUtils.tempDir, "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 tmpFileName = PathUtils.join(PathUtils.tempDir, "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.
+
+ info("Test writing to a new binary file");
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "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 tmpFileName = PathUtils.join(PathUtils.tempDir, "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_bad_call() {
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "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 tmpFileName = PathUtils.join(PathUtils.tempDir, "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);
+ });
+
+ add_task(async function test_write_directory() {
+ const fileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_directory.tmp");
+ const tmpPath = `${fileName}.tmp`;
+ const bytes = Uint8Array.of(1, 2, 3, 4);
+
+ await IOUtils.makeDirectory(fileName);
+ await Assert.rejects(
+ IOUtils.write(fileName, bytes),
+ /NotAllowedError: Could not open the file at .+ for writing/);
+
+ await Assert.rejects(
+ IOUtils.write(fileName, bytes, { tmpPath }),
+ /NotAllowedError: Could not open the file at .+ for writing/);
+
+ ok(!await IOUtils.exists(PathUtils.join(fileName, PathUtils.filename(tmpPath))));
+ });
+
+ add_task(async function test_read_offset() {
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_read_offset.tmp");
+
+ const bytes = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
+ const byteArray = Uint8Array.of(...bytes);
+
+ await IOUtils.write(tmpFileName, byteArray);
+
+ for (const offset of [0, 5]) {
+ info(`Reading bytes from offset ${offset}`);
+
+ const readBytes = await IOUtils.read(tmpFileName, { offset });
+ Assert.deepEqual(
+ Array.from(readBytes),
+ bytes.slice(offset),
+ `should have read bytes from offset ${offset}`
+ );
+ }
+
+ for (const offset of [0, 5]) {
+ info(`Reading up to 5 bytes from offset ${offset}`);
+
+ const readBytes = await IOUtils.read(tmpFileName, {offset, maxBytes: 5});
+ Assert.deepEqual(
+ Array.from(readBytes),
+ bytes.slice(offset, offset + 5),
+ `should have read 5 bytes from offset ${offset}`
+ );
+ }
+
+ {
+ info(`Reading bytes from offset 10`);
+ const readBytes = await IOUtils.read(tmpFileName, {offset: 10});
+ is(readBytes.length, 0, "should have read 0 bytes");
+ }
+
+ {
+ info(`Reading up to 10 bytes from offset 5`);
+ const readBytes = await IOUtils.read(tmpFileName, {offset: 5, maxBytes: 10});
+ is(readBytes.length, 5, "should have read 5 bytes");
+ Assert.deepEqual(
+ Array.from(readBytes),
+ bytes.slice(5, 10),
+ "should have read last 5 bytes"
+ );
+ }
+ });
+
+ add_task(async function test_write_appendOrCreate() {
+ const fileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_appendOrCreate.tmp");
+
+ await IOUtils.write(fileName, Uint8Array.of(0, 1, 2, 3, 4), { mode: "appendOrCreate" });
+
+ {
+ const contents = await IOUtils.read(fileName);
+ Assert.deepEqual(Array.from(contents), [0, 1, 2, 3, 4], "read bytes should be equal");
+ }
+
+ await IOUtils.write(fileName, Uint8Array.of(5, 6, 7, 8, 9), { mode: "appendOrCreate" });
+
+ {
+ const contents = await IOUtils.read(fileName);
+ Assert.deepEqual(Array.from(contents), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "read bytes should be equal after appendOrCreateing");
+ }
+
+ await cleanup(fileName);
+ });
+
+ add_task(async function test_write_append() {
+ const fileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_append.tmp");
+
+ await IOUtils.write(fileName, Uint8Array.of(0, 1, 2, 3, 4));
+
+ const beforeAppend = await IOUtils.read(fileName);
+ Assert.deepEqual(Array.from(beforeAppend), [0, 1, 2, 3, 4], "read bytes should be equal");
+
+ await IOUtils.write(fileName, Uint8Array.of(5, 6, 7, 8, 9), { mode: "append" });
+
+ const afterAppend = await IOUtils.read(fileName);
+ Assert.deepEqual(Array.from(afterAppend), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "read bytes should be equal after appending");
+
+ await cleanup(fileName);
+ });
+
+ add_task(async function test_write_append_no_create() {
+ const fileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_append_no_create.tmp");
+
+ await Assert.rejects(
+ IOUtils.write(fileName, Uint8Array.of(5, 6, 7, 8, 9), { mode: "append" }),
+ /NotFoundError: Could not open the file at .*/
+ );
+ });
+ </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..0acb191e1b
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_read_write_json.html
@@ -0,0 +1,193 @@
+<!-- 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.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+ const { ObjectUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/ObjectUtils.sys.mjs"
+ );
+
+ 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 filename = PathUtils.join(PathUtils.tempDir, "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(PathUtils.tempDir, "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(PathUtils.tempDir, "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 filename = PathUtils.join(PathUtils.tempDir, "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);
+ });
+
+ add_task(async function test_append_json() {
+ const filename = PathUtils.join(PathUtils.tempDir, "test_ioutils_append_json.tmp");
+
+ await IOUtils.writeJSON(filename, OBJECT);
+
+ await Assert.rejects(
+ IOUtils.writeJSON(filename, OBJECT, {mode: "append"}),
+ /NotSupportedError: IOUtils.writeJSON does not support appending to files/,
+ "IOUtils.writeJSON() cannot append"
+ );
+
+ await cleanup(filename);
+ });
+
+ add_task(async function test_read_json_bom() {
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_read_json_bom.tmp");
+ const raw = `\uFEFF${JSON.stringify({hello: "world"})}`;
+ await IOUtils.writeUTF8(tmpFileName, raw);
+
+ ok(
+ ObjectUtils.deepEqual(
+ await IOUtils.readJSON(tmpFileName),
+ { hello: "world" },
+ ),
+ "IOUtils.readJSON should skip BOM"
+ );
+
+ await IOUtils.writeUTF8(tmpFileName, raw, { compress: true });
+
+ ok(
+ ObjectUtils.deepEqual(
+ await IOUtils.readJSON(tmpFileName, { decompress: true }),
+ { hello: "world" },
+ ),
+ "IOUtils.readJSON should skip BOM for compressed 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_read_write_utf8.html b/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html
new file mode 100644
index 0000000000..cdea016732
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html
@@ -0,0 +1,384 @@
+<!-- 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.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ // 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 = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "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, {
+ mode: "create",
+ }),
+ /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,
+ { mode: "overwrite" }
+ );
+ 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 = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "test_write_utf8_with_backup_and_tmp_options.tmp");
+ let backupFileName = destFileName + ".backup";
+ let tmpFileName = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "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_bad_call() {
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "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);
+ });
+
+ add_task(async function test_skipBOM() {
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_readutf8_bom.tmp");
+
+ const raw = `\uFEFFstring`;
+
+ await IOUtils.writeUTF8(tmpFileName, raw);
+
+ is(
+ await IOUtils.readUTF8(tmpFileName),
+ "string",
+ "IOUtils.readUTF8 should skip BOM by default"
+ );
+
+ await IOUtils.writeUTF8(tmpFileName, raw, { compress: true });
+
+ is(
+ await IOUtils.readUTF8(tmpFileName, { decompress: true }),
+ "string",
+ "IOUtils.readUTF8 should skip BOM by default for compressed 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..f368fc09d3
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_remove.html
@@ -0,0 +1,118 @@
+<!-- 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.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ add_task(async function test_create_and_remove_file() {
+ info("Test creating and removing a single file");
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "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 tempDirName = PathUtils.join(PathUtils.tempDir, "test_ioutils_create_and_remove.tmp.d");
+ await IOUtils.makeDirectory(tempDirName);
+ ok(await dirExists(tempDirName), `Expected directory ${tempDirName} to exist`);
+
+ await IOUtils.remove(tempDirName);
+ ok(!await dirExists(tempDirName), "IOUtils::remove can remove empty directories");
+ });
+
+ add_task(async function test_remove_non_existing() {
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "test_ioutils_remove.tmp.d");
+ const tmpChildDir = PathUtils.join(tmpParentDir, "child.tmp.d");
+ const tmpTopLevelFileName = PathUtils.join(tmpParentDir, "top.tmp");
+ const tmpNestedFileName = PathUtils.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"
+ );
+ });
+
+ if (Services.appinfo.OS === "WINNT") {
+ add_task(async function test_remove_retry_readonly() {
+
+ const tmpDir = PathUtils.join(PathUtils.tempDir, "test_ioutils_remove_retry_readonly.tmp.d");
+ const path = PathUtils.join(tmpDir, "file.txt");
+
+ await createDir(tmpDir);
+ await createFile(path, "");
+
+ await IOUtils.setWindowsAttributes(path, { readOnly: true });
+
+ await Assert.rejects(
+ IOUtils.remove(path),
+ /NotAllowedError/,
+ "Cannot remove a readonly file by default"
+ );
+
+ Assert.ok(await fileExists(path), "File should still exist");
+
+ await IOUtils.remove(path, { retryReadonly: true });
+
+ Assert.ok(!await fileExists(path), "File should not exist");
+
+ await IOUtils.remove(tmpDir);
+ });
+ }
+ </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..36f7dab72a
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_set_permissions.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";
+
+ add_task(async function test_setPermissions() {
+ const tempFile = PathUtils.join(PathUtils.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 {
+ let umask = Services.sysinfo.getProperty("umask");
+ is(stat.permissions, 0o421 & ~umask, "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);
+ });
+
+ add_task(async function test_setPermissionsWithoutHonoringUmask() {
+ const tempFile = PathUtils.join(PathUtils.tempDir, "setPermissions.tmp");
+
+ await IOUtils.writeUTF8(tempFile, "");
+ await IOUtils.setPermissions(tempFile, 0o421, false);
+
+ 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_set_modification_time.html b/dom/system/tests/ioutils/test_ioutils_stat_set_modification_time.html
new file mode 100644
index 0000000000..e508817a41
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_stat_set_modification_time.html
@@ -0,0 +1,242 @@
+<!-- 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.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ add_task(async function test_stat() {
+ info("Test attempt to stat a regular empty file");
+
+ const emptyFileName = PathUtils.join(PathUtils.tempDir, "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(PathUtils.tempDir, "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(PathUtils.tempDir, "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"
+ );
+ Assert.less(
+ (fileInfo.lastAccessed - new Date().valueOf()),
+ 1000,
+ "IOUtils::stat can get the last access 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 notExistsFile = PathUtils.join(PathUtils.tempDir, "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_setModificationTime_and_stat() {
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_setModificationTime_and_stat.tmp");
+ {
+ info("Test attempt to setModificationTime a file");
+ await createFile(tmpFileName);
+
+ const oldFileInfo = await IOUtils.stat(tmpFileName);
+ await sleep(500);
+
+ // Now update the time stamp.
+ const stamp = await IOUtils.setModificationTime(tmpFileName);
+ const newFileInfo = await IOUtils.stat(tmpFileName);
+
+ ok(
+ newFileInfo.lastModified > oldFileInfo.lastModified,
+ "IOUtils::setModificationTime can update the lastModified time stamp on the file system"
+ );
+ is(
+ stamp,
+ newFileInfo.lastModified,
+ "IOUtils::setModificationTime returns the updated time stamp."
+ );
+ is(
+ newFileInfo.lastAccessed,
+ oldFileInfo.lastAccessed,
+ "IOUtils::setModificationTime does not change lastAccessed"
+ );
+
+ await sleep(500);
+
+ const newerStamp = await IOUtils.setAccessTime(tmpFileName);
+ const newerFileInfo = await IOUtils.stat(tmpFileName);
+
+ ok(
+ newerFileInfo.lastAccessed > newFileInfo.lastAccessed,
+ "IOUtils::setAccessTime can update the lastAccessed time stamp on the file system"
+ );
+ is(
+ newerStamp,
+ newerFileInfo.lastAccessed,
+ "IOUtils::setAccessTime returns the updated time stamp."
+ );
+ is(
+ newerFileInfo.lastModified,
+ newFileInfo.lastModified,
+ "IOUtils::setAccessTime does not change lastModified"
+ );
+ }
+
+ const tmpDirName = PathUtils.join(PathUtils.tempDir, "test_setModificationTime_and_stat.tmp.d");
+ {
+ info("Test attempt to setModificationTime a directory");
+ await createDir(tmpDirName);
+
+ const oldFileInfo = await IOUtils.stat(tmpDirName);
+ await sleep(500);
+
+ const stamp = await IOUtils.setModificationTime(tmpDirName);
+ const newFileInfo = await IOUtils.stat(tmpDirName);
+
+ ok(
+ newFileInfo.lastModified > oldFileInfo.lastModified,
+ "IOUtils::setModificationTime can update the lastModified time stamp on a directory"
+ );
+ is(
+ stamp,
+ newFileInfo.lastModified,
+ "IOUtils::setModificationTime returns the updated time stamp on a directory"
+ );
+ }
+
+ await cleanup(tmpFileName, tmpDirName);
+ });
+
+ add_task(async function test_setModificationTime_custom_mod_time() {
+ const tempFileName = PathUtils.join(PathUtils.tempDir, "test_setModificationTime_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.setModificationTime(tempFileName, future);
+ const futureInfo = await IOUtils.stat(tempFileName);
+ Assert.less(originalInfo.lastModified, futureInfo.lastModified, "IOUtils::setModificationTime can set a future modification time for the file");
+
+ is(newModTime, futureInfo.lastModified, "IOUtils::setModificationTime returns the updated time stamp");
+ is(newModTime, future, "IOUtils::setModificationTime 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.setModificationTime(tempFileName, past);
+ const pastInfo = await IOUtils.stat(tempFileName);
+ Assert.greater(originalInfo.lastModified, pastInfo.lastModified, "IOUtils::setModificationTime can set a past modification time for the file");
+
+ is(newModTime, pastInfo.lastModified, "IOUtils::setModificationTime returns the updated time stamp");
+ is(newModTime, past, "IOUtils::setModificationTime 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 tempFileName = PathUtils.join(PathUtils.tempDir, "test_stat_btime.tmp");
+ await createFile(tempFileName);
+ const originalInfo = await IOUtils.stat(tempFileName);
+
+ const future = originalInfo.lastModified + 6000;
+ await IOUtils.setModificationTime(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_setModificationTime_failures() {
+ info("Test attempt to setModificationTime a non-existing file");
+ const notExistsFile = PathUtils.join(PathUtils.tempDir, "test_setModificationTime_not_exists.tmp");
+
+ await Assert.rejects(
+ IOUtils.setModificationTime(notExistsFile),
+ /Could not set modification time of file\(.*\) because it does not exist/,
+ "IOUtils::setModificationTime throws if the target file does not exist"
+ );
+
+ info("Test attempt to set modification time to Epoch");
+ const tempFileName = PathUtils.join(PathUtils.tempDir, "test_setModificationTime_epoch.tmp");
+ await createFile(tempFileName);
+
+ await Assert.rejects(
+ IOUtils.setModificationTime(tempFileName, 0),
+ /Refusing to set the modification time of file\(.*\) to 0/,
+ "IOUtils::setModificationTime 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_windows_file_attributes.html b/dom/system/tests/ioutils/test_ioutils_windows_file_attributes.html
new file mode 100644
index 0000000000..a5b72bd078
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_windows_file_attributes.html
@@ -0,0 +1,137 @@
+<!-- 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.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ add_task(async function getSetWindowsAttributes() {
+ const tmpDir = PathUtils.join(PathUtils.tempDir, "ioutils-windows-attributes.tmp.d");
+ await createDir(tmpDir);
+ ok(await dirExists(tmpDir), `Expected ${tmpDir} to be a directory`);
+
+ const filePath = PathUtils.join(tmpDir, "file.tmp");
+ await createFile(filePath);
+ ok(await fileExists(filePath), `Expected ${filePath} to exist`);
+
+ {
+ info("Getting attributes for newly created file.");
+ const attrs = await IOUtils.getWindowsAttributes(filePath);
+
+ ok(attrs.readOnly === false, `Newly created file ${filePath} is not a read-only file`);
+ ok(attrs.hidden === false, `Newly created file ${filePath} is not a hidden file`);
+ ok(attrs.system === false, `Newly created file ${filePath} is not a system file`);
+ }
+
+ {
+ info("Setting read-only on an existing file.");
+ await IOUtils.setWindowsAttributes(filePath, { readOnly: true })
+ const attrs = await IOUtils.getWindowsAttributes(filePath);
+
+ ok(attrs.readOnly === true, `Updated file ${filePath} is a read-only file`);
+ ok(attrs.hidden === false, `Updated file ${filePath} is not a hidden file`);
+ ok(attrs.system === false, `Updated file ${filePath} is not a system file`);
+ }
+
+ info("Attempting to write to a read-only file.");
+
+ await Assert.rejects(
+ IOUtils.writeUTF8(filePath, "hello, world"),
+ /NotAllowedError: Could not open the file at .+ for writing/,
+ "IOUtils::writeUTF8 on a read-only file fails."
+ );
+
+ {
+ info("Setting hidden on an existing file.");
+ await IOUtils.setWindowsAttributes(filePath, { hidden: true })
+ const attrs = await IOUtils.getWindowsAttributes(filePath);
+
+ ok(attrs.readOnly === true, `Updated file ${filePath} is still a read-only file`);
+ ok(attrs.hidden === true, `Updated file ${filePath} is a hidden file`);
+ ok(attrs.system === false, `Updated file ${filePath} is not a system file`);
+ }
+
+ {
+ info("Setting system on an existing file.");
+ await IOUtils.setWindowsAttributes(filePath, { system: true })
+ const attrs = await IOUtils.getWindowsAttributes(filePath);
+
+ ok(attrs.readOnly === true, `Updated file ${filePath} is still a read-only file`);
+ ok(attrs.hidden === true, `Updated file ${filePath} is still a hidden file`);
+ ok(attrs.system === true, `Updated file ${filePath} is a system file`);
+ }
+
+ {
+ info("Clearing all Windows attributes on an existing file.");
+ await IOUtils.setWindowsAttributes(filePath, { readOnly: false, hidden: false, system: false });
+ const attrs = await IOUtils.getWindowsAttributes(filePath);
+
+ ok(attrs.readOnly === false, `Updated file ${filePath} is not a read-only file`);
+ ok(attrs.hidden === false, `Updated file ${filePath} is not a hidden file`);
+ ok(attrs.system === false, `Updated file ${filePath} is not a system file`);
+ }
+
+ {
+ info("Setting all Windows attributes on an existing file.");
+ await IOUtils.setWindowsAttributes(filePath, { readOnly: true, hidden: true, system: true });
+ const attrs = await IOUtils.getWindowsAttributes(filePath);
+
+ ok(attrs.readOnly === true, `Updated file ${filePath} is a read-only file`);
+ ok(attrs.hidden === true, `Updated file ${filePath} is a hidden file`);
+ ok(attrs.system === true, `Updated file ${filePath} is a system file`);
+ }
+
+ {
+ info("Clearing read-only on an existing file.");
+ await IOUtils.setWindowsAttributes(filePath, { readOnly: false });
+ const attrs = await IOUtils.getWindowsAttributes(filePath);
+
+ ok(attrs.readOnly === false, `Updated file ${filePath} is no longer a read-only file`);
+ ok(attrs.hidden === true, `Updated file ${filePath} is still a hidden file`);
+ ok(attrs.system === true, `Updated file ${filePath} is still a system file`);
+ }
+
+ {
+ info("Clearing hidden on an existing file.");
+ await IOUtils.setWindowsAttributes(filePath, { hidden: false });
+ const attrs = await IOUtils.getWindowsAttributes(filePath);
+
+ ok(attrs.readOnly === false, `Updated file ${filePath} is still not a read-only file`);
+ ok(attrs.hidden === false, `Updated file ${filePath} is no longer a hidden file`);
+ ok(attrs.system === true, `Updated file ${filePath} is still a system file`);
+ }
+
+ {
+ info("Clearing system on an existing file.");
+ await IOUtils.setWindowsAttributes(filePath, { system: false });
+ const attrs = await IOUtils.getWindowsAttributes(filePath);
+
+ ok(attrs.readOnly === false, `Updated file ${filePath} is still not a read-only file`);
+ ok(attrs.hidden === false, `Updated file ${filePath} is sitll not a hidden file`);
+ ok(attrs.system === false, `Updated file ${filePath} is no longer a system file`);
+ }
+
+ await cleanup(tmpDir);
+ });
+
+ </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/mochitest.toml b/dom/system/tests/mochitest.toml
new file mode 100644
index 0000000000..b607853077
--- /dev/null
+++ b/dom/system/tests/mochitest.toml
@@ -0,0 +1,11 @@
+[DEFAULT]
+tags = "condprof"
+scheme = "https"
+
+support-files = ["file_bug1197901.html"]
+
+["test_bug1197901.html"]
+skip-if = [
+ "os == 'android'",
+ "condprof", #: "Only focused window should get the sensor events"
+]
diff --git a/dom/system/tests/pathutils_worker.js b/dom/system/tests/pathutils_worker.js
new file mode 100644
index 0000000000..9e4742b764
--- /dev/null
+++ b/dom/system/tests/pathutils_worker.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env worker */
+
+"use strict";
+
+/* import-globals-from /testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js */
+importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js");
+
+self.onmessage = async function (message) {
+ let expected = message.data;
+ info("ON message");
+ info(JSON.stringify(expected));
+ const profileDir = await PathUtils.getProfileDir();
+ is(
+ profileDir,
+ expected.profileDir,
+ "PathUtils.profileDir() in a worker should match PathUtils.profileDir on main thread"
+ );
+
+ const localProfileDir = await PathUtils.getLocalProfileDir();
+ is(
+ localProfileDir,
+ expected.localProfileDir,
+ "PathUtils.getLocalProfileDir() in a worker should match PathUtils.localProfileDir on main thread"
+ );
+
+ const tempDir = await PathUtils.getTempDir();
+ is(
+ tempDir,
+ expected.tempDir,
+ "PathUtils.getTempDir() in a worker should match PathUtils.tempDir on main thread"
+ );
+
+ finish();
+};
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_pathutils.html b/dom/system/tests/test_pathutils.html
new file mode 100644
index 0000000000..4391de77fd
--- /dev/null
+++ b/dom/system/tests/test_pathutils.html
@@ -0,0 +1,602 @@
+<!-- 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>
+<script src="chrome://mochikit/content/tests/SimpleTest/WorkerHandler.js"></script>
+<link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+<script>
+ "use strict";
+
+ const { Assert } = ChromeUtils.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ const UNRECOGNIZED_PATH = /Could not initialize path: NS_ERROR_FILE_UNRECOGNIZED_PATH/;
+ const SPLIT_RELATIVE_ABSOLUTE = /PathUtils.splitRelative requires a relative path/;
+ const SPLIT_RELATIVE_EMPTY = /PathUtils.splitRelative: Empty directory components \(""\) not allowed by options/;
+ const SPLIT_RELATIVE_PARENT = /PathUtils.splitRelative: Parent directory components \("\.\."\) not allowed by options/;
+ const SPLIT_RELATIVE_CURRENT = /PathUtils.splitRelative: Current directory components \("\."\) not allowed by options/;
+ 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"
+ );
+
+ Assert.throws(
+ () => PathUtils.parent("C:", -1),
+ /^NotSupportedError: PathUtils.parent: A depth of at least 1 is required/,
+ "PathUtils.parent() with a negative depth throws"
+ );
+ Assert.throws(
+ () => PathUtils.parent("C:", 0),
+ /^NotSupportedError: PathUtils.parent: A depth of at least 1 is required/,
+ "PathUtils.parent() with a zero depth throws"
+ );
+
+ {
+ const path = "C:\\Users\\User\\AppData\\Local\\Mozilla\\Firefox\\Profiles\\foo.default";
+
+ const expected = [
+ "C:\\Users\\User\\AppData\\Local\\Mozilla\\Firefox\\Profiles",
+ "C:\\Users\\User\\AppData\\Local\\Mozilla\\Firefox",
+ "C:\\Users\\User\\AppData\\Local\\Mozilla",
+ "C:\\Users\\User\\AppData\\Local",
+ "C:\\Users\\User\\AppData",
+ "C:\\Users\\User",
+ "C:\\Users",
+ "C:",
+ null,
+ ];
+
+ for (const [i, parent] of expected.entries()) {
+ is(PathUtils.parent(path, i + 1), parent, `PathUtils.parent() with depth=${i + 1}`)
+ }
+ }
+ } 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"
+ );
+
+ Assert.throws(
+ () => PathUtils.parent("/", -1),
+ /^NotSupportedError: PathUtils.parent: A depth of at least 1 is required/,
+ "PathUtils.parent() with a negative depth throws"
+ );
+ Assert.throws(
+ () => PathUtils.parent("/", 0),
+ /^NotSupportedError: PathUtils.parent: A depth of at least 1 is required/,
+ "PathUtils.parent() with a zero depth throws"
+ );
+
+ {
+ const path = "/home/user/.mozilla/firefox/foo.default";
+ const expected = [
+ "/home/user/.mozilla/firefox",
+ "/home/user/.mozilla",
+ "/home/user",
+ "/home",
+ "/",
+ null,
+ ];
+
+ for (const [i, parent] of expected.entries()) {
+ is(
+ PathUtils.parent(path, i + 1),
+ parent,
+ `PathUtils.parent() with depth=${i + 1}`
+ );
+ }
+ }
+ }
+ });
+
+ add_task(function test_join() {
+ Assert.throws(
+ () => PathUtils.join(),
+ EMPTY_PATH,
+ "PathUtils.join() does not support empty paths"
+ );
+ 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(PathUtils.tempDir),
+ "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",
+ ".",
+ "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_splitRelative() {
+ Assert.throws(
+ () => PathUtils.splitRelative(""),
+ EMPTY_PATH,
+ "PathUtils.splitRelative() should throw with empty path"
+ );
+
+ if (Services.appinfo.OS === "WINNT") {
+ Assert.throws(
+ () => PathUtils.splitRelative("C:\\"),
+ SPLIT_RELATIVE_ABSOLUTE,
+ "PathUtils.splitRelative() should throw with a drive path"
+ );
+
+ Assert.throws(
+ () => PathUtils.splitRelative("\\\\server\\share\\"),
+ SPLIT_RELATIVE_ABSOLUTE,
+ "PathUtils.splitRelative() should throw with a UNC path"
+ );
+
+ Assert.throws(
+ () => PathUtils.splitRelative("foo\\\\bar"),
+ SPLIT_RELATIVE_EMPTY,
+ "PathUtils.splitRelative() should throw with empty component by default"
+ );
+
+ Assert.deepEqual(
+ PathUtils.splitRelative("foo\\\\bar", { allowEmpty: true }),
+ ["foo", "", "bar"],
+ "PathUtils.splitRelative() with an empty component is allowed with option"
+ );
+
+ Assert.deepEqual(
+ PathUtils.splitRelative("foo"),
+ ["foo"],
+ "PathUtils.splitRelative() on a relative path with one component"
+ );
+
+ Assert.deepEqual(
+ PathUtils.splitRelative("foo\\"),
+ ["foo"],
+ "PathUtils.splitRelative() on a relative path with one component and a trailing slash"
+ );
+
+ Assert.deepEqual(
+ PathUtils.splitRelative("foo\\bar"),
+ ["foo", "bar"],
+ "PathUtils.splitRelative() on a relative path with two components"
+ );
+ } else {
+ Assert.throws(
+ () => PathUtils.splitRelative("/foo/bar"),
+ SPLIT_RELATIVE_ABSOLUTE,
+ "PathUtils.splitRelative() should throw with an absolute path"
+ );
+
+ Assert.throws(
+ () => PathUtils.splitRelative("foo//bar"),
+ SPLIT_RELATIVE_EMPTY,
+ "PathUtils.splitRelative() should throw with empty component by default"
+ );
+
+ Assert.deepEqual(
+ PathUtils.splitRelative("foo//bar", { allowEmpty: true }),
+ ["foo", "", "bar"],
+ "PathUtils.splitRelative() with an empty component is allowed with option"
+ );
+
+ Assert.deepEqual(
+ PathUtils.splitRelative("foo"),
+ ["foo"],
+ "PathUtils.splitRelative() on a relative path with one component"
+ );
+
+ Assert.deepEqual(
+ PathUtils.splitRelative("foo/"),
+ ["foo"],
+ "PathUtils.splitRelative() on a relative path with one component and a trailing slash"
+ );
+
+ Assert.deepEqual(
+ PathUtils.splitRelative("foo/bar"),
+ ["foo", "bar"],
+ "PathUtils.splitRelative() on a relative path with two components",
+ );
+ }
+
+ Assert.throws(
+ () => PathUtils.splitRelative("."),
+ SPLIT_RELATIVE_CURRENT,
+ "PathUtils.splitRelative() with a current dir component is disallowed by default"
+ );
+
+ Assert.throws(
+ () => PathUtils.splitRelative(".."),
+ SPLIT_RELATIVE_PARENT,
+ "PathUtils.splitRelative() with a parent dir component is disallowed by default"
+ );
+
+ Assert.deepEqual(
+ PathUtils.splitRelative(".", { allowCurrentDir: true }),
+ ["."],
+ "PathUtils.splitRelative() with a current dir component is allowed with option"
+ );
+
+ Assert.deepEqual(
+ PathUtils.splitRelative("..", { allowParentDir: true }),
+ [".."],
+ "PathUtils.splitRelative() with a parent dir component is allowed with option"
+ );
+ });
+
+ 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_isAbsolute() {
+ if (Services.appinfo.OS === "WINNT") {
+ ok(PathUtils.isAbsolute("C:"), "Drive paths are absolute paths on Windows");
+ ok(PathUtils.isAbsolute("C:\\Windows"), "Paths from the root are absolute paths on Windows");
+ ok(!PathUtils.isAbsolute("foo"), "Paths containing a single item are not absolute paths on Windows");
+ ok(!PathUtils.isAbsolute(".\\foo"), "Paths relative to the current working directory are not absolute paths on Windows");
+ ok(!PathUtils.isAbsolute("..\\foo"), "Paths relative to the parent directory are not absolute paths on Windows");
+ } else {
+ ok(PathUtils.isAbsolute("/"), "Root paths are absolute paths");
+ ok(PathUtils.isAbsolute("/home"), "Paths with a root stem are absolute paths");
+ ok(!PathUtils.isAbsolute("foo"), "Paths containing a single non-root item are not absolute paths");
+ ok(!PathUtils.isAbsolute("./foo"), "Paths relative to the current working directory are not absolute paths");
+ ok(!PathUtils.isAbsolute("../foo"), "Paths relative to the parent directory are not absolute paths");
+ }
+ });
+
+ add_task(async function test_getDirectories() {
+ // See: nsAppDirectoryServiceDefs.h
+ const tests = [
+ ["profileDir", "ProfD"],
+ ["localProfileDir", "ProfLD"],
+ ["tempDir", "TmpD"],
+ ];
+
+ for (const [attrName, dirConstant] of tests) {
+ const expected = Services.dirsvc.get(dirConstant, Ci.nsIFile).path;
+
+ const attrValue = PathUtils[attrName];
+ is(attrValue, expected, `PathUtils.${attrName} == Services.dirsvc.get("${dirConstant}", Ci.nsIFile).path`);
+ }
+ });
+</script>
+
+<body>
+</body>
+
+</html>
diff --git a/dom/system/tests/test_pathutils_worker.xhtml b/dom/system/tests/test_pathutils_worker.xhtml
new file mode 100644
index 0000000000..dc181ce07d
--- /dev/null
+++ b/dom/system/tests/test_pathutils_worker.xhtml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<!-- Any copyright is dedicated to the Public Domain.
+- http://creativecommons.org/publicdomain/zero/1.0/ -->
+<window title="Testing PathUtils 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>
+ <script src="chrome://mochikit/content/tests/SimpleTest/WorkerHandler.js"></script>
+
+ <script type="application/javascript">
+ <![CDATA[
+ function test() {
+ SimpleTest.waitForExplicitFinish();
+
+ info("test_pathtuils_worker.xhtml: Starting test");
+
+ const worker = new ChromeWorker("pathutils_worker.js");
+ info("test_pathtuils_worker.xhtml: ChromeWorker created");
+
+ listenForTests(worker, { verbose: false });
+ worker.postMessage({
+ profileDir: PathUtils.profileDir,
+ localProfileDir: PathUtils.localProfileDir,
+ tempDir: PathUtils.tempDir,
+ });
+
+ info("test_pathtuils_worker.xhtml: Test running...");
+ }
+ ]]>
+ </script>
+ <body xmlns="https://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/windows/PWindowsUtils.ipdl b/dom/system/windows/PWindowsUtils.ipdl
new file mode 100644
index 0000000000..fa5b800f3b
--- /dev/null
+++ b/dom/system/windows/PWindowsUtils.ipdl
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* 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 protocol PWindowsLocation;
+
+namespace mozilla {
+namespace dom {
+
+// Manager of utility actors that run in the windows utility process.
+[ChildProc=Utility]
+protocol PWindowsUtils {
+ manages PWindowsLocation;
+
+child:
+ // Proxies the ILocation COM API for geolocation
+ async PWindowsLocation();
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/system/windows/WindowsUtilsChild.h b/dom/system/windows/WindowsUtilsChild.h
new file mode 100644
index 0000000000..eb6c65251b
--- /dev/null
+++ b/dom/system/windows/WindowsUtilsChild.h
@@ -0,0 +1,30 @@
+/* -*- 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_WindowsUtilsChild_h__
+#define mozilla_dom_WindowsUtilsChild_h__
+
+#include "mozilla/dom/PWindowsUtilsChild.h"
+#include "mozilla/dom/WindowsLocationChild.h"
+
+namespace mozilla::dom {
+
+// Manager for utilities in the WindowsUtils utility process.
+class WindowsUtilsChild final : public PWindowsUtilsChild {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WindowsUtilsChild, override);
+
+ public:
+ already_AddRefed<PWindowsLocationChild> AllocPWindowsLocationChild() {
+ return MakeAndAddRef<WindowsLocationChild>();
+ }
+
+ protected:
+ ~WindowsUtilsChild() = default;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_WindowsUtilsChild_h__
diff --git a/dom/system/windows/WindowsUtilsParent.h b/dom/system/windows/WindowsUtilsParent.h
new file mode 100644
index 0000000000..bc447a1167
--- /dev/null
+++ b/dom/system/windows/WindowsUtilsParent.h
@@ -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/. */
+
+#ifndef mozilla_dom_WindowsUtilsParent_h__
+#define mozilla_dom_WindowsUtilsParent_h__
+
+#include "mozilla/dom/PWindowsUtilsParent.h"
+#include "mozilla/ProcInfo.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "mozilla/ipc/UtilityProcessParent.h"
+
+namespace mozilla::dom {
+
+// Main-process manager for utilities in the WindowsUtils utility process.
+class WindowsUtilsParent final : public PWindowsUtilsParent {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WindowsUtilsParent, override);
+
+ nsresult BindToUtilityProcess(
+ RefPtr<mozilla::ipc::UtilityProcessParent> aUtilityParent) {
+ Endpoint<PWindowsUtilsParent> parentEnd;
+ Endpoint<PWindowsUtilsChild> childEnd;
+ nsresult rv = PWindowsUtils::CreateEndpoints(base::GetCurrentProcId(),
+ aUtilityParent->OtherPid(),
+ &parentEnd, &childEnd);
+
+ if (NS_FAILED(rv)) {
+ MOZ_ASSERT(false, "Protocol endpoints failure");
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!aUtilityParent->SendStartWindowsUtilsService(std::move(childEnd))) {
+ MOZ_ASSERT(false, "SendStartWindowsUtilsService failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ DebugOnly<bool> ok = parentEnd.Bind(this);
+ MOZ_ASSERT(ok);
+ return NS_OK;
+ }
+
+ UtilityActorName GetActorName() { return UtilityActorName::WindowsUtils; }
+
+ protected:
+ ~WindowsUtilsParent() = default;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_WindowsUtilsParent_h__
diff --git a/dom/system/windows/location/PWindowsLocation.ipdl b/dom/system/windows/location/PWindowsLocation.ipdl
new file mode 100644
index 0000000000..db53b5c805
--- /dev/null
+++ b/dom/system/windows/location/PWindowsLocation.ipdl
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* 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 protocol PWindowsUtils;
+
+[RefCounted] using class nsIDOMGeoPosition from "nsGeoPositionIPCSerialiser.h";
+
+namespace mozilla {
+namespace dom {
+
+// Proxies geolocation functions to a utility process so that we
+// can safely handle crashes in the ILocation API. Messages to the child
+// are proxies for the ILocation COM object. Messages to the parent
+// are proxied nsIGeolocationUpdate callbacks.
+protocol PWindowsLocation {
+ manager PWindowsUtils;
+
+child:
+ async Startup();
+ async RegisterForReport();
+ async UnregisterForReport();
+ async SetHighAccuracy(bool aEnable);
+
+ async __delete__();
+
+parent:
+ // Update geolocation with new position information.
+ async Update(nullable nsIDOMGeoPosition aPosition);
+
+ // The geolocation API has reported an error.
+ async Failed(uint16_t aError);
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/system/windows/location/WindowsLocationChild.cpp b/dom/system/windows/location/WindowsLocationChild.cpp
new file mode 100644
index 0000000000..514ae4610f
--- /dev/null
+++ b/dom/system/windows/location/WindowsLocationChild.cpp
@@ -0,0 +1,257 @@
+/* -*- 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 "WindowsLocationChild.h"
+#include "nsCOMPtr.h"
+#include "WindowsLocationProvider.h"
+#include "mozilla/dom/GeolocationPosition.h"
+#include "mozilla/dom/GeolocationPositionErrorBinding.h"
+#include "mozilla/Telemetry.h"
+#include "nsIGeolocationProvider.h"
+
+#include <locationapi.h>
+
+namespace mozilla::dom {
+
+extern LazyLogModule gWindowsLocationProviderLog;
+#define LOG(...) \
+ MOZ_LOG(gWindowsLocationProviderLog, LogLevel::Debug, (__VA_ARGS__))
+
+class LocationEvent final : public ILocationEvents {
+ public:
+ explicit LocationEvent(WindowsLocationChild* aActor)
+ : mActor(aActor), mRefCnt(0) {}
+
+ // IUnknown interface
+ STDMETHODIMP_(ULONG) AddRef() override;
+ STDMETHODIMP_(ULONG) Release() override;
+ STDMETHODIMP QueryInterface(REFIID iid, void** ppv) override;
+
+ // ILocationEvents interface
+ STDMETHODIMP OnStatusChanged(REFIID aReportType,
+ LOCATION_REPORT_STATUS aStatus) override;
+ STDMETHODIMP OnLocationChanged(REFIID aReportType,
+ ILocationReport* aReport) override;
+
+ private:
+ // Making this a WeakPtr breaks the following cycle of strong references:
+ // WindowsLocationChild -> ILocation -> ILocationEvents (this)
+ // -> WindowsLocationChild.
+ WeakPtr<WindowsLocationChild> mActor;
+
+ ULONG mRefCnt;
+};
+
+STDMETHODIMP_(ULONG)
+LocationEvent::AddRef() { return InterlockedIncrement(&mRefCnt); }
+
+STDMETHODIMP_(ULONG)
+LocationEvent::Release() {
+ ULONG count = InterlockedDecrement(&mRefCnt);
+ if (!count) {
+ delete this;
+ return 0;
+ }
+ return count;
+}
+
+STDMETHODIMP
+LocationEvent::QueryInterface(REFIID iid, void** ppv) {
+ if (!ppv) {
+ return E_INVALIDARG;
+ }
+
+ if (iid == IID_IUnknown) {
+ *ppv = static_cast<IUnknown*>(this);
+ } else if (iid == IID_ILocationEvents) {
+ *ppv = static_cast<ILocationEvents*>(this);
+ } else {
+ *ppv = nullptr;
+ return E_NOINTERFACE;
+ }
+
+ AddRef();
+ return S_OK;
+}
+
+STDMETHODIMP
+LocationEvent::OnStatusChanged(REFIID aReportType,
+ LOCATION_REPORT_STATUS aStatus) {
+ LOG("LocationEvent::OnStatusChanged(%p, %p, %s, %04x)", this, mActor.get(),
+ aReportType == IID_ILatLongReport ? "true" : "false",
+ static_cast<uint32_t>(aStatus));
+
+ if (!mActor || 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.
+ // We ignore those messages.
+ 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;
+ }
+
+ mActor->SendFailed(err);
+ return S_OK;
+}
+
+STDMETHODIMP
+LocationEvent::OnLocationChanged(REFIID aReportType, ILocationReport* aReport) {
+ LOG("LocationEvent::OnLocationChanged(%p, %p, %s)", this, mActor.get(),
+ aReportType == IID_ILatLongReport ? "true" : "false");
+
+ if (!mActor || 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);
+ mActor->SendUpdate(position);
+
+ return S_OK;
+}
+
+WindowsLocationChild::WindowsLocationChild() {
+ LOG("WindowsLocationChild::WindowsLocationChild(%p)", this);
+}
+
+WindowsLocationChild::~WindowsLocationChild() {
+ LOG("WindowsLocationChild::~WindowsLocationChild(%p)", this);
+}
+
+::mozilla::ipc::IPCResult WindowsLocationChild::RecvStartup() {
+ LOG("WindowsLocationChild::RecvStartup(%p, %p)", this, mLocation.get());
+ if (mLocation) {
+ return IPC_OK();
+ }
+
+ RefPtr<ILocation> location;
+ if (FAILED(::CoCreateInstance(CLSID_Location, nullptr, CLSCTX_INPROC_SERVER,
+ IID_ILocation, getter_AddRefs(location)))) {
+ LOG("WindowsLocationChild(%p) failed to create ILocation", this);
+ // We will use MLS provider
+ SendFailed(GeolocationPositionError_Binding::POSITION_UNAVAILABLE);
+ return IPC_OK();
+ }
+
+ IID reportTypes[] = {IID_ILatLongReport};
+ if (FAILED(location->RequestPermissions(nullptr, reportTypes, 1, FALSE))) {
+ LOG("WindowsLocationChild(%p) failed to set ILocation permissions", this);
+ // We will use MLS provider
+ SendFailed(GeolocationPositionError_Binding::POSITION_UNAVAILABLE);
+ return IPC_OK();
+ }
+
+ mLocation = location;
+ return IPC_OK();
+}
+
+::mozilla::ipc::IPCResult WindowsLocationChild::RecvSetHighAccuracy(
+ bool aEnable) {
+ LOG("WindowsLocationChild::RecvSetHighAccuracy(%p, %p, %s)", this,
+ mLocation.get(), aEnable ? "true" : "false");
+
+ // We sometimes call SetHighAccuracy before Startup, so we save the
+ // request and set it later, in RegisterForReport.
+ mHighAccuracy = aEnable;
+
+ return IPC_OK();
+}
+
+::mozilla::ipc::IPCResult WindowsLocationChild::RecvRegisterForReport() {
+ LOG("WindowsLocationChild::RecvRegisterForReport(%p, %p)", this,
+ mLocation.get());
+
+ if (!mLocation) {
+ SendFailed(GeolocationPositionError_Binding::POSITION_UNAVAILABLE);
+ return IPC_OK();
+ }
+
+ LOCATION_DESIRED_ACCURACY desiredAccuracy;
+ if (mHighAccuracy) {
+ desiredAccuracy = LOCATION_DESIRED_ACCURACY_HIGH;
+ } else {
+ desiredAccuracy = LOCATION_DESIRED_ACCURACY_DEFAULT;
+ }
+
+ if (NS_WARN_IF(FAILED(mLocation->SetDesiredAccuracy(IID_ILatLongReport,
+ desiredAccuracy)))) {
+ SendFailed(GeolocationPositionError_Binding::POSITION_UNAVAILABLE);
+ return IPC_OK();
+ }
+
+ auto event = MakeRefPtr<LocationEvent>(this);
+ if (NS_WARN_IF(
+ FAILED(mLocation->RegisterForReport(event, IID_ILatLongReport, 0)))) {
+ SendFailed(GeolocationPositionError_Binding::POSITION_UNAVAILABLE);
+ }
+
+ LOG("WindowsLocationChild::RecvRegisterForReport successfully registered");
+ return IPC_OK();
+}
+
+::mozilla::ipc::IPCResult WindowsLocationChild::RecvUnregisterForReport() {
+ LOG("WindowsLocationChild::RecvUnregisterForReport(%p, %p)", this,
+ mLocation.get());
+
+ if (!mLocation) {
+ return IPC_OK();
+ }
+
+ // This will free the LocationEvent we created in RecvRegisterForReport.
+ Unused << NS_WARN_IF(
+ FAILED(mLocation->UnregisterForReport(IID_ILatLongReport)));
+
+ // The ILocation object is not reusable. Unregistering, restarting and
+ // re-registering for reports does not work; the callback is never
+ // called in that case. For that reason, we re-create the ILocation
+ // object with a call to Startup after unregistering if we need it again.
+ mLocation = nullptr;
+ return IPC_OK();
+}
+
+void WindowsLocationChild::ActorDestroy(ActorDestroyReason aWhy) {
+ LOG("WindowsLocationChild::ActorDestroy(%p, %p)", this, mLocation.get());
+ mLocation = nullptr;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/system/windows/location/WindowsLocationChild.h b/dom/system/windows/location/WindowsLocationChild.h
new file mode 100644
index 0000000000..ac51cff213
--- /dev/null
+++ b/dom/system/windows/location/WindowsLocationChild.h
@@ -0,0 +1,44 @@
+/* -*- 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_WindowsLocationChild_h__
+#define mozilla_dom_WindowsLocationChild_h__
+
+#include "mozilla/dom/PWindowsLocationChild.h"
+#include "mozilla/WeakPtr.h"
+
+class ILocation;
+
+namespace mozilla::dom {
+
+// Geolocation actor in utility process.
+class WindowsLocationChild final : public PWindowsLocationChild,
+ public SupportsWeakPtr {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WindowsLocationChild, override);
+
+ public:
+ WindowsLocationChild();
+
+ using IPCResult = ::mozilla::ipc::IPCResult;
+
+ IPCResult RecvStartup();
+ IPCResult RecvRegisterForReport();
+ IPCResult RecvUnregisterForReport();
+ IPCResult RecvSetHighAccuracy(bool aEnable);
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ private:
+ ~WindowsLocationChild() override;
+
+ // The COM object the actors are proxying calls for.
+ RefPtr<ILocation> mLocation;
+
+ bool mHighAccuracy = false;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_WindowsLocationChild_h__
diff --git a/dom/system/windows/location/WindowsLocationParent.cpp b/dom/system/windows/location/WindowsLocationParent.cpp
new file mode 100644
index 0000000000..f19bcb8b0d
--- /dev/null
+++ b/dom/system/windows/location/WindowsLocationParent.cpp
@@ -0,0 +1,36 @@
+/* -*- 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 "WindowsLocationParent.h"
+#include "nsIDOMGeoPosition.h"
+#include "WindowsLocationProvider.h"
+
+namespace mozilla::dom {
+
+::mozilla::ipc::IPCResult WindowsLocationParent::RecvUpdate(
+ RefPtr<nsIDOMGeoPosition> aGeoPosition) {
+ if (mProvider) {
+ mProvider->RecvUpdate(aGeoPosition);
+ }
+ return IPC_OK();
+}
+
+// A failure occurred. This may be translated into a
+// nsIGeolocationUpdate::NotifyError or may be ignored if the MLS fallback
+// is available.
+::mozilla::ipc::IPCResult WindowsLocationParent::RecvFailed(uint16_t err) {
+ if (mProvider) {
+ mProvider->RecvFailed(err);
+ }
+ return IPC_OK();
+}
+
+void WindowsLocationParent::ActorDestroy(ActorDestroyReason aReason) {
+ if (mProvider) {
+ mProvider->ActorStopped();
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/system/windows/location/WindowsLocationParent.h b/dom/system/windows/location/WindowsLocationParent.h
new file mode 100644
index 0000000000..01538246ea
--- /dev/null
+++ b/dom/system/windows/location/WindowsLocationParent.h
@@ -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/. */
+
+#ifndef mozilla_dom_WindowsLocationParent_h__
+#define mozilla_dom_WindowsLocationParent_h__
+
+#include "nsCOMPtr.h"
+#include "mozilla/dom/PWindowsLocationParent.h"
+
+class nsGeoPosition;
+class nsIGeolocationUpdate;
+
+namespace mozilla::dom {
+
+class WindowsLocationProvider;
+
+// Geolocation actor in main process.
+// This may receive messages asynchronously, even after it sends Unregister
+// to the child.
+class WindowsLocationParent final : public PWindowsLocationParent {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WindowsLocationParent, override);
+
+ using IPCResult = ::mozilla::ipc::IPCResult;
+
+ explicit WindowsLocationParent(WindowsLocationProvider* aProvider)
+ : mProvider(aProvider) {}
+
+ // Update geolocation with new position information.
+ IPCResult RecvUpdate(RefPtr<nsIDOMGeoPosition> aGeoPosition);
+
+ // A failure occurred. This may be translated into a
+ // nsIGeolocationUpdate::NotifyError or may be ignored if the MLS fallback
+ // is available.
+ IPCResult RecvFailed(uint16_t err);
+
+ void ActorDestroy(ActorDestroyReason aReason) override;
+
+ // After this, the actor will simply ignore any incoming messages.
+ void DetachFromLocationProvider() { mProvider = nullptr; }
+
+ private:
+ ~WindowsLocationParent() override = default;
+
+ WindowsLocationProvider* mProvider;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_WindowsLocationParent_h__
diff --git a/dom/system/windows/location/WindowsLocationProvider.cpp b/dom/system/windows/location/WindowsLocationProvider.cpp
new file mode 100644
index 0000000000..92a6f2c9cc
--- /dev/null
+++ b/dom/system/windows/location/WindowsLocationProvider.cpp
@@ -0,0 +1,350 @@
+/* -*- 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 "WindowsLocationParent.h"
+#include "mozilla/dom/WindowsUtilsParent.h"
+#include "GeolocationPosition.h"
+#include "nsComponentManagerUtils.h"
+#include "mozilla/ipc/UtilityProcessManager.h"
+#include "mozilla/ipc/UtilityProcessSandboxing.h"
+#include "prtime.h"
+#include "MLSFallback.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/Logging.h"
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/dom/GeolocationPositionErrorBinding.h"
+
+namespace mozilla::dom {
+
+LazyLogModule gWindowsLocationProviderLog("WindowsLocationProvider");
+#define LOG(...) \
+ MOZ_LOG(gWindowsLocationProviderLog, LogLevel::Debug, (__VA_ARGS__))
+
+class MLSUpdate : public nsIGeolocationUpdate {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIGEOLOCATIONUPDATE
+ explicit MLSUpdate(nsIGeolocationUpdate* aCallback) : mCallback(aCallback) {}
+
+ private:
+ nsCOMPtr<nsIGeolocationUpdate> mCallback;
+ virtual ~MLSUpdate() {}
+};
+
+NS_IMPL_ISUPPORTS(MLSUpdate, nsIGeolocationUpdate);
+
+NS_IMETHODIMP
+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
+MLSUpdate::NotifyError(uint16_t aError) {
+ if (!mCallback) {
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsIGeolocationUpdate> callback(mCallback);
+ return callback->NotifyError(aError);
+}
+
+NS_IMPL_ISUPPORTS(WindowsLocationProvider, nsIGeolocationProvider)
+
+WindowsLocationProvider::WindowsLocationProvider() {
+ LOG("WindowsLocationProvider::WindowsLocationProvider(%p)", this);
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MaybeCreateLocationActor();
+}
+
+WindowsLocationProvider::~WindowsLocationProvider() {
+ LOG("WindowsLocationProvider::~WindowsLocationProvider(%p,%p,%p)", this,
+ mActor.get(), mActorPromise.get());
+ Send__delete__();
+ ReleaseUtilityProcess();
+ CancelMLSProvider();
+}
+
+void WindowsLocationProvider::MaybeCreateLocationActor() {
+ LOG("WindowsLocationProvider::MaybeCreateLocationActor(%p)", this);
+ if (mActor || mActorPromise) {
+ return;
+ }
+
+ auto utilityProc = mozilla::ipc::UtilityProcessManager::GetSingleton();
+ MOZ_ASSERT(utilityProc);
+
+ // Create a PWindowsLocation actor in the Windows utility process.
+ // This will attempt to launch the process if it doesn't already exist.
+ RefPtr<WindowsLocationProvider> self = this;
+ auto wuPromise = utilityProc->GetWindowsUtilsPromise();
+ mActorPromise = wuPromise->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self](RefPtr<WindowsUtilsParent> wup) {
+ self->mActorPromise = nullptr;
+ auto actor = MakeRefPtr<WindowsLocationParent>(self);
+ if (!wup->SendPWindowsLocationConstructor(actor)) {
+ LOG("WindowsLocationProvider(%p) SendPWindowsLocationConstructor "
+ "failed",
+ self.get());
+ actor->DetachFromLocationProvider();
+ self->mActor = nullptr;
+ return WindowsLocationPromise::CreateAndReject(false, __func__);
+ }
+ LOG("WindowsLocationProvider connected to actor (%p,%p,%p)", self.get(),
+ self->mActor.get(), self->mActorPromise.get());
+ self->mActor = actor;
+ return WindowsLocationPromise::CreateAndResolve(self->mActor, __func__);
+ },
+
+ [self](nsresult aError) {
+ LOG("WindowsLocationProvider failed to connect to actor (%p,%p,%p)",
+ self.get(), self->mActor.get(), self->mActorPromise.get());
+ self->mActorPromise = nullptr;
+ return WindowsLocationPromise::CreateAndReject(false, __func__);
+ });
+
+ if (mActor) {
+ // Utility process already existed and mActorPromise was resolved
+ // immediately.
+ mActorPromise = nullptr;
+ }
+}
+
+void WindowsLocationProvider::ReleaseUtilityProcess() {
+ LOG("WindowsLocationProvider::ReleaseUtilityProcess(%p)", this);
+ auto utilityProc = mozilla::ipc::UtilityProcessManager::GetIfExists();
+ if (utilityProc) {
+ utilityProc->ReleaseWindowsUtils();
+ }
+}
+
+template <typename Fn>
+bool WindowsLocationProvider::WhenActorIsReady(Fn&& fn) {
+ if (mActor) {
+ return fn(mActor);
+ }
+
+ if (mActorPromise) {
+ mActorPromise->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [fn](const RefPtr<WindowsLocationParent>& actor) {
+ Unused << fn(actor.get());
+ return actor;
+ },
+ [](bool) { return false; });
+ return true;
+ }
+
+ // The remote process failed to start.
+ return false;
+}
+
+bool WindowsLocationProvider::SendStartup() {
+ LOG("WindowsLocationProvider::SendStartup(%p)", this);
+ MaybeCreateLocationActor();
+ return WhenActorIsReady(
+ [](WindowsLocationParent* actor) { return actor->SendStartup(); });
+}
+
+bool WindowsLocationProvider::SendRegisterForReport(
+ nsIGeolocationUpdate* aCallback) {
+ LOG("WindowsLocationProvider::SendRegisterForReport(%p)", this);
+ RefPtr<WindowsLocationProvider> self = this;
+ RefPtr<nsIGeolocationUpdate> cb = aCallback;
+ return WhenActorIsReady([self, cb](WindowsLocationParent* actor) {
+ MOZ_ASSERT(!self->mCallback);
+ if (actor->SendRegisterForReport()) {
+ self->mCallback = cb;
+ return true;
+ }
+ return false;
+ });
+}
+
+bool WindowsLocationProvider::SendUnregisterForReport() {
+ LOG("WindowsLocationProvider::SendUnregisterForReport(%p)", this);
+ RefPtr<WindowsLocationProvider> self = this;
+ return WhenActorIsReady([self](WindowsLocationParent* actor) {
+ self->mCallback = nullptr;
+ if (actor->SendUnregisterForReport()) {
+ return true;
+ }
+ return false;
+ });
+}
+
+bool WindowsLocationProvider::SendSetHighAccuracy(bool aEnable) {
+ LOG("WindowsLocationProvider::SendSetHighAccuracy(%p)", this);
+ return WhenActorIsReady([aEnable](WindowsLocationParent* actor) {
+ return actor->SendSetHighAccuracy(aEnable);
+ });
+}
+
+bool WindowsLocationProvider::Send__delete__() {
+ LOG("WindowsLocationProvider::Send__delete__(%p)", this);
+ return WhenActorIsReady([self = RefPtr{this}](WindowsLocationParent*) {
+ if (WindowsLocationParent::Send__delete__(self->mActor)) {
+ if (self->mActor) {
+ self->mActor->DetachFromLocationProvider();
+ self->mActor = nullptr;
+ }
+ return true;
+ }
+ return false;
+ });
+}
+
+void WindowsLocationProvider::RecvUpdate(
+ RefPtr<nsIDOMGeoPosition> aGeoPosition) {
+ LOG("WindowsLocationProvider::RecvUpdate(%p)", this);
+ if (!mCallback) {
+ return;
+ }
+
+ mCallback->Update(aGeoPosition.get());
+
+ Telemetry::Accumulate(Telemetry::GEOLOCATION_WIN8_SOURCE_IS_MLS, false);
+}
+
+void WindowsLocationProvider::RecvFailed(uint16_t err) {
+ LOG("WindowsLocationProvider::RecvFailed(%p)", this);
+ // Cannot get current location at this time. We use MLS instead.
+ if (mMLSProvider || !mCallback) {
+ return;
+ }
+
+ if (NS_SUCCEEDED(CreateAndWatchMLSProvider(mCallback))) {
+ return;
+ }
+
+ // No ILocation and no MLS, so we have failed completely.
+ // We keep strong references to objects that we need to guarantee
+ // will live past the NotifyError callback.
+ RefPtr<WindowsLocationProvider> self = this;
+ nsCOMPtr<nsIGeolocationUpdate> callback = mCallback;
+ callback->NotifyError(err);
+}
+
+void WindowsLocationProvider::ActorStopped() {
+ // ActorDestroy has run. Make sure UtilityProcessHost no longer tries to use
+ // it.
+ ReleaseUtilityProcess();
+
+ if (mWatching) {
+ // Treat as remote geolocation error, which will cause it to fallback
+ // to MLS if it hasn't already.
+ mWatching = false;
+ RecvFailed(GeolocationPositionError_Binding::POSITION_UNAVAILABLE);
+ return;
+ }
+
+ MOZ_ASSERT(!mActorPromise);
+ if (mActor) {
+ mActor->DetachFromLocationProvider();
+ mActor = nullptr;
+ }
+}
+
+NS_IMETHODIMP
+WindowsLocationProvider::Startup() {
+ LOG("WindowsLocationProvider::Startup(%p, %p, %p)", this, mActor.get(),
+ mActorPromise.get());
+ // If this fails, we will use the MLS fallback.
+ SendStartup();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WindowsLocationProvider::Watch(nsIGeolocationUpdate* aCallback) {
+ LOG("WindowsLocationProvider::Watch(%p, %p, %p, %p, %d)", this, mActor.get(),
+ mActorPromise.get(), aCallback, mWatching);
+ if (mWatching) {
+ return NS_OK;
+ }
+
+ if (SendRegisterForReport(aCallback)) {
+ mWatching = true;
+ return NS_OK;
+ }
+
+ // Couldn't send request. We will use MLS instead.
+ return CreateAndWatchMLSProvider(aCallback);
+}
+
+NS_IMETHODIMP
+WindowsLocationProvider::Shutdown() {
+ LOG("WindowsLocationProvider::Shutdown(%p, %p, %p)", this, mActor.get(),
+ mActorPromise.get());
+
+ if (mWatching) {
+ SendUnregisterForReport();
+ mWatching = false;
+ }
+
+ CancelMLSProvider();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WindowsLocationProvider::SetHighAccuracy(bool enable) {
+ LOG("WindowsLocationProvider::SetHighAccuracy(%p, %p, %p, %s)", this,
+ mActor.get(), mActorPromise.get(), enable ? "true" : "false");
+ if (mMLSProvider) {
+ // Ignored when running MLS fallback.
+ return NS_OK;
+ }
+
+ if (!SendSetHighAccuracy(enable)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Since we SendSetHighAccuracy asynchronously, we cannot say for sure
+ // that it will succeed. If it does fail then we will get a
+ // RecvFailed IPC message, which will cause a fallback to MLS.
+ return NS_OK;
+}
+
+nsresult WindowsLocationProvider::CreateAndWatchMLSProvider(
+ nsIGeolocationUpdate* aCallback) {
+ LOG("WindowsLocationProvider::CreateAndWatchMLSProvider"
+ "(%p, %p, %p, %p, %p)",
+ this, mMLSProvider.get(), mActor.get(), mActorPromise.get(), aCallback);
+
+ if (mMLSProvider) {
+ return NS_OK;
+ }
+
+ mMLSProvider = new MLSFallback(0);
+ return mMLSProvider->Startup(new MLSUpdate(aCallback));
+}
+
+void WindowsLocationProvider::CancelMLSProvider() {
+ LOG("WindowsLocationProvider::CancelMLSProvider"
+ "(%p, %p, %p, %p, %p)",
+ this, mMLSProvider.get(), mActor.get(), mActorPromise.get(),
+ mCallback.get());
+
+ if (!mMLSProvider) {
+ return;
+ }
+
+ mMLSProvider->Shutdown();
+ mMLSProvider = nullptr;
+}
+
+#undef LOG
+
+} // namespace mozilla::dom
diff --git a/dom/system/windows/location/WindowsLocationProvider.h b/dom/system/windows/location/WindowsLocationProvider.h
new file mode 100644
index 0000000000..d1e4dfa936
--- /dev/null
+++ b/dom/system/windows/location/WindowsLocationProvider.h
@@ -0,0 +1,78 @@
+/* -*- 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 "mozilla/MozPromise.h"
+
+class MLSFallback;
+
+namespace mozilla::dom {
+
+class WindowsLocationParent;
+
+// Uses a PWindowsLocation actor to subscribe to geolocation updates from the
+// Windows utility process and falls back to MLS when it is not available or
+// fails.
+class WindowsLocationProvider final : public nsIGeolocationProvider {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIGEOLOCATIONPROVIDER
+
+ WindowsLocationProvider();
+
+ private:
+ friend WindowsLocationParent;
+
+ ~WindowsLocationProvider();
+
+ nsresult CreateAndWatchMLSProvider(nsIGeolocationUpdate* aCallback);
+ void CancelMLSProvider();
+
+ void MaybeCreateLocationActor();
+ void ReleaseUtilityProcess();
+
+ // These methods either send the message on the existing actor or queue
+ // the messages to be sent (in order) once the actor exists.
+ bool SendStartup();
+ bool SendRegisterForReport(nsIGeolocationUpdate* aCallback);
+ bool SendUnregisterForReport();
+ bool SendSetHighAccuracy(bool aEnable);
+ bool Send__delete__();
+
+ void RecvUpdate(RefPtr<nsIDOMGeoPosition> aGeoPosition);
+ // See bug 1539864 for MOZ_CAN_RUN_SCRIPT_BOUNDARY justification.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void RecvFailed(uint16_t err);
+
+ // The utility process actor has ended its connection, either successfully
+ // or with an error.
+ void ActorStopped();
+
+ // Run fn once actor is ready to send messages, which may be immediately.
+ template <typename Fn>
+ bool WhenActorIsReady(Fn&& fn);
+
+ RefPtr<MLSFallback> mMLSProvider;
+
+ nsCOMPtr<nsIGeolocationUpdate> mCallback;
+
+ using WindowsLocationPromise =
+ MozPromise<RefPtr<WindowsLocationParent>, bool, false>;
+
+ // Before the utility process exists, we have a promise that we will get our
+ // location actor. mActor and mActorPromise are never both set.
+ RefPtr<WindowsLocationPromise> mActorPromise;
+ RefPtr<WindowsLocationParent> mActor;
+
+ bool mWatching = false;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_WindowsLocationProvider_h__
diff --git a/dom/system/windows/location/moz.build b/dom/system/windows/location/moz.build
new file mode 100644
index 0000000000..8fafb99fb1
--- /dev/null
+++ b/dom/system/windows/location/moz.build
@@ -0,0 +1,29 @@
+# -*- 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/.
+
+EXPORTS.mozilla.dom += [
+ "WindowsLocationChild.h",
+ "WindowsLocationParent.h",
+]
+
+UNIFIED_SOURCES += [
+ "WindowsLocationParent.cpp",
+ "WindowsLocationProvider.cpp",
+]
+
+SOURCES += [
+ "WindowsLocationChild.cpp", # includes locationapi.h
+]
+
+IPDL_SOURCES += [
+ "PWindowsLocation.ipdl",
+]
+
+LOCAL_INCLUDES += ["/dom/geolocation"]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/system/windows/moz.build b/dom/system/windows/moz.build
new file mode 100644
index 0000000000..0ee12d4cce
--- /dev/null
+++ b/dom/system/windows/moz.build
@@ -0,0 +1,24 @@
+# -*- 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/.
+
+EXPORTS.mozilla.dom += [
+ "WindowsUtilsChild.h",
+ "WindowsUtilsParent.h",
+]
+
+UNIFIED_SOURCES += [
+ "nsHapticFeedback.cpp",
+]
+
+IPDL_SOURCES += [
+ "PWindowsUtils.ipdl",
+]
+
+DIRS += ["location"]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+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
+};