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