From 43a97878ce14b72f0981164f87f2e35e14151312 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 11:22:09 +0200 Subject: Adding upstream version 110.0.1. Signed-off-by: Daniel Baumann --- dom/system/IOUtils.cpp | 2884 ++++++++++++++++++++ dom/system/IOUtils.h | 921 +++++++ dom/system/NetworkGeolocationProvider.jsm | 511 ++++ dom/system/OSFileConstants.cpp | 983 +++++++ dom/system/OSFileConstants.h | 53 + dom/system/PathUtils.cpp | 657 +++++ dom/system/PathUtils.h | 267 ++ dom/system/android/AndroidLocationProvider.cpp | 52 + dom/system/android/AndroidLocationProvider.h | 23 + dom/system/android/moz.build | 17 + dom/system/android/nsHapticFeedback.cpp | 18 + dom/system/android/nsHapticFeedback.h | 16 + dom/system/components.conf | 17 + dom/system/linux/GeoclueLocationProvider.cpp | 1060 +++++++ dom/system/linux/GeoclueLocationProvider.h | 32 + dom/system/linux/GpsdLocationProvider.cpp | 446 +++ dom/system/linux/GpsdLocationProvider.h | 51 + dom/system/linux/PortalLocationProvider.cpp | 351 +++ dom/system/linux/PortalLocationProvider.h | 54 + dom/system/linux/moz.build | 24 + dom/system/mac/CoreLocationLocationProvider.h | 61 + dom/system/mac/CoreLocationLocationProvider.mm | 246 ++ dom/system/mac/moz.build | 21 + dom/system/mac/nsOSPermissionRequest.h | 31 + dom/system/mac/nsOSPermissionRequest.mm | 91 + dom/system/moz.build | 122 + dom/system/nsDeviceSensors.cpp | 556 ++++ dom/system/nsDeviceSensors.h | 72 + dom/system/nsIOSFileConstantsService.idl | 28 + dom/system/nsIOSPermissionRequest.idl | 69 + dom/system/nsOSPermissionRequest.h | 18 + dom/system/nsOSPermissionRequestBase.cpp | 96 + dom/system/nsOSPermissionRequestBase.h | 38 + dom/system/tests/chrome.ini | 9 + dom/system/tests/file_bug1197901.html | 16 + dom/system/tests/ioutils/chrome.ini | 23 + .../tests/ioutils/file_ioutils_test_fixtures.js | 78 + dom/system/tests/ioutils/file_ioutils_worker.js | 102 + dom/system/tests/ioutils/test_ioutils.html | 26 + .../ioutils/test_ioutils_compute_hex_digest.html | 54 + .../tests/ioutils/test_ioutils_copy_move.html | 359 +++ .../tests/ioutils/test_ioutils_create_unique.html | 88 + .../tests/ioutils/test_ioutils_dir_iteration.html | 94 + dom/system/tests/ioutils/test_ioutils_getfile.html | 86 + .../tests/ioutils/test_ioutils_mac_xattr.html | 90 + dom/system/tests/ioutils/test_ioutils_mkdir.html | 133 + .../tests/ioutils/test_ioutils_read_write.html | 550 ++++ .../ioutils/test_ioutils_read_write_json.html | 165 ++ .../ioutils/test_ioutils_read_write_utf8.html | 387 +++ dom/system/tests/ioutils/test_ioutils_remove.html | 90 + .../ioutils/test_ioutils_set_permissions.html | 84 + .../test_ioutils_stat_set_modification_time.html | 241 ++ .../test_ioutils_windows_file_attributes.html | 135 + dom/system/tests/ioutils/test_ioutils_worker.xhtml | 40 + dom/system/tests/mochitest.ini | 10 + dom/system/tests/pathutils_worker.js | 44 + dom/system/tests/test_bug1197901.html | 96 + dom/system/tests/test_constants.xhtml | 140 + dom/system/tests/test_pathutils.html | 604 ++++ dom/system/tests/test_pathutils_worker.xhtml | 39 + dom/system/tests/worker_constants.js | 95 + dom/system/windows/WindowsLocationProvider.cpp | 297 ++ dom/system/windows/WindowsLocationProvider.h | 50 + dom/system/windows/moz.build | 11 + dom/system/windows/nsHapticFeedback.cpp | 15 + dom/system/windows/nsHapticFeedback.h | 15 + 66 files changed, 14152 insertions(+) create mode 100644 dom/system/IOUtils.cpp create mode 100644 dom/system/IOUtils.h create mode 100644 dom/system/NetworkGeolocationProvider.jsm create mode 100644 dom/system/OSFileConstants.cpp create mode 100644 dom/system/OSFileConstants.h create mode 100644 dom/system/PathUtils.cpp create mode 100644 dom/system/PathUtils.h create mode 100644 dom/system/android/AndroidLocationProvider.cpp create mode 100644 dom/system/android/AndroidLocationProvider.h create mode 100644 dom/system/android/moz.build create mode 100644 dom/system/android/nsHapticFeedback.cpp create mode 100644 dom/system/android/nsHapticFeedback.h create mode 100644 dom/system/components.conf create mode 100644 dom/system/linux/GeoclueLocationProvider.cpp create mode 100644 dom/system/linux/GeoclueLocationProvider.h create mode 100644 dom/system/linux/GpsdLocationProvider.cpp create mode 100644 dom/system/linux/GpsdLocationProvider.h create mode 100644 dom/system/linux/PortalLocationProvider.cpp create mode 100644 dom/system/linux/PortalLocationProvider.h create mode 100644 dom/system/linux/moz.build create mode 100644 dom/system/mac/CoreLocationLocationProvider.h create mode 100644 dom/system/mac/CoreLocationLocationProvider.mm create mode 100644 dom/system/mac/moz.build create mode 100644 dom/system/mac/nsOSPermissionRequest.h create mode 100644 dom/system/mac/nsOSPermissionRequest.mm create mode 100644 dom/system/moz.build create mode 100644 dom/system/nsDeviceSensors.cpp create mode 100644 dom/system/nsDeviceSensors.h create mode 100644 dom/system/nsIOSFileConstantsService.idl create mode 100644 dom/system/nsIOSPermissionRequest.idl create mode 100644 dom/system/nsOSPermissionRequest.h create mode 100644 dom/system/nsOSPermissionRequestBase.cpp create mode 100644 dom/system/nsOSPermissionRequestBase.h create mode 100644 dom/system/tests/chrome.ini create mode 100644 dom/system/tests/file_bug1197901.html create mode 100644 dom/system/tests/ioutils/chrome.ini create mode 100644 dom/system/tests/ioutils/file_ioutils_test_fixtures.js create mode 100644 dom/system/tests/ioutils/file_ioutils_worker.js create mode 100644 dom/system/tests/ioutils/test_ioutils.html create mode 100644 dom/system/tests/ioutils/test_ioutils_compute_hex_digest.html create mode 100644 dom/system/tests/ioutils/test_ioutils_copy_move.html create mode 100644 dom/system/tests/ioutils/test_ioutils_create_unique.html create mode 100644 dom/system/tests/ioutils/test_ioutils_dir_iteration.html create mode 100644 dom/system/tests/ioutils/test_ioutils_getfile.html create mode 100644 dom/system/tests/ioutils/test_ioutils_mac_xattr.html create mode 100644 dom/system/tests/ioutils/test_ioutils_mkdir.html create mode 100644 dom/system/tests/ioutils/test_ioutils_read_write.html create mode 100644 dom/system/tests/ioutils/test_ioutils_read_write_json.html create mode 100644 dom/system/tests/ioutils/test_ioutils_read_write_utf8.html create mode 100644 dom/system/tests/ioutils/test_ioutils_remove.html create mode 100644 dom/system/tests/ioutils/test_ioutils_set_permissions.html create mode 100644 dom/system/tests/ioutils/test_ioutils_stat_set_modification_time.html create mode 100644 dom/system/tests/ioutils/test_ioutils_windows_file_attributes.html create mode 100644 dom/system/tests/ioutils/test_ioutils_worker.xhtml create mode 100644 dom/system/tests/mochitest.ini create mode 100644 dom/system/tests/pathutils_worker.js create mode 100644 dom/system/tests/test_bug1197901.html create mode 100644 dom/system/tests/test_constants.xhtml create mode 100644 dom/system/tests/test_pathutils.html create mode 100644 dom/system/tests/test_pathutils_worker.xhtml create mode 100644 dom/system/tests/worker_constants.js create mode 100644 dom/system/windows/WindowsLocationProvider.cpp create mode 100644 dom/system/windows/WindowsLocationProvider.h create mode 100644 dom/system/windows/moz.build create mode 100644 dom/system/windows/nsHapticFeedback.cpp create mode 100644 dom/system/windows/nsHapticFeedback.h (limited to 'dom/system') diff --git a/dom/system/IOUtils.cpp b/dom/system/IOUtils.cpp new file mode 100644 index 0000000000..3c12b4016f --- /dev/null +++ b/dom/system/IOUtils.cpp @@ -0,0 +1,2884 @@ +/* -*- 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 + +#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/Assertions.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/Compression.h" +#include "mozilla/Encoding.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/FileUtils.h" +#include "mozilla/Maybe.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/Services.h" +#include "mozilla/Span.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/TextUtils.h" +#include "mozilla/Unused.h" +#include "mozilla/Utf8.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/IOUtilsBinding.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/WorkerRef.h" +#include "PathUtils.h" +#include "nsCOMPtr.h" +#include "nsError.h" +#include "nsFileStreams.h" +#include "nsIDirectoryEnumerator.h" +#include "nsIFile.h" +#include "nsIGlobalObject.h" +#include "nsIInputStream.h" +#include "nsISupports.h" +#include "nsLocalFile.h" +#include "nsNetUtil.h" +#include "nsNSSComponent.h" +#include "nsPrintfCString.h" +#include "nsReadableUtils.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsTArray.h" +#include "nsThreadManager.h" +#include "nsXULAppAPI.h" +#include "prerror.h" +#include "prio.h" +#include "prtime.h" +#include "prtypes.h" +#include "ScopedNSSTypes.h" +#include "secoidt.h" + +#if defined(XP_UNIX) && !defined(ANDROID) +# include "nsSystemInfo.h" +#endif + +#if defined(XP_WIN) +# include "nsILocalFileWin.h" +#elif defined(XP_MACOSX) +# include "nsILocalFileMac.h" +#endif + +#ifdef XP_UNIX +# include "base/process_util.h" +#endif + +#define REJECT_IF_INIT_PATH_FAILED(_file, _path, _promise) \ + do { \ + if (nsresult _rv = PathUtils::InitFileWithPath((_file), (_path)); \ + NS_FAILED(_rv)) { \ + (_promise)->MaybeRejectWithOperationError( \ + FormatErrorMessage(_rv, "Could not parse path (%s)", \ + NS_ConvertUTF16toUTF8(_path).get())); \ + return; \ + } \ + } while (0) + +static constexpr auto SHUTDOWN_ERROR = + "IOUtils: Shutting down and refusing additional I/O tasks"_ns; + +namespace mozilla::dom { + +// static helper functions + +/** + * Platform-specific (e.g. Windows, Unix) implementations of XPCOM APIs may + * report I/O errors inconsistently. For convenience, this function will attempt + * to match a |nsresult| against known results which imply a file cannot be + * found. + * + * @see nsLocalFileWin.cpp + * @see nsLocalFileUnix.cpp + */ +static bool IsFileNotFound(nsresult aResult) { + return aResult == NS_ERROR_FILE_NOT_FOUND; +} +/** + * Like |IsFileNotFound|, but checks for known results that suggest a file + * is not a directory. + */ +static bool IsNotDirectory(nsresult aResult) { + return aResult == NS_ERROR_FILE_DESTINATION_NOT_DIR || + aResult == NS_ERROR_FILE_NOT_DIRECTORY; +} + +/** + * Formats an error message and appends the error name to the end. + */ +template +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(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(aError)); +} + +[[nodiscard]] inline bool ToJSValue( + JSContext* aCx, const IOUtils::InternalFileInfo& aInternalFileInfo, + JS::MutableHandle aValue) { + FileInfo info; + info.mPath.Construct(aInternalFileInfo.mPath); + info.mType.Construct(aInternalFileInfo.mType); + info.mSize.Construct(aInternalFileInfo.mSize); + + if (aInternalFileInfo.mCreationTime.isSome()) { + info.mCreationTime.Construct(aInternalFileInfo.mCreationTime.ref()); + } + info.mLastAccessed.Construct(aInternalFileInfo.mLastAccessed); + info.mLastModified.Construct(aInternalFileInfo.mLastModified); + + info.mPermissions.Construct(aInternalFileInfo.mPermissions); + + return ToJSValue(aCx, info, aValue); +} + +template +static void ResolveJSPromise(Promise* aPromise, T&& aValue) { + if constexpr (std::is_same_v) { + aPromise->MaybeResolveWithUndefined(); + } else if constexpr (std::is_same_v>) { + TypedArrayCreator array(aValue); + aPromise->MaybeResolve(array); + } else { + aPromise->MaybeResolve(std::forward(aValue)); + } +} + +static void RejectJSPromise(Promise* aPromise, const IOUtils::IOError& aError) { + const auto& errMsg = aError.Message(); + + switch (aError.Code()) { + case NS_ERROR_FILE_UNRESOLVABLE_SYMLINK: + [[fallthrough]]; // to NS_ERROR_FILE_INVALID_PATH + case NS_ERROR_FILE_NOT_FOUND: + [[fallthrough]]; // to NS_ERROR_FILE_INVALID_PATH + case NS_ERROR_FILE_INVALID_PATH: + aPromise->MaybeRejectWithNotFoundError(errMsg.refOr("File not found"_ns)); + break; + case NS_ERROR_FILE_IS_LOCKED: + [[fallthrough]]; // to NS_ERROR_FILE_ACCESS_DENIED + case NS_ERROR_FILE_ACCESS_DENIED: + aPromise->MaybeRejectWithNotAllowedError( + errMsg.refOr("Access was denied to the target file"_ns)); + break; + case NS_ERROR_FILE_TOO_BIG: + aPromise->MaybeRejectWithNotReadableError( + errMsg.refOr("Target file is too big"_ns)); + break; + case NS_ERROR_FILE_NO_DEVICE_SPACE: + aPromise->MaybeRejectWithNotReadableError( + errMsg.refOr("Target device is full"_ns)); + break; + case NS_ERROR_FILE_ALREADY_EXISTS: + aPromise->MaybeRejectWithNoModificationAllowedError( + errMsg.refOr("Target file already exists"_ns)); + break; + case NS_ERROR_FILE_COPY_OR_MOVE_FAILED: + aPromise->MaybeRejectWithOperationError( + errMsg.refOr("Failed to copy or move the target file"_ns)); + break; + case NS_ERROR_FILE_READ_ONLY: + aPromise->MaybeRejectWithReadOnlyError( + errMsg.refOr("Target file is read only"_ns)); + break; + case NS_ERROR_FILE_NOT_DIRECTORY: + [[fallthrough]]; // to NS_ERROR_FILE_DESTINATION_NOT_DIR + case NS_ERROR_FILE_DESTINATION_NOT_DIR: + aPromise->MaybeRejectWithInvalidAccessError( + errMsg.refOr("Target file is not a directory"_ns)); + break; + case NS_ERROR_FILE_IS_DIRECTORY: + aPromise->MaybeRejectWithInvalidAccessError( + errMsg.refOr("Target file is a directory"_ns)); + break; + case NS_ERROR_FILE_UNKNOWN_TYPE: + aPromise->MaybeRejectWithInvalidAccessError( + errMsg.refOr("Target file is of unknown type"_ns)); + break; + case NS_ERROR_FILE_NAME_TOO_LONG: + aPromise->MaybeRejectWithOperationError( + errMsg.refOr("Target file path is too long"_ns)); + break; + case NS_ERROR_FILE_UNRECOGNIZED_PATH: + aPromise->MaybeRejectWithOperationError( + errMsg.refOr("Target file path is not recognized"_ns)); + break; + case NS_ERROR_FILE_DIR_NOT_EMPTY: + aPromise->MaybeRejectWithOperationError( + errMsg.refOr("Target directory is not empty"_ns)); + break; + case NS_ERROR_FILE_DEVICE_FAILURE: + [[fallthrough]]; // to NS_ERROR_FILE_FS_CORRUPTED + case NS_ERROR_FILE_FS_CORRUPTED: + aPromise->MaybeRejectWithNotReadableError( + errMsg.refOr("Target file system may be corrupt or unavailable"_ns)); + break; + case NS_ERROR_FILE_CORRUPTED: + aPromise->MaybeRejectWithNotReadableError( + errMsg.refOr("Target file could not be read and may be corrupt"_ns)); + break; + case NS_ERROR_ILLEGAL_INPUT: + [[fallthrough]]; // NS_ERROR_ILLEGAL_VALUE + case NS_ERROR_ILLEGAL_VALUE: + aPromise->MaybeRejectWithDataError( + errMsg.refOr("Argument is not allowed"_ns)); + break; + case NS_ERROR_NOT_AVAILABLE: + aPromise->MaybeRejectWithNotFoundError(errMsg.refOr("Unavailable"_ns)); + break; + case NS_ERROR_ABORT: + aPromise->MaybeRejectWithAbortError(errMsg.refOr("Operation aborted"_ns)); + break; + default: + aPromise->MaybeRejectWithUnknownError(FormatErrorMessage( + aError.Code(), errMsg.refOr("Unexpected error"_ns).get())); + } +} + +static void RejectShuttingDown(Promise* aPromise) { + RejectJSPromise(aPromise, + IOUtils::IOError(NS_ERROR_ABORT).WithMessage(SHUTDOWN_ERROR)); +} + +static bool AssertParentProcessWithCallerLocationImpl(GlobalObject& aGlobal, + nsCString& reason) { + if (MOZ_LIKELY(XRE_IsParentProcess())) { + return true; + } + + AutoJSAPI jsapi; + nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); + MOZ_ALWAYS_TRUE(global); + MOZ_ALWAYS_TRUE(jsapi.Init(global)); + + JSContext* cx = jsapi.cx(); + + JS::AutoFilename scriptFilename; + unsigned lineNo = 0; + unsigned colNo = 0; + + NS_ENSURE_TRUE( + JS::DescribeScriptedCaller(cx, &scriptFilename, &lineNo, &colNo), false); + + NS_ENSURE_TRUE(scriptFilename.get(), false); + + reason.AppendPrintf(" Called from %s:%d:%d.", scriptFilename.get(), lineNo, + colNo); + return false; +} + +static void AssertParentProcessWithCallerLocation(GlobalObject& aGlobal) { + nsCString reason = "IOUtils can only be used in the parent process."_ns; + if (!AssertParentProcessWithCallerLocationImpl(aGlobal, reason)) { + MOZ_CRASH_UNSAFE_PRINTF("%s", reason.get()); + } +} + +// IOUtils implementation +/* static */ +IOUtils::StateMutex IOUtils::sState{"IOUtils::sState"}; + +/* static */ +template +already_AddRefed IOUtils::WithPromiseAndState(GlobalObject& aGlobal, + ErrorResult& aError, + Fn aFn) { + AssertParentProcessWithCallerLocation(aGlobal); + + RefPtr promise = CreateJSPromise(aGlobal, aError); + if (!promise) { + return nullptr; + } + + if (auto state = GetState()) { + aFn(promise, state.ref()); + } else { + RejectShuttingDown(promise); + } + return promise.forget(); +} + +/* static */ +template +void IOUtils::DispatchAndResolve(IOUtils::EventQueue* aQueue, Promise* aPromise, + Fn aFunc) { + RefPtr workerRef; + if (!NS_IsMainThread()) { + // We need to manually keep the worker alive until the promise returned by + // Dispatch() resolves or rejects. + workerRef = StrongWorkerRef::CreateForcibly(GetCurrentThreadWorkerPrivate(), + __func__); + } + + if (RefPtr> p = aQueue->Dispatch(std::move(aFunc))) { + p->Then( + GetCurrentSerialEventTarget(), __func__, + [workerRef, promise = RefPtr(aPromise)](OkT&& ok) { + ResolveJSPromise(promise, std::forward(ok)); + }, + [workerRef, promise = RefPtr(aPromise)](const IOError& err) { + RejectJSPromise(promise, err); + }); + } +} + +/* static */ +already_AddRefed IOUtils::Read(GlobalObject& aGlobal, + const nsAString& aPath, + const ReadOptions& aOptions, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + Maybe toRead = Nothing(); + if (!aOptions.mMaxBytes.IsNull()) { + if (aOptions.mMaxBytes.Value() == 0) { + // Resolve with an empty buffer. + nsTArray arr(0); + promise->MaybeResolve(TypedArrayCreator(arr)); + return; + } + toRead.emplace(aOptions.mMaxBytes.Value()); + } + + DispatchAndResolve( + state->mEventQueue, promise, + [file = std::move(file), offset = aOptions.mOffset, toRead, + decompress = aOptions.mDecompress]() { + return ReadSync(file, offset, toRead, decompress, + BufferKind::Uint8Array); + }); + }); +} + +/* static */ +RefPtr IOUtils::OpenFileForSyncReading(GlobalObject& aGlobal, + const nsAString& aPath, + ErrorResult& aRv) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + + // This API is only exposed to workers, so we should not be on the main + // thread here. + MOZ_RELEASE_ASSERT(!NS_IsMainThread()); + + nsCOMPtr file = new nsLocalFile(); + if (nsresult rv = PathUtils::InitFileWithPath(file, aPath); NS_FAILED(rv)) { + aRv.ThrowOperationError(FormatErrorMessage( + rv, "Could not parse path (%s)", NS_ConvertUTF16toUTF8(aPath).get())); + return nullptr; + } + + RefPtr stream = new nsFileRandomAccessStream(); + if (nsresult rv = + stream->Init(file, PR_RDONLY | nsIFile::OS_READAHEAD, 0666, 0); + NS_FAILED(rv)) { + aRv.ThrowOperationError( + FormatErrorMessage(rv, "Could not open the file at %s", + NS_ConvertUTF16toUTF8(aPath).get())); + return nullptr; + } + + int64_t size = 0; + if (nsresult rv = stream->GetSize(&size); NS_FAILED(rv)) { + aRv.ThrowOperationError(FormatErrorMessage( + rv, "Could not get the stream size for the file at %s", + NS_ConvertUTF16toUTF8(aPath).get())); + return nullptr; + } + + return new SyncReadFile(aGlobal.GetAsSupports(), std::move(stream), size); +} + +/* static */ +already_AddRefed IOUtils::ReadUTF8(GlobalObject& aGlobal, + const nsAString& aPath, + const ReadUTF8Options& aOptions, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve( + state->mEventQueue, promise, + [file = std::move(file), decompress = aOptions.mDecompress]() { + return ReadUTF8Sync(file, decompress); + }); + }); +} + +/* static */ +already_AddRefed IOUtils::ReadJSON(GlobalObject& aGlobal, + const nsAString& aPath, + const ReadUTF8Options& aOptions, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + RefPtr workerRef; + if (!NS_IsMainThread()) { + // We need to manually keep the worker alive until the promise + // returned by Dispatch() resolves or rejects. + workerRef = StrongWorkerRef::CreateForcibly( + GetCurrentThreadWorkerPrivate(), __func__); + } + + state->mEventQueue + ->template Dispatch( + [file, decompress = aOptions.mDecompress]() { + return ReadUTF8Sync(file, decompress); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [workerRef, promise = RefPtr{promise}, + file](JsBuffer&& aBuffer) { + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(promise->GetGlobalObject()))) { + promise->MaybeRejectWithUnknownError( + "Could not initialize JS API"); + return; + } + JSContext* cx = jsapi.cx(); + + JS::Rooted jsonStr( + cx, + IOUtils::JsBuffer::IntoString(cx, std::move(aBuffer))); + if (!jsonStr) { + RejectJSPromise(promise, IOError(NS_ERROR_OUT_OF_MEMORY)); + return; + } + + JS::Rooted val(cx); + if (!JS_ParseJSON(cx, jsonStr, &val)) { + JS::Rooted exn(cx); + if (JS_GetPendingException(cx, &exn)) { + JS_ClearPendingException(cx); + promise->MaybeReject(exn); + } else { + RejectJSPromise( + promise, + IOError(NS_ERROR_DOM_UNKNOWN_ERR) + .WithMessage( + "ParseJSON threw an uncatchable exception " + "while parsing file(%s)", + file->HumanReadablePath().get())); + } + + return; + } + + promise->MaybeResolve(val); + }, + [workerRef, promise = RefPtr{promise}](const IOError& aErr) { + RejectJSPromise(promise, aErr); + }); + }); +} + +/* static */ +already_AddRefed IOUtils::Write(GlobalObject& aGlobal, + const nsAString& aPath, + const Uint8Array& aData, + const WriteOptions& aOptions, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + aData.ComputeState(); + auto buf = + Buffer::CopyFrom(Span(aData.Data(), aData.Length())); + if (buf.isNothing()) { + promise->MaybeRejectWithOperationError( + "Out of memory: Could not allocate buffer while writing to file"); + return; + } + + auto opts = InternalWriteOpts::FromBinding(aOptions); + if (opts.isErr()) { + RejectJSPromise(promise, opts.unwrapErr()); + return; + } + + DispatchAndResolve( + state->mEventQueue, promise, + [file = std::move(file), buf = std::move(*buf), + opts = opts.unwrap()]() { return WriteSync(file, buf, opts); }); + }); +} + +/* static */ +already_AddRefed IOUtils::WriteUTF8(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aString, + const WriteOptions& aOptions, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + auto opts = InternalWriteOpts::FromBinding(aOptions); + if (opts.isErr()) { + RejectJSPromise(promise, opts.unwrapErr()); + return; + } + + DispatchAndResolve( + state->mEventQueue, promise, + [file = std::move(file), str = nsCString(aString), + opts = opts.unwrap()]() { + return WriteSync(file, AsBytes(Span(str)), opts); + }); + }); +} + +static bool AppendJsonAsUtf8(const char16_t* aData, uint32_t aLen, void* aStr) { + nsCString* str = static_cast(aStr); + return AppendUTF16toUTF8(Span(aData, aLen), *str, fallible); +} + +/* static */ +already_AddRefed IOUtils::WriteJSON(GlobalObject& aGlobal, + const nsAString& aPath, + JS::Handle aValue, + const WriteOptions& aOptions, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + auto opts = InternalWriteOpts::FromBinding(aOptions); + if (opts.isErr()) { + RejectJSPromise(promise, opts.unwrapErr()); + return; + } + + if (opts.inspect().mMode == WriteMode::Append || + opts.inspect().mMode == WriteMode::AppendOrCreate) { + promise->MaybeRejectWithNotSupportedError( + "IOUtils.writeJSON does not support appending to files."_ns); + return; + } + + JSContext* cx = aGlobal.Context(); + JS::Rooted rootedValue(cx, aValue); + nsCString utf8Str; + + if (!JS_Stringify(cx, &rootedValue, nullptr, JS::NullHandleValue, + AppendJsonAsUtf8, &utf8Str)) { + JS::Rooted exn(cx, JS::UndefinedValue()); + if (JS_GetPendingException(cx, &exn)) { + JS_ClearPendingException(cx); + promise->MaybeReject(exn); + } else { + RejectJSPromise( + promise, + IOError(NS_ERROR_DOM_UNKNOWN_ERR) + .WithMessage("Could not serialize object to JSON")); + } + return; + } + + DispatchAndResolve( + state->mEventQueue, promise, + [file = std::move(file), utf8Str = std::move(utf8Str), + opts = opts.unwrap()]() { + return WriteSync(file, AsBytes(Span(utf8Str)), opts); + }); + }); +} + +/* static */ +already_AddRefed IOUtils::Move(GlobalObject& aGlobal, + const nsAString& aSourcePath, + const nsAString& aDestPath, + const MoveOptions& aOptions, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr sourceFile = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(sourceFile, aSourcePath, promise); + + nsCOMPtr destFile = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(destFile, aDestPath, promise); + + DispatchAndResolve( + state->mEventQueue, promise, + [sourceFile = std::move(sourceFile), destFile = std::move(destFile), + noOverwrite = aOptions.mNoOverwrite]() { + return MoveSync(sourceFile, destFile, noOverwrite); + }); + }); +} + +/* static */ +already_AddRefed IOUtils::Remove(GlobalObject& aGlobal, + const nsAString& aPath, + const RemoveOptions& aOptions, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve( + state->mEventQueue, promise, + [file = std::move(file), ignoreAbsent = aOptions.mIgnoreAbsent, + recursive = aOptions.mRecursive]() { + return RemoveSync(file, ignoreAbsent, recursive); + }); + }); +} + +/* static */ +already_AddRefed IOUtils::MakeDirectory( + GlobalObject& aGlobal, const nsAString& aPath, + const MakeDirectoryOptions& aOptions, ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve(state->mEventQueue, promise, + [file = std::move(file), + createAncestors = aOptions.mCreateAncestors, + ignoreExisting = aOptions.mIgnoreExisting, + permissions = aOptions.mPermissions]() { + return MakeDirectorySync(file, createAncestors, + ignoreExisting, + permissions); + }); + }); +} + +already_AddRefed IOUtils::Stat(GlobalObject& aGlobal, + const nsAString& aPath, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve( + state->mEventQueue, promise, + [file = std::move(file)]() { return StatSync(file); }); + }); +} + +/* static */ +already_AddRefed IOUtils::Copy(GlobalObject& aGlobal, + const nsAString& aSourcePath, + const nsAString& aDestPath, + const CopyOptions& aOptions, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr sourceFile = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(sourceFile, aSourcePath, promise); + + nsCOMPtr destFile = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(destFile, aDestPath, promise); + + DispatchAndResolve( + state->mEventQueue, promise, + [sourceFile = std::move(sourceFile), destFile = std::move(destFile), + noOverwrite = aOptions.mNoOverwrite, + recursive = aOptions.mRecursive]() { + return CopySync(sourceFile, destFile, noOverwrite, recursive); + }); + }); +} + +/* static */ +already_AddRefed IOUtils::SetAccessTime( + GlobalObject& aGlobal, const nsAString& aPath, + const Optional& aAccess, ErrorResult& aError) { + return SetTime(aGlobal, aPath, aAccess, &nsIFile::SetLastAccessedTime, + aError); +} + +/* static */ +already_AddRefed IOUtils::SetModificationTime( + GlobalObject& aGlobal, const nsAString& aPath, + const Optional& aModification, ErrorResult& aError) { + return SetTime(aGlobal, aPath, aModification, &nsIFile::SetLastModifiedTime, + aError); +} + +/* static */ +already_AddRefed IOUtils::SetTime(GlobalObject& aGlobal, + const nsAString& aPath, + const Optional& aNewTime, + IOUtils::SetTimeFn aSetTimeFn, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + int64_t newTime = aNewTime.WasPassed() ? aNewTime.Value() + : PR_Now() / PR_USEC_PER_MSEC; + DispatchAndResolve( + state->mEventQueue, promise, + [file = std::move(file), aSetTimeFn, newTime]() { + return SetTimeSync(file, aSetTimeFn, newTime); + }); + }); +} + +/* static */ +already_AddRefed IOUtils::GetChildren( + GlobalObject& aGlobal, const nsAString& aPath, + const GetChildrenOptions& aOptions, ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve>( + state->mEventQueue, promise, + [file = std::move(file), ignoreAbsent = aOptions.mIgnoreAbsent]() { + return GetChildrenSync(file, ignoreAbsent); + }); + }); +} + +/* static */ +already_AddRefed IOUtils::SetPermissions(GlobalObject& aGlobal, + const nsAString& aPath, + uint32_t aPermissions, + const bool aHonorUmask, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { +#if defined(XP_UNIX) && !defined(ANDROID) + if (aHonorUmask) { + aPermissions &= ~nsSystemInfo::gUserUmask; + } +#endif + + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve( + state->mEventQueue, promise, + [file = std::move(file), permissions = aPermissions]() { + return SetPermissionsSync(file, permissions); + }); + }); +} + +/* static */ +already_AddRefed IOUtils::Exists(GlobalObject& aGlobal, + const nsAString& aPath, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve( + state->mEventQueue, promise, + [file = std::move(file)]() { return ExistsSync(file); }); + }); +} + +/* static */ +already_AddRefed IOUtils::CreateUniqueFile(GlobalObject& aGlobal, + const nsAString& aParent, + const nsAString& aPrefix, + const uint32_t aPermissions, + ErrorResult& aError) { + return CreateUnique(aGlobal, aParent, aPrefix, nsIFile::NORMAL_FILE_TYPE, + aPermissions, aError); +} + +/* static */ +already_AddRefed IOUtils::CreateUniqueDirectory( + GlobalObject& aGlobal, const nsAString& aParent, const nsAString& aPrefix, + const uint32_t aPermissions, ErrorResult& aError) { + return CreateUnique(aGlobal, aParent, aPrefix, nsIFile::DIRECTORY_TYPE, + aPermissions, aError); +} + +/* static */ +already_AddRefed IOUtils::CreateUnique(GlobalObject& aGlobal, + const nsAString& aParent, + const nsAString& aPrefix, + const uint32_t aFileType, + const uint32_t aPermissions, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aParent, promise); + + if (nsresult rv = file->Append(aPrefix); NS_FAILED(rv)) { + RejectJSPromise(promise, + IOError(rv).WithMessage( + "Could not append prefix `%s' to parent `%s'", + NS_ConvertUTF16toUTF8(aPrefix).get(), + file->HumanReadablePath().get())); + return; + } + + DispatchAndResolve( + state->mEventQueue, promise, + [file = std::move(file), aPermissions, aFileType]() { + return CreateUniqueSync(file, aFileType, aPermissions); + }); + }); +} + +/* static */ +already_AddRefed IOUtils::ComputeHexDigest( + GlobalObject& aGlobal, const nsAString& aPath, + const HashAlgorithm aAlgorithm, ErrorResult& aError) { + const bool nssInitialized = EnsureNSSInitializedChromeOrContent(); + + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + if (!nssInitialized) { + RejectJSPromise(promise, + IOError(NS_ERROR_UNEXPECTED) + .WithMessage("Could not initialize NSS")); + return; + } + + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve(state->mEventQueue, promise, + [file = std::move(file), aAlgorithm]() { + return ComputeHexDigestSync(file, + aAlgorithm); + }); + }); +} + +#if defined(XP_WIN) + +/* static */ +already_AddRefed IOUtils::GetWindowsAttributes(GlobalObject& aGlobal, + const nsAString& aPath, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + RefPtr workerRef; + if (!NS_IsMainThread()) { + // We need to manually keep the worker alive until the promise + // returned by Dispatch() resolves or rejects. + workerRef = StrongWorkerRef::CreateForcibly( + GetCurrentThreadWorkerPrivate(), __func__); + } + + state->mEventQueue + ->template Dispatch([file = std::move(file)]() { + return GetWindowsAttributesSync(file); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [workerRef, promise = RefPtr{promise}](const uint32_t aAttrs) { + WindowsFileAttributes attrs; + + attrs.mReadOnly.Construct(aAttrs & FILE_ATTRIBUTE_READONLY); + attrs.mHidden.Construct(aAttrs & FILE_ATTRIBUTE_HIDDEN); + attrs.mSystem.Construct(aAttrs & FILE_ATTRIBUTE_SYSTEM); + + promise->MaybeResolve(attrs); + }, + [workerRef, promise = RefPtr{promise}](const IOError& aErr) { + RejectJSPromise(promise, aErr); + }); + }); +} + +/* static */ +already_AddRefed IOUtils::SetWindowsAttributes( + GlobalObject& aGlobal, const nsAString& aPath, + const WindowsFileAttributes& aAttrs, ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + uint32_t setAttrs = 0; + uint32_t clearAttrs = 0; + + if (aAttrs.mReadOnly.WasPassed()) { + if (aAttrs.mReadOnly.Value()) { + setAttrs |= FILE_ATTRIBUTE_READONLY; + } else { + clearAttrs |= FILE_ATTRIBUTE_READONLY; + } + } + + if (aAttrs.mHidden.WasPassed()) { + if (aAttrs.mHidden.Value()) { + setAttrs |= FILE_ATTRIBUTE_HIDDEN; + } else { + clearAttrs |= FILE_ATTRIBUTE_HIDDEN; + } + } + + if (aAttrs.mSystem.WasPassed()) { + if (aAttrs.mSystem.Value()) { + setAttrs |= FILE_ATTRIBUTE_SYSTEM; + } else { + clearAttrs |= FILE_ATTRIBUTE_SYSTEM; + } + } + + DispatchAndResolve( + state->mEventQueue, promise, + [file = std::move(file), setAttrs, clearAttrs]() { + return SetWindowsAttributesSync(file, setAttrs, clearAttrs); + }); + }); +} + +#elif defined(XP_MACOSX) + +/* static */ +already_AddRefed IOUtils::HasMacXAttr(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aAttr, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve( + state->mEventQueue, promise, + [file = std::move(file), attr = nsCString(aAttr)]() { + return HasMacXAttrSync(file, attr); + }); + }); +} + +/* static */ +already_AddRefed IOUtils::GetMacXAttr(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aAttr, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve>( + state->mEventQueue, promise, + [file = std::move(file), attr = nsCString(aAttr)]() { + return GetMacXAttrSync(file, attr); + }); + }); +} + +/* static */ +already_AddRefed IOUtils::SetMacXAttr(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aAttr, + const Uint8Array& aValue, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + aValue.ComputeState(); + nsTArray value; + + if (!value.AppendElements(aValue.Data(), aValue.Length(), fallible)) { + RejectJSPromise( + promise, + IOError(NS_ERROR_OUT_OF_MEMORY) + .WithMessage( + "Could not allocate buffer to set extended attribute")); + return; + } + + DispatchAndResolve(state->mEventQueue, promise, + [file = std::move(file), attr = nsCString(aAttr), + value = std::move(value)] { + return SetMacXAttrSync(file, attr, value); + }); + }); +} + +/* static */ +already_AddRefed IOUtils::DelMacXAttr(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aAttr, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve( + state->mEventQueue, promise, + [file = std::move(file), attr = nsCString(aAttr)] { + return DelMacXAttrSync(file, attr); + }); + }); +} + +#endif + +/* static */ +already_AddRefed IOUtils::GetFile( + GlobalObject& aGlobal, const Sequence& aComponents, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + ErrorResult joinErr; + nsCOMPtr file = PathUtils::Join(aComponents, joinErr); + if (joinErr.Failed()) { + promise->MaybeReject(std::move(joinErr)); + return; + } + + nsCOMPtr parent; + if (nsresult rv = file->GetParent(getter_AddRefs(parent)); + NS_FAILED(rv)) { + RejectJSPromise(promise, IOError(rv).WithMessage( + "Could not get parent directory")); + return; + } + + state->mEventQueue + ->template Dispatch([parent = std::move(parent)]() { + return MakeDirectorySync(parent, /* aCreateAncestors = */ true, + /* aIgnoreExisting = */ true, 0755); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [file = std::move(file), promise = RefPtr(promise)](const Ok&) { + promise->MaybeResolve(file); + }, + [promise = RefPtr(promise)](const IOError& err) { + RejectJSPromise(promise, err); + }); + }); +} + +/* static */ +already_AddRefed IOUtils::GetDirectory( + GlobalObject& aGlobal, const Sequence& aComponents, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + ErrorResult joinErr; + nsCOMPtr dir = PathUtils::Join(aComponents, joinErr); + if (joinErr.Failed()) { + promise->MaybeReject(std::move(joinErr)); + return; + } + + state->mEventQueue + ->template Dispatch([dir]() { + return MakeDirectorySync(dir, /* aCreateAncestors = */ true, + /* aIgnoreExisting = */ true, 0755); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [dir, promise = RefPtr(promise)](const Ok&) { + promise->MaybeResolve(dir); + }, + [promise = RefPtr(promise)](const IOError& err) { + RejectJSPromise(promise, err); + }); + }); +} + +/* static */ +already_AddRefed IOUtils::CreateJSPromise(GlobalObject& aGlobal, + ErrorResult& aError) { + nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr promise = Promise::Create(global, aError); + if (aError.Failed()) { + return nullptr; + } + MOZ_ASSERT(promise); + return do_AddRef(promise); +} + +/* static */ +Result IOUtils::ReadSync( + nsIFile* aFile, const uint64_t aOffset, const Maybe aMaxBytes, + const bool aDecompress, IOUtils::BufferKind aBufferKind) { + MOZ_ASSERT(!NS_IsMainThread()); + + if (aMaxBytes.isSome() && aDecompress) { + return Err( + IOError(NS_ERROR_ILLEGAL_INPUT) + .WithMessage( + "The `maxBytes` and `decompress` options are not compatible")); + } + + if (aOffset > static_cast(INT64_MAX)) { + return Err(IOError(NS_ERROR_ILLEGAL_INPUT) + .WithMessage("Requested offset is too large (%" PRIu64 + " > %" PRId64 ")", + aOffset, INT64_MAX)); + } + + const int64_t offset = static_cast(aOffset); + + RefPtr stream = new nsFileRandomAccessStream(); + if (nsresult rv = + stream->Init(aFile, PR_RDONLY | nsIFile::OS_READAHEAD, 0666, 0); + NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage("Could not open the file at %s", + aFile->HumanReadablePath().get())); + } + + uint32_t bufSize = 0; + + if (aMaxBytes.isNothing()) { + // Limitation: We cannot read more than the maximum size of a TypedArray + // (UINT32_MAX bytes). Reject if we have been requested to + // perform too large of a read. + + int64_t rawStreamSize = -1; + if (nsresult rv = stream->GetSize(&rawStreamSize); NS_FAILED(rv)) { + return Err(IOError(NS_ERROR_FILE_ACCESS_DENIED) + .WithMessage("Could not get info for the file at %s", + aFile->HumanReadablePath().get())); + } + MOZ_RELEASE_ASSERT(rawStreamSize >= 0); + + uint64_t streamSize = static_cast(rawStreamSize); + if (aOffset >= streamSize) { + bufSize = 0; + } else { + if (streamSize - offset > static_cast(UINT32_MAX)) { + return Err(IOError(NS_ERROR_FILE_TOO_BIG) + .WithMessage( + "Could not read the file at %s with offset %" PRIu32 + " because it is too large(size=%" PRIu64 " bytes)", + aFile->HumanReadablePath().get(), offset, + streamSize)); + } + + bufSize = static_cast(streamSize - offset); + } + } else { + bufSize = aMaxBytes.value(); + } + + if (offset > 0) { + if (nsresult rv = stream->Seek(PR_SEEK_SET, offset); NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Could not seek to position %" PRId64 " in file %s", offset, + aFile->HumanReadablePath().get())); + } + } + + JsBuffer buffer = JsBuffer::CreateEmpty(aBufferKind); + + if (bufSize > 0) { + auto result = JsBuffer::Create(aBufferKind, bufSize); + if (result.isErr()) { + return result.propagateErr(); + } + buffer = result.unwrap(); + Span toRead = buffer.BeginWriting(); + + // Read the file from disk. + uint32_t totalRead = 0; + while (totalRead != bufSize) { + // Read no more than INT32_MAX on each call to stream->Read, otherwise it + // returns an error. + uint32_t bytesToReadThisChunk = + std::min(bufSize - totalRead, INT32_MAX); + uint32_t bytesRead = 0; + if (nsresult rv = + stream->Read(toRead.Elements(), bytesToReadThisChunk, &bytesRead); + NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Encountered an unexpected error while reading file(%s)", + aFile->HumanReadablePath().get())); + } + if (bytesRead == 0) { + break; + } + totalRead += bytesRead; + toRead = toRead.From(bytesRead); + } + + buffer.SetLength(totalRead); + } + + // Decompress the file contents, if required. + if (aDecompress) { + return MozLZ4::Decompress(AsBytes(buffer.BeginReading()), aBufferKind); + } + + return std::move(buffer); +} + +/* static */ +Result IOUtils::ReadUTF8Sync( + nsIFile* aFile, bool aDecompress) { + auto result = ReadSync(aFile, 0, Nothing{}, aDecompress, BufferKind::String); + if (result.isErr()) { + return result.propagateErr(); + } + + JsBuffer buffer = result.unwrap(); + if (!IsUtf8(buffer.BeginReading())) { + return Err( + IOError(NS_ERROR_FILE_CORRUPTED) + .WithMessage( + "Could not read file(%s) because it is not UTF-8 encoded", + aFile->HumanReadablePath().get())); + } + + return buffer; +} + +/* static */ +Result IOUtils::WriteSync( + nsIFile* aFile, const Span& aByteArray, + const IOUtils::InternalWriteOpts& aOptions) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsIFile* backupFile = aOptions.mBackupFile; + nsIFile* tempFile = aOptions.mTmpFile; + + bool exists = false; + MOZ_TRY(aFile->Exists(&exists)); + + if (exists && aOptions.mMode == WriteMode::Create) { + return Err(IOError(NS_ERROR_FILE_ALREADY_EXISTS) + .WithMessage("Refusing to overwrite the file at %s\n" + "Specify `mode: \"overwrite\"` to allow " + "overwriting the destination", + aFile->HumanReadablePath().get())); + } + + // If backupFile was specified, perform the backup as a move. + if (exists && backupFile) { + // We copy `destFile` here to a new `nsIFile` because + // `nsIFile::MoveToFollowingLinks` will update the path of the file. If we + // did not do this, we would end up having `destFile` point to the same + // location as `backupFile`. Then, when we went to write to `destFile`, we + // would end up overwriting `backupFile` and never actually write to the + // file we were supposed to. + nsCOMPtr toMove; + MOZ_ALWAYS_SUCCEEDS(aFile->Clone(getter_AddRefs(toMove))); + + bool noOverwrite = aOptions.mMode == WriteMode::Create; + + if (MoveSync(toMove, backupFile, noOverwrite).isErr()) { + return Err(IOError(NS_ERROR_FILE_COPY_OR_MOVE_FAILED) + .WithMessage("Failed to backup the source file(%s) to %s", + aFile->HumanReadablePath().get(), + backupFile->HumanReadablePath().get())); + } + } + + // If tempFile was specified, we will write to there first, then perform a + // move to ensure the file ends up at the final requested destination. + nsIFile* writeFile; + + if (tempFile) { + writeFile = tempFile; + } else { + writeFile = aFile; + } + + int32_t flags = PR_WRONLY; + + switch (aOptions.mMode) { + case WriteMode::Overwrite: + flags |= PR_TRUNCATE | PR_CREATE_FILE; + break; + + case WriteMode::Append: + flags |= PR_APPEND; + break; + + case WriteMode::AppendOrCreate: + flags |= PR_APPEND | PR_CREATE_FILE; + break; + + case WriteMode::Create: + flags |= PR_CREATE_FILE | PR_EXCL; + break; + + default: + MOZ_CRASH("IOUtils: unknown write mode"); + } + + if (aOptions.mFlush) { + flags |= PR_SYNC; + } + + // Try to perform the write and ensure that the file is closed before + // continuing. + uint32_t totalWritten = 0; + { + // Compress the byte array if required. + nsTArray compressed; + Span bytes; + if (aOptions.mCompress) { + auto rv = MozLZ4::Compress(aByteArray); + if (rv.isErr()) { + return rv.propagateErr(); + } + compressed = rv.unwrap(); + bytes = Span(reinterpret_cast(compressed.Elements()), + compressed.Length()); + } else { + bytes = Span(reinterpret_cast(aByteArray.Elements()), + aByteArray.Length()); + } + + RefPtr stream = new nsFileOutputStream(); + if (nsresult rv = stream->Init(writeFile, flags, 0666, 0); NS_FAILED(rv)) { + // Normalize platform-specific errors for opening a directory to an access + // denied error. + if (rv == nsresult::NS_ERROR_FILE_IS_DIRECTORY) { + rv = NS_ERROR_FILE_ACCESS_DENIED; + } + return Err( + IOError(rv).WithMessage("Could not open the file at %s for writing", + writeFile->HumanReadablePath().get())); + } + + // nsFileRandomAccessStream::Write uses PR_Write under the hood, which + // accepts a *int32_t* for the chunk size. + uint32_t chunkSize = INT32_MAX; + Span pendingBytes = bytes; + + while (pendingBytes.Length() > 0) { + if (pendingBytes.Length() < chunkSize) { + chunkSize = pendingBytes.Length(); + } + + uint32_t bytesWritten = 0; + if (nsresult rv = + stream->Write(pendingBytes.Elements(), chunkSize, &bytesWritten); + NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Could not write chunk (size = %" PRIu32 + ") to file %s. The file may be corrupt.", + chunkSize, writeFile->HumanReadablePath().get())); + } + pendingBytes = pendingBytes.From(bytesWritten); + totalWritten += bytesWritten; + } + } + + // If tempFile was passed, check destFile against writeFile and, if they + // differ, the operation is finished by performing a move. + if (tempFile) { + nsAutoStringN<256> destPath; + nsAutoStringN<256> writePath; + + MOZ_ALWAYS_SUCCEEDS(aFile->GetPath(destPath)); + MOZ_ALWAYS_SUCCEEDS(writeFile->GetPath(writePath)); + + // nsIFile::MoveToFollowingLinks will only update the path of the file if + // the move succeeds. + if (destPath != writePath) { + if (aOptions.mTmpFile) { + bool isDir = false; + if (nsresult rv = aFile->IsDirectory(&isDir); + NS_FAILED(rv) && !IsFileNotFound(rv)) { + return Err(IOError(rv).WithMessage("Could not stat the file at %s", + aFile->HumanReadablePath().get())); + } + + // If we attempt to write to a directory *without* a temp file, we get a + // permission error. + // + // However, if we are writing to a temp file first, when we copy the + // temp file over the destination file, we actually end up copying it + // inside the directory, which is not what we want. In this case, we are + // just going to bail out early. + if (isDir) { + return Err( + IOError(NS_ERROR_FILE_ACCESS_DENIED) + .WithMessage("Could not open the file at %s for writing", + aFile->HumanReadablePath().get())); + } + } + + if (MoveSync(writeFile, aFile, /* aNoOverwrite = */ false).isErr()) { + return Err( + IOError(NS_ERROR_FILE_COPY_OR_MOVE_FAILED) + .WithMessage( + "Could not move temporary file(%s) to destination(%s)", + writeFile->HumanReadablePath().get(), + aFile->HumanReadablePath().get())); + } + } + } + return totalWritten; +} + +/* static */ +Result 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 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 +Result 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 destDir; + nsAutoString destName; + MOZ_TRY(aDest->GetLeafName(destName)); + MOZ_TRY(aDest->GetParent(getter_AddRefs(destDir))); + + // We know `destName` is a file and therefore must have a parent directory. + MOZ_RELEASE_ASSERT(destDir); + + // NB: if destDir doesn't exist, then |CopyToFollowingLinks| or + // |MoveToFollowingLinks| will create it. + rv = (aSource->*aMethod)(destDir, destName); + if (NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Could not %s the source file(%s) to the destination(%s)", aMethodName, + aSource->HumanReadablePath().get(), aDest->HumanReadablePath().get())); + } + return Ok(); +} + +/* static */ +Result 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 IOUtils::MakeDirectorySync(nsIFile* aFile, + bool aCreateAncestors, + bool aIgnoreExisting, + int32_t aMode) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr parent; + MOZ_TRY(aFile->GetParent(getter_AddRefs(parent))); + if (!parent) { + // If we don't have a parent directory, we were called with a + // root directory. If the directory doesn't already exist (e.g., asking + // for a drive on Windows that does not exist), we will not be able to + // create it. + // + // Calling `nsLocalFile::Create()` on Windows can fail with + // `NS_ERROR_ACCESS_DENIED` trying to create a root directory, but we + // would rather the call succeed, so return early if the directory exists. + // + // Otherwise, we fall through to `nsiFile::Create()` and let it fail there + // instead. + bool exists = false; + MOZ_TRY(aFile->Exists(&exists)); + if (exists) { + return Ok(); + } + } + + nsresult rv = + aFile->Create(nsIFile::DIRECTORY_TYPE, aMode, !aCreateAncestors); + if (NS_FAILED(rv)) { + if (rv == NS_ERROR_FILE_ALREADY_EXISTS) { + // NB: We may report a success only if the target is an existing + // directory. We don't want to silence errors that occur if the target is + // an existing file, since trying to create a directory where a regular + // file exists may be indicative of a logic error. + bool isDirectory; + MOZ_TRY(aFile->IsDirectory(&isDirectory)); + if (!isDirectory) { + return Err(IOError(NS_ERROR_FILE_NOT_DIRECTORY) + .WithMessage("Could not create directory because the " + "target file(%s) exists " + "and is not a directory", + aFile->HumanReadablePath().get())); + } + // The directory exists. + // The caller may suppress this error. + if (aIgnoreExisting) { + return Ok(); + } + // Otherwise, forward it. + return Err(IOError(rv).WithMessage( + "Could not create directory because it already exists at %s\n" + "Specify the `ignoreExisting: true` option to mitigate this " + "error", + aFile->HumanReadablePath().get())); + } + return Err(IOError(rv).WithMessage("Could not create directory at %s", + aFile->HumanReadablePath().get())); + } + return Ok(); +} + +Result IOUtils::StatSync( + nsIFile* aFile) { + MOZ_ASSERT(!NS_IsMainThread()); + + InternalFileInfo info; + MOZ_ALWAYS_SUCCEEDS(aFile->GetPath(info.mPath)); + + bool isRegular = false; + // IsFile will stat and cache info in the file object. If the file doesn't + // exist, or there is an access error, we'll discover it here. + // Any subsequent errors are unexpected and will just be forwarded. + nsresult rv = aFile->IsFile(&isRegular); + if (NS_FAILED(rv)) { + IOError err(rv); + if (IsFileNotFound(rv)) { + return Err( + err.WithMessage("Could not stat file(%s) because it does not exist", + aFile->HumanReadablePath().get())); + } + return Err(err); + } + + // Now we can populate the info object by querying the file. + info.mType = FileType::Regular; + if (!isRegular) { + bool isDir = false; + MOZ_TRY(aFile->IsDirectory(&isDir)); + info.mType = isDir ? FileType::Directory : FileType::Other; + } + + int64_t size = -1; + if (info.mType == FileType::Regular) { + MOZ_TRY(aFile->GetFileSize(&size)); + } + info.mSize = size; + + PRTime creationTime = 0; + if (nsresult rv = aFile->GetCreationTime(&creationTime); NS_SUCCEEDED(rv)) { + info.mCreationTime.emplace(static_cast(creationTime)); + } else if (NS_FAILED(rv) && rv != NS_ERROR_NOT_IMPLEMENTED) { + // This field is only supported on some platforms. + return Err(IOError(rv)); + } + + PRTime lastAccessed = 0; + MOZ_TRY(aFile->GetLastAccessedTime(&lastAccessed)); + info.mLastAccessed = static_cast(lastAccessed); + + PRTime lastModified = 0; + MOZ_TRY(aFile->GetLastModifiedTime(&lastModified)); + info.mLastModified = static_cast(lastModified); + + MOZ_TRY(aFile->GetPermissions(&info.mPermissions)); + + return info; +} + +/* static */ +Result IOUtils::SetTimeSync( + nsIFile* aFile, IOUtils::SetTimeFn aSetTimeFn, int64_t aNewTime) { + MOZ_ASSERT(!NS_IsMainThread()); + + // nsIFile::SetLastModifiedTime will *not* do what is expected when passed 0 + // as an argument. Rather than setting the time to 0, it will recalculate the + // system time and set it to that value instead. We explicit forbid this, + // because this side effect is surprising. + // + // If it ever becomes possible to set a file time to 0, this check should be + // removed, though this use case seems rare. + if (aNewTime == 0) { + return Err( + IOError(NS_ERROR_ILLEGAL_VALUE) + .WithMessage( + "Refusing to set the modification time of file(%s) to 0.\n" + "To use the current system time, call `setModificationTime` " + "with no arguments", + aFile->HumanReadablePath().get())); + } + + nsresult rv = (aFile->*aSetTimeFn)(aNewTime); + + if (NS_FAILED(rv)) { + IOError err(rv); + if (IsFileNotFound(rv)) { + return Err( + err.WithMessage("Could not set modification time of file(%s) " + "because it does not exist", + aFile->HumanReadablePath().get())); + } + return Err(err); + } + return aNewTime; +} + +/* static */ +Result, IOUtils::IOError> IOUtils::GetChildrenSync( + nsIFile* aFile, bool aIgnoreAbsent) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsTArray children; + nsCOMPtr iter; + nsresult rv = aFile->GetDirectoryEntries(getter_AddRefs(iter)); + if (aIgnoreAbsent && IsFileNotFound(rv)) { + return children; + } + if (NS_FAILED(rv)) { + IOError err(rv); + if (IsFileNotFound(rv)) { + return Err(err.WithMessage( + "Could not get children of file(%s) because it does not exist", + aFile->HumanReadablePath().get())); + } + if (IsNotDirectory(rv)) { + return Err(err.WithMessage( + "Could not get children of file(%s) because it is not a directory", + aFile->HumanReadablePath().get())); + } + return Err(err); + } + + bool hasMoreElements = false; + MOZ_TRY(iter->HasMoreElements(&hasMoreElements)); + while (hasMoreElements) { + nsCOMPtr 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 IOUtils::SetPermissionsSync( + nsIFile* aFile, const uint32_t aPermissions) { + MOZ_ASSERT(!NS_IsMainThread()); + + MOZ_TRY(aFile->SetPermissions(aPermissions)); + return Ok{}; +} + +/* static */ +Result IOUtils::ExistsSync(nsIFile* aFile) { + MOZ_ASSERT(!NS_IsMainThread()); + + bool exists = false; + MOZ_TRY(aFile->Exists(&exists)); + + return exists; +} + +/* static */ +Result IOUtils::CreateUniqueSync( + nsIFile* aFile, const uint32_t aFileType, const uint32_t aPermissions) { + MOZ_ASSERT(!NS_IsMainThread()); + + if (nsresult rv = aFile->CreateUnique(aFileType, aPermissions); + NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage("Could not create unique path")); + } + + nsString path; + MOZ_ALWAYS_SUCCEEDS(aFile->GetPath(path)); + + return path; +} + +/* static */ +Result IOUtils::ComputeHexDigestSync( + nsIFile* aFile, const HashAlgorithm aAlgorithm) { + static constexpr size_t BUFFER_SIZE = 8192; + + SECOidTag alg; + switch (aAlgorithm) { + case HashAlgorithm::Sha1: + alg = SEC_OID_SHA1; + break; + + case HashAlgorithm::Sha256: + alg = SEC_OID_SHA256; + break; + + case HashAlgorithm::Sha384: + alg = SEC_OID_SHA384; + break; + + case HashAlgorithm::Sha512: + alg = SEC_OID_SHA512; + break; + + default: + MOZ_RELEASE_ASSERT(false, "Unexpected HashAlgorithm"); + } + + Digest digest; + if (nsresult rv = digest.Begin(alg); NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage("Could not hash file at %s", + aFile->HumanReadablePath().get())); + } + + RefPtr stream; + if (nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), aFile); + NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage("Could not open the file at %s", + aFile->HumanReadablePath().get())); + } + + char buffer[BUFFER_SIZE]; + uint32_t read = 0; + for (;;) { + if (nsresult rv = stream->Read(buffer, BUFFER_SIZE, &read); NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Encountered an unexpected error while reading file(%s)", + aFile->HumanReadablePath().get())); + } + if (read == 0) { + break; + } + + if (nsresult rv = + digest.Update(reinterpret_cast(buffer), read); + NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage("Could not hash file at %s", + aFile->HumanReadablePath().get())); + } + } + + AutoTArray rawDigest; + if (nsresult rv = digest.End(rawDigest); NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage("Could not hash file at %s", + aFile->HumanReadablePath().get())); + } + + nsCString hexDigest; + if (!hexDigest.SetCapacity(2 * rawDigest.Length(), fallible)) { + return Err(IOError(NS_ERROR_OUT_OF_MEMORY)); + } + + const char HEX[] = "0123456789abcdef"; + for (uint8_t b : rawDigest) { + hexDigest.Append(HEX[(b >> 4) & 0xF]); + hexDigest.Append(HEX[b & 0xF]); + } + + return hexDigest; +} + +#if defined(XP_WIN) + +Result IOUtils::GetWindowsAttributesSync( + nsIFile* aFile) { + MOZ_ASSERT(!NS_IsMainThread()); + + uint32_t attrs = 0; + + nsCOMPtr file = do_QueryInterface(aFile); + MOZ_ASSERT(file); + + if (nsresult rv = file->GetWindowsFileAttributes(&attrs); NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Could not get Windows file attributes for the file at `%s'", + aFile->HumanReadablePath().get())); + } + return attrs; +} + +Result IOUtils::SetWindowsAttributesSync( + nsIFile* aFile, const uint32_t aSetAttrs, const uint32_t aClearAttrs) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr file = do_QueryInterface(aFile); + MOZ_ASSERT(file); + + if (nsresult rv = file->SetWindowsFileAttributes(aSetAttrs, aClearAttrs); + NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Could not set Windows file attributes for the file at `%s'", + aFile->HumanReadablePath().get())); + } + + return Ok{}; +} + +#elif defined(XP_MACOSX) + +/* static */ +Result IOUtils::HasMacXAttrSync( + nsIFile* aFile, const nsCString& aAttr) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr file = do_QueryInterface(aFile); + MOZ_ASSERT(file); + + bool hasAttr = false; + if (nsresult rv = file->HasXAttr(aAttr, &hasAttr); NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Could not read the extended attribute `%s' from the file `%s'", + aAttr.get(), aFile->HumanReadablePath().get())); + } + + return hasAttr; +} + +/* static */ +Result, IOUtils::IOError> IOUtils::GetMacXAttrSync( + nsIFile* aFile, const nsCString& aAttr) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr file = do_QueryInterface(aFile); + MOZ_ASSERT(file); + + nsTArray value; + if (nsresult rv = file->GetXAttr(aAttr, value); NS_FAILED(rv)) { + auto err = IOError(rv); + + if (rv == NS_ERROR_NOT_AVAILABLE) { + return Err(err.WithMessage( + "The file `%s' does not have an extended attribute `%s'", + aFile->HumanReadablePath().get(), aAttr.get())); + } + + return Err(err.WithMessage( + "Could not read the extended attribute `%s' from the file `%s'", + aAttr.get(), aFile->HumanReadablePath().get())); + } + + return value; +} + +/* static */ +Result IOUtils::SetMacXAttrSync( + nsIFile* aFile, const nsCString& aAttr, const nsTArray& aValue) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr file = do_QueryInterface(aFile); + MOZ_ASSERT(file); + + if (nsresult rv = file->SetXAttr(aAttr, aValue); NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Could not set extended attribute `%s' on file `%s'", aAttr.get(), + aFile->HumanReadablePath().get())); + } + + return Ok{}; +} + +/* static */ +Result IOUtils::DelMacXAttrSync(nsIFile* aFile, + const nsCString& aAttr) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr file = do_QueryInterface(aFile); + MOZ_ASSERT(file); + + if (nsresult rv = file->DelXAttr(aAttr); NS_FAILED(rv)) { + auto err = IOError(rv); + + if (rv == NS_ERROR_NOT_AVAILABLE) { + return Err(err.WithMessage( + "The file `%s' does not have an extended attribute `%s'", + aFile->HumanReadablePath().get(), aAttr.get())); + } + + return Err(IOError(rv).WithMessage( + "Could not delete extended attribute `%s' on file `%s'", aAttr.get(), + aFile->HumanReadablePath().get())); + } + + return Ok{}; +} + +#endif + +/* static */ +void IOUtils::GetProfileBeforeChange(GlobalObject& aGlobal, + JS::MutableHandle aClient, + ErrorResult& aRv) { + return GetShutdownClient(aGlobal, aClient, aRv, + ShutdownPhase::ProfileBeforeChange); +} + +/* static */ +void IOUtils::GetSendTelemetry(GlobalObject& aGlobal, + JS::MutableHandle aClient, + ErrorResult& aRv) { + return GetShutdownClient(aGlobal, aClient, aRv, ShutdownPhase::SendTelemetry); +} + +/** + * Assert that the given phase has a shutdown client exposed by IOUtils + * + * There is no shutdown client exposed for XpcomWillShutdown. + */ +static void AssertHasShutdownClient(const IOUtils::ShutdownPhase aPhase) { + MOZ_RELEASE_ASSERT(aPhase >= IOUtils::ShutdownPhase::ProfileBeforeChange && + aPhase < IOUtils::ShutdownPhase::XpcomWillShutdown); +} + +/* static */ +void IOUtils::GetShutdownClient(GlobalObject& aGlobal, + JS::MutableHandle aClient, + ErrorResult& aRv, + const IOUtils::ShutdownPhase aPhase) { + MOZ_RELEASE_ASSERT(XRE_IsParentProcess()); + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + AssertHasShutdownClient(aPhase); + + if (auto state = GetState()) { + MOZ_RELEASE_ASSERT(state.ref()->mBlockerStatus != + ShutdownBlockerStatus::Uninitialized); + + if (state.ref()->mBlockerStatus == ShutdownBlockerStatus::Failed) { + aRv.ThrowAbortError("IOUtils: could not register shutdown blockers"); + return; + } + + MOZ_RELEASE_ASSERT(state.ref()->mBlockerStatus == + ShutdownBlockerStatus::Initialized); + auto result = state.ref()->mEventQueue->GetShutdownClient(aPhase); + if (result.isErr()) { + aRv.ThrowAbortError("IOUtils: could not get shutdown client"); + return; + } + + RefPtr client = result.unwrap(); + MOZ_RELEASE_ASSERT(client); + if (nsresult rv = client->GetJsclient(aClient); NS_FAILED(rv)) { + aRv.ThrowAbortError("IOUtils: Could not get shutdown jsclient"); + } + return; + } + + aRv.ThrowAbortError( + "IOUtils: profileBeforeChange phase has already finished"); +} + +/* sstatic */ +Maybe IOUtils::GetState() { + auto state = sState.Lock(); + if (state->mQueueStatus == EventQueueStatus::Shutdown) { + return Nothing{}; + } + + if (state->mQueueStatus == EventQueueStatus::Uninitialized) { + MOZ_RELEASE_ASSERT(!state->mEventQueue); + state->mEventQueue = new EventQueue(); + state->mQueueStatus = EventQueueStatus::Initialized; + + MOZ_RELEASE_ASSERT(state->mBlockerStatus == + ShutdownBlockerStatus::Uninitialized); + } + + if (NS_IsMainThread() && + state->mBlockerStatus == ShutdownBlockerStatus::Uninitialized) { + state->SetShutdownHooks(); + } + + return Some(std::move(state)); +} + +IOUtils::EventQueue::EventQueue() { + MOZ_ALWAYS_SUCCEEDS(NS_CreateBackgroundTaskQueue( + "IOUtils::EventQueue", getter_AddRefs(mBackgroundEventTarget))); + + MOZ_RELEASE_ASSERT(mBackgroundEventTarget); +} + +void IOUtils::State::SetShutdownHooks() { + if (mBlockerStatus != ShutdownBlockerStatus::Uninitialized) { + return; + } + + if (NS_WARN_IF(NS_FAILED(mEventQueue->SetShutdownHooks()))) { + mBlockerStatus = ShutdownBlockerStatus::Failed; + } else { + mBlockerStatus = ShutdownBlockerStatus::Initialized; + } + + if (mBlockerStatus != ShutdownBlockerStatus::Initialized) { + NS_WARNING("IOUtils: could not register shutdown blockers."); + } +} + +nsresult IOUtils::EventQueue::SetShutdownHooks() { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + + constexpr static auto STACK = u"IOUtils::EventQueue::SetShutdownHooks"_ns; + constexpr static auto FILE = NS_LITERAL_STRING_FROM_CSTRING(__FILE__); + + nsCOMPtr svc = services::GetAsyncShutdownService(); + if (!svc) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr profileBeforeChangeBlocker; + + // Create a shutdown blocker for the profile-before-change phase. + { + profileBeforeChangeBlocker = + new IOUtilsShutdownBlocker(ShutdownPhase::ProfileBeforeChange); + + nsCOMPtr globalClient; + MOZ_TRY(svc->GetProfileBeforeChange(getter_AddRefs(globalClient))); + MOZ_RELEASE_ASSERT(globalClient); + + MOZ_TRY(globalClient->AddBlocker(profileBeforeChangeBlocker, FILE, __LINE__, + STACK)); + } + + // Create the shutdown barrier for profile-before-change so that consumers can + // register shutdown blockers. + // + // The blocker we just created will wait for all clients registered on this + // barrier to finish. + { + nsCOMPtr barrier; + + // It is okay for this to fail. The created shutdown blocker won't await + // anything and shutdown will proceed. + MOZ_TRY(svc->MakeBarrier( + u"IOUtils: waiting for profileBeforeChange IO to complete"_ns, + getter_AddRefs(barrier))); + MOZ_RELEASE_ASSERT(barrier); + + mBarriers[ShutdownPhase::ProfileBeforeChange] = std::move(barrier); + } + + // Create a shutdown blocker for the profile-before-change-telemetry phase. + nsCOMPtr sendTelemetryBlocker; + { + sendTelemetryBlocker = + new IOUtilsShutdownBlocker(ShutdownPhase::SendTelemetry); + + nsCOMPtr globalClient; + MOZ_TRY(svc->GetSendTelemetry(getter_AddRefs(globalClient))); + MOZ_RELEASE_ASSERT(globalClient); + + MOZ_TRY( + globalClient->AddBlocker(sendTelemetryBlocker, FILE, __LINE__, STACK)); + } + + // Create the shutdown barrier for profile-before-change-telemetry so that + // consumers can register shutdown blockers. + // + // The blocker we just created will wait for all clients registered on this + // barrier to finish. + { + nsCOMPtr barrier; + + MOZ_TRY(svc->MakeBarrier( + u"IOUtils: waiting for sendTelemetry IO to complete"_ns, + getter_AddRefs(barrier))); + MOZ_RELEASE_ASSERT(barrier); + + // Add a blocker on the previous shutdown phase. + nsCOMPtr client; + MOZ_TRY(barrier->GetClient(getter_AddRefs(client))); + + MOZ_TRY( + client->AddBlocker(profileBeforeChangeBlocker, FILE, __LINE__, STACK)); + + mBarriers[ShutdownPhase::SendTelemetry] = std::move(barrier); + } + + // Create a shutdown blocker for the xpcom-will-shutdown phase. + { + nsCOMPtr globalClient; + MOZ_TRY(svc->GetXpcomWillShutdown(getter_AddRefs(globalClient))); + MOZ_RELEASE_ASSERT(globalClient); + + nsCOMPtr blocker = + new IOUtilsShutdownBlocker(ShutdownPhase::XpcomWillShutdown); + MOZ_TRY(globalClient->AddBlocker( + blocker, FILE, __LINE__, u"IOUtils::EventQueue::SetShutdownHooks"_ns)); + } + + // Create a shutdown barrier for the xpcom-will-shutdown phase. + // + // The blocker we just created will wait for all clients registered on this + // barrier to finish. + // + // The only client registered on this barrier should be a blocker for the + // previous phase. This is to ensure that all shutdown IO happens when + // shutdown phases do not happen (e.g., in xpcshell tests where + // profile-before-change does not occur). + { + nsCOMPtr barrier; + + MOZ_TRY(svc->MakeBarrier( + u"IOUtils: waiting for xpcomWillShutdown IO to complete"_ns, + getter_AddRefs(barrier))); + MOZ_RELEASE_ASSERT(barrier); + + // Add a blocker on the previous shutdown phase. + nsCOMPtr client; + MOZ_TRY(barrier->GetClient(getter_AddRefs(client))); + + client->AddBlocker(sendTelemetryBlocker, FILE, __LINE__, + u"IOUtils::EventQueue::SetShutdownHooks"_ns); + + mBarriers[ShutdownPhase::XpcomWillShutdown] = std::move(barrier); + } + + return NS_OK; +} + +template +RefPtr> IOUtils::EventQueue::Dispatch(Fn aFunc) { + MOZ_RELEASE_ASSERT(mBackgroundEventTarget); + + auto promise = + MakeRefPtr::Private>(__func__); + mBackgroundEventTarget->Dispatch( + NS_NewRunnableFunction("IOUtils::EventQueue::Dispatch", + [promise, func = std::move(aFunc)] { + Result result = func(); + if (result.isErr()) { + promise->Reject(result.unwrapErr(), __func__); + } else { + promise->Resolve(result.unwrap(), __func__); + } + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + return promise; +}; + +Result, nsresult> +IOUtils::EventQueue::GetShutdownBarrier(const IOUtils::ShutdownPhase aPhase) { + if (!mBarriers[aPhase]) { + return Err(NS_ERROR_NOT_AVAILABLE); + } + + return do_AddRef(mBarriers[aPhase]); +} + +Result, nsresult> +IOUtils::EventQueue::GetShutdownClient(const IOUtils::ShutdownPhase aPhase) { + AssertHasShutdownClient(aPhase); + + if (!mBarriers[aPhase]) { + return Err(NS_ERROR_NOT_AVAILABLE); + } + + nsCOMPtr client; + MOZ_TRY(mBarriers[aPhase]->GetClient(getter_AddRefs(client))); + + return do_AddRef(client); +} + +/* static */ +Result, IOUtils::IOError> IOUtils::MozLZ4::Compress( + Span aUncompressed) { + nsTArray 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 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(aUncompressed.Elements()), + aUncompressed.Length(), + reinterpret_cast(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::MozLZ4::Decompress( + Span 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 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(contents.Elements()), contents.Length(), + reinterpret_cast(decompressed.Elements()), + expectedDecompressedSize, &actualSize)) { + return Err( + IOError(NS_ERROR_FILE_CORRUPTED) + .WithMessage( + "Could not decompress file contents, the file may be corrupt")); + } + decompressed.SetLength(actualSize); + return decompressed; +} + +NS_IMPL_ISUPPORTS(IOUtilsShutdownBlocker, nsIAsyncShutdownBlocker, + nsIAsyncShutdownCompletionCallback); + +NS_IMETHODIMP IOUtilsShutdownBlocker::GetName(nsAString& aName) { + aName = u"IOUtils Blocker ("_ns; + aName.Append(PHASE_NAMES[mPhase]); + aName.Append(')'); + + return NS_OK; +} + +NS_IMETHODIMP IOUtilsShutdownBlocker::BlockShutdown( + nsIAsyncShutdownClient* aBarrierClient) { + using EventQueueStatus = IOUtils::EventQueueStatus; + using ShutdownPhase = IOUtils::ShutdownPhase; + + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + + nsCOMPtr barrier; + + { + auto state = IOUtils::sState.Lock(); + if (state->mQueueStatus == EventQueueStatus::Shutdown) { + // If the previous blockers have already run, then the event queue is + // already torn down and we have nothing to do. + + MOZ_RELEASE_ASSERT(mPhase == ShutdownPhase::XpcomWillShutdown); + MOZ_RELEASE_ASSERT(!state->mEventQueue); + + Unused << NS_WARN_IF(NS_FAILED(aBarrierClient->RemoveBlocker(this))); + mParentClient = nullptr; + + return NS_OK; + } + + MOZ_RELEASE_ASSERT(state->mEventQueue); + + mParentClient = aBarrierClient; + + barrier = state->mEventQueue->GetShutdownBarrier(mPhase).unwrapOr(nullptr); + } + + // We cannot barrier->Wait() while holding the mutex because it will lead to + // deadlock. + if (!barrier || NS_WARN_IF(NS_FAILED(barrier->Wait(this)))) { + // If we don't have a barrier, we still need to flush the IOUtils event + // queue and disable task submission. + // + // Likewise, if waiting on the barrier failed, we are going to make our best + // attempt to clean up. + Unused << Done(); + } + + return NS_OK; +} + +NS_IMETHODIMP IOUtilsShutdownBlocker::Done() { + using EventQueueStatus = IOUtils::EventQueueStatus; + using ShutdownPhase = IOUtils::ShutdownPhase; + + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + + bool didFlush = false; + + { + auto state = IOUtils::sState.Lock(); + + if (state->mEventQueue) { + MOZ_RELEASE_ASSERT(state->mQueueStatus == EventQueueStatus::Initialized); + + // This method is called once we have served all shutdown clients. Now we + // flush the remaining IO queue. This ensures any straggling IO that was + // not part of the shutdown blocker finishes before we move to the next + // phase. + state->mEventQueue->Dispatch([]() { return Ok{}; }) + ->Then(GetMainThreadSerialEventTarget(), __func__, + [self = RefPtr(this)]() { self->OnFlush(); }); + + // And if we're the last shutdown phase to allow IO, disable the event + // queue to disallow further IO requests. + if (mPhase >= LAST_IO_PHASE) { + state->mQueueStatus = EventQueueStatus::Shutdown; + } + + didFlush = true; + } + } + + // If we have already shut down the event loop, then call OnFlush to stop + // blocking our parent shutdown client. + if (!didFlush) { + MOZ_RELEASE_ASSERT(mPhase == ShutdownPhase::XpcomWillShutdown); + OnFlush(); + } + + return NS_OK; +} + +void IOUtilsShutdownBlocker::OnFlush() { + if (mParentClient) { + (void)NS_WARN_IF(NS_FAILED(mParentClient->RemoveBlocker(this))); + mParentClient = nullptr; + + // If we are past the last shutdown phase that allows IO, + // we can shutdown the event queue here because no additional IO requests + // will be allowed (see |Done()|). + if (mPhase >= LAST_IO_PHASE) { + auto state = IOUtils::sState.Lock(); + if (state->mEventQueue) { + state->mEventQueue = nullptr; + } + } + } +} + +NS_IMETHODIMP IOUtilsShutdownBlocker::GetState(nsIPropertyBag** aState) { + return NS_OK; +} + +Result +IOUtils::InternalWriteOpts::FromBinding(const WriteOptions& aOptions) { + InternalWriteOpts opts; + opts.mFlush = aOptions.mFlush; + opts.mMode = aOptions.mMode; + + if (aOptions.mBackupFile.WasPassed()) { + opts.mBackupFile = new nsLocalFile(); + if (nsresult rv = PathUtils::InitFileWithPath(opts.mBackupFile, + aOptions.mBackupFile.Value()); + NS_FAILED(rv)) { + return Err(IOUtils::IOError(rv).WithMessage( + "Could not parse path of backupFile (%s)", + NS_ConvertUTF16toUTF8(aOptions.mBackupFile.Value()).get())); + } + } + + if (aOptions.mTmpPath.WasPassed()) { + opts.mTmpFile = new nsLocalFile(); + if (nsresult rv = PathUtils::InitFileWithPath(opts.mTmpFile, + aOptions.mTmpPath.Value()); + NS_FAILED(rv)) { + return Err(IOUtils::IOError(rv).WithMessage( + "Could not parse path of temp file (%s)", + NS_ConvertUTF16toUTF8(aOptions.mTmpPath.Value()).get())); + } + } + + opts.mCompress = aOptions.mCompress; + return opts; +} + +/* static */ +Result IOUtils::JsBuffer::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(js::StringBufferArena, mCapacity)); + } else { + MOZ_RELEASE_ASSERT(aBufferKind == BufferKind::Uint8Array); + mBuffer = JS::UniqueChars( + js_pod_arena_malloc(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(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 arrayBuffer( + aCx, JS::NewArrayBufferWithContents(aCx, aBuffer.mLength, + reinterpret_cast(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); +} + +[[nodiscard]] bool ToJSValue(JSContext* aCx, IOUtils::JsBuffer&& aBuffer, + JS::MutableHandle aValue) { + if (aBuffer.mBufferKind == IOUtils::BufferKind::String) { + JSString* str = IOUtils::JsBuffer::IntoString(aCx, std::move(aBuffer)); + if (!str) { + return false; + } + + aValue.setString(str); + return true; + } + + JSObject* array = IOUtils::JsBuffer::IntoUint8Array(aCx, std::move(aBuffer)); + if (!array) { + return false; + } + + aValue.setObject(*array); + return true; +} + +// SyncReadFile + +NS_IMPL_CYCLE_COLLECTING_ADDREF(SyncReadFile) +NS_IMPL_CYCLE_COLLECTING_RELEASE(SyncReadFile) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SyncReadFile) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(SyncReadFile, mParent) + +SyncReadFile::SyncReadFile(nsISupports* aParent, + RefPtr&& aStream, + int64_t aSize) + : mParent(aParent), mStream(std::move(aStream)), mSize(aSize) { + MOZ_RELEASE_ASSERT(mSize >= 0); +} + +SyncReadFile::~SyncReadFile() = default; + +JSObject* SyncReadFile::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return SyncReadFile_Binding::Wrap(aCx, this, aGivenProto); +} + +void SyncReadFile::ReadBytesInto(const Uint8Array& aDestArray, + const int64_t aOffset, ErrorResult& aRv) { + if (!mStream) { + return aRv.ThrowOperationError("SyncReadFile is closed"); + } + + aDestArray.ComputeState(); + + auto rangeEnd = CheckedInt64(aOffset) + aDestArray.Length(); + if (!rangeEnd.isValid()) { + return aRv.ThrowOperationError("Requested range overflows i64"); + } + + if (rangeEnd.value() > mSize) { + return aRv.ThrowOperationError( + "Requested range overflows SyncReadFile size"); + } + + uint32_t readLen{aDestArray.Length()}; + if (readLen == 0) { + return; + } + + if (nsresult rv = mStream->Seek(PR_SEEK_SET, aOffset); NS_FAILED(rv)) { + return aRv.ThrowOperationError( + FormatErrorMessage(rv, "Could not seek to position %lld", aOffset)); + } + + Span toRead(reinterpret_cast(aDestArray.Data()), readLen); + + uint32_t totalRead = 0; + while (totalRead != readLen) { + // Read no more than INT32_MAX on each call to mStream->Read, otherwise it + // returns an error. + uint32_t bytesToReadThisChunk = + std::min(readLen - totalRead, INT32_MAX); + + uint32_t bytesRead = 0; + if (nsresult rv = + mStream->Read(toRead.Elements(), bytesToReadThisChunk, &bytesRead); + NS_FAILED(rv)) { + return aRv.ThrowOperationError(FormatErrorMessage( + rv, "Encountered an unexpected error while reading file stream")); + } + if (bytesRead == 0) { + return aRv.ThrowOperationError( + "Reading stopped before the entire array was filled"); + } + totalRead += bytesRead; + toRead = toRead.From(bytesRead); + } +} + +void SyncReadFile::Close() { mStream = nullptr; } + +#ifdef XP_UNIX +namespace { + +static nsCString FromUnixString(const IOUtils::UnixString& aString) { + if (aString.IsUTF8String()) { + return aString.GetAsUTF8String(); + } + if (aString.IsUint8Array()) { + const auto& u8a = aString.GetAsUint8Array(); + u8a.ComputeState(); + // Cast to deal with char signedness + return nsCString(reinterpret_cast(u8a.Data()), u8a.Length()); + } + MOZ_CRASH("unreachable"); +} + +} // namespace + +// static +uint32_t IOUtils::LaunchProcess(GlobalObject& aGlobal, + const Sequence& aArgv, + const LaunchOptions& aOptions, + ErrorResult& aRv) { + // The binding is worker-only, so should always be off-main-thread. + MOZ_ASSERT(!NS_IsMainThread()); + + // This generally won't work in child processes due to sandboxing. + AssertParentProcessWithCallerLocation(aGlobal); + + std::vector argv; + base::LaunchOptions options; + + for (const auto& arg : aArgv) { + argv.push_back(FromUnixString(arg).get()); + } + + size_t envLen = aOptions.mEnvironment.Length(); + base::EnvironmentArray envp(new char*[envLen + 1]); + for (size_t i = 0; i < envLen; ++i) { + // EnvironmentArray is a UniquePtr instance which will `free` + // these strings. + envp[i] = strdup(FromUnixString(aOptions.mEnvironment[i]).get()); + } + envp[envLen] = nullptr; + options.full_env = std::move(envp); + + if (aOptions.mWorkdir.WasPassed()) { + options.workdir = FromUnixString(aOptions.mWorkdir.Value()).get(); + } + + if (aOptions.mFdMap.WasPassed()) { + for (const auto& fdItem : aOptions.mFdMap.Value()) { + options.fds_to_remap.push_back({fdItem.mSrc, fdItem.mDst}); + } + } + +# ifdef XP_MACOSX + options.disclaim = aOptions.mDisclaim; +# endif + + base::ProcessHandle pid; + static_assert(sizeof(pid) <= sizeof(uint32_t), + "WebIDL long should be large enough for a pid"); + bool ok = base::LaunchApp(argv, options, &pid); + if (!ok) { + aRv.Throw(NS_ERROR_FAILURE); + return 0; + } + + MOZ_ASSERT(pid >= 0); + return static_cast(pid); +} +#endif // XP_UNIX + +} // namespace mozilla::dom + +#undef REJECT_IF_INIT_PATH_FAILED diff --git a/dom/system/IOUtils.h b/dom/system/IOUtils.h new file mode 100644 index 0000000000..a5fea23c60 --- /dev/null +++ b/dom/system/IOUtils.h @@ -0,0 +1,921 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_IOUtils__ +#define mozilla_dom_IOUtils__ + +#include "js/Utility.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Attributes.h" +#include "mozilla/Buffer.h" +#include "mozilla/DataMutex.h" +#include "mozilla/MozPromise.h" +#include "mozilla/Result.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/IOUtilsBinding.h" +#include "mozilla/dom/TypedArray.h" +#include "nsIAsyncShutdown.h" +#include "nsIFile.h" +#include "nsISerialEventTarget.h" +#include "nsPrintfCString.h" +#include "nsProxyRelease.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsTArray.h" +#include "prio.h" + +class nsFileRandomAccessStream; + +namespace mozilla { + +/** + * Utility class to be used with |UniquePtr| to automatically close NSPR file + * descriptors when they go out of scope. + * + * Example: + * + * UniquePtr fd = PR_Open(path, flags, mode); + */ +class PR_CloseDelete { + public: + constexpr PR_CloseDelete() = default; + PR_CloseDelete(const PR_CloseDelete& aOther) = default; + PR_CloseDelete(PR_CloseDelete&& aOther) = default; + PR_CloseDelete& operator=(const PR_CloseDelete& aOther) = default; + PR_CloseDelete& operator=(PR_CloseDelete&& aOther) = default; + + void operator()(PRFileDesc* aPtr) const { PR_Close(aPtr); } +}; + +namespace dom { + +/** + * Implementation for the Web IDL interface at dom/chrome-webidl/IOUtils.webidl. + * Methods of this class must only be called from the parent process. + */ +class IOUtils final { + public: + class IOError; + + enum class ShutdownPhase : uint8_t { + ProfileBeforeChange, + SendTelemetry, + XpcomWillShutdown, + Count, + }; + + template + using PhaseArray = + EnumeratedArray; + + static already_AddRefed Read(GlobalObject& aGlobal, + const nsAString& aPath, + const ReadOptions& aOptions, + ErrorResult& aError); + + static already_AddRefed ReadUTF8(GlobalObject& aGlobal, + const nsAString& aPath, + const ReadUTF8Options& aOptions, + ErrorResult& aError); + + static already_AddRefed ReadJSON(GlobalObject& aGlobal, + const nsAString& aPath, + const ReadUTF8Options& aOptions, + ErrorResult& aError); + + static already_AddRefed Write(GlobalObject& aGlobal, + const nsAString& aPath, + const Uint8Array& aData, + const WriteOptions& aOptions, + ErrorResult& aError); + + static already_AddRefed WriteUTF8(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aString, + const WriteOptions& aOptions, + ErrorResult& aError); + + static already_AddRefed WriteJSON(GlobalObject& aGlobal, + const nsAString& aPath, + JS::Handle aValue, + const WriteOptions& aOptions, + ErrorResult& aError); + + static already_AddRefed Move(GlobalObject& aGlobal, + const nsAString& aSourcePath, + const nsAString& aDestPath, + const MoveOptions& aOptions, + ErrorResult& aError); + + static already_AddRefed Remove(GlobalObject& aGlobal, + const nsAString& aPath, + const RemoveOptions& aOptions, + ErrorResult& aError); + + static already_AddRefed MakeDirectory( + GlobalObject& aGlobal, const nsAString& aPath, + const MakeDirectoryOptions& aOptions, ErrorResult& aError); + + static already_AddRefed Stat(GlobalObject& aGlobal, + const nsAString& aPath, + ErrorResult& aError); + + static already_AddRefed Copy(GlobalObject& aGlobal, + const nsAString& aSourcePath, + const nsAString& aDestPath, + const CopyOptions& aOptions, + ErrorResult& aError); + + static already_AddRefed SetAccessTime( + GlobalObject& aGlobal, const nsAString& aPath, + const Optional& aAccess, ErrorResult& aError); + + static already_AddRefed SetModificationTime( + GlobalObject& aGlobal, const nsAString& aPath, + const Optional& aModification, ErrorResult& aError); + + private: + using SetTimeFn = decltype(&nsIFile::SetLastAccessedTime); + + static_assert( + std::is_same_v); + + static already_AddRefed SetTime(GlobalObject& aGlobal, + const nsAString& aPath, + const Optional& aNewTime, + SetTimeFn aSetTimeFn, + ErrorResult& aError); + + public: + static already_AddRefed GetChildren( + GlobalObject& aGlobal, const nsAString& aPath, + const GetChildrenOptions& aOptions, ErrorResult& aError); + + static already_AddRefed SetPermissions(GlobalObject& aGlobal, + const nsAString& aPath, + uint32_t aPermissions, + const bool aHonorUmask, + ErrorResult& aError); + + static already_AddRefed Exists(GlobalObject& aGlobal, + const nsAString& aPath, + ErrorResult& aError); + + static already_AddRefed CreateUniqueFile(GlobalObject& aGlobal, + const nsAString& aParent, + const nsAString& aPrefix, + const uint32_t aPermissions, + ErrorResult& aError); + static already_AddRefed CreateUniqueDirectory( + GlobalObject& aGlobal, const nsAString& aParent, const nsAString& aPrefix, + const uint32_t aPermissions, ErrorResult& aError); + + private: + /** + * A helper method for CreateUniqueFile and CreateUniqueDirectory. + */ + static already_AddRefed CreateUnique(GlobalObject& aGlobal, + const nsAString& aParent, + const nsAString& aPrefix, + const uint32_t aFileType, + const uint32_t aPermissions, + ErrorResult& aError); + + public: + static already_AddRefed ComputeHexDigest( + GlobalObject& aGlobal, const nsAString& aPath, + const HashAlgorithm aAlgorithm, ErrorResult& aError); + +#if defined(XP_WIN) + static already_AddRefed GetWindowsAttributes(GlobalObject& aGlobal, + const nsAString& aPath, + ErrorResult& aError); + + static already_AddRefed SetWindowsAttributes( + GlobalObject& aGlobal, const nsAString& aPath, + const mozilla::dom::WindowsFileAttributes& aAttrs, ErrorResult& aError); +#elif defined(XP_MACOSX) + static already_AddRefed HasMacXAttr(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aAttr, + ErrorResult& aError); + static already_AddRefed GetMacXAttr(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aAttr, + ErrorResult& aError); + static already_AddRefed SetMacXAttr(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aAttr, + const Uint8Array& aValue, + ErrorResult& aError); + static already_AddRefed DelMacXAttr(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aAttr, + ErrorResult& aError); +#endif + +#ifdef XP_UNIX + using UnixString = OwningUTF8StringOrUint8Array; + static uint32_t LaunchProcess(GlobalObject& aGlobal, + const Sequence& aArgv, + const LaunchOptions& aOptions, + ErrorResult& aRv); +#endif + + static already_AddRefed GetFile( + GlobalObject& aGlobal, const Sequence& aComponents, + ErrorResult& aError); + + static already_AddRefed GetDirectory( + GlobalObject& aGlobal, const Sequence& aComponents, + ErrorResult& aError); + + static void GetProfileBeforeChange(GlobalObject& aGlobal, + JS::MutableHandle, + ErrorResult& aRv); + + static void GetSendTelemetry(GlobalObject& aGlobal, + JS::MutableHandle, ErrorResult& aRv); + + static RefPtr OpenFileForSyncReading(GlobalObject& aGlobal, + const nsAString& aPath, + ErrorResult& aRv); + + class JsBuffer; + + /** + * The kind of buffer to allocate. + * + * This controls what kind of JS object (a JSString or a Uint8Array) is + * returned by |ToJSValue()|. + */ + enum class BufferKind { + String, + Uint8Array, + }; + + private: + ~IOUtils() = default; + + template + using IOPromise = MozPromise; + + friend class IOUtilsShutdownBlocker; + struct InternalFileInfo; + struct InternalWriteOpts; + class MozLZ4; + class EventQueue; + class State; + + template + static already_AddRefed WithPromiseAndState(GlobalObject& aGlobal, + ErrorResult& aError, + Fn aFn); + + /** + * Dispatch a task on the event queue and resolve or reject the associated + * promise based on the result. + * + * NB: If the calling thread is a worker, this function takes care of keepting + * it alive until the |IOPromise| can complete. + * + * @param aPromise The promise corresponding to the task running on the event + * queue. + * @param aFunc The task to run. + */ + template + static void DispatchAndResolve(EventQueue* aQueue, Promise* aPromise, + Fn aFunc); + + /** + * Creates a new JS Promise. + * + * @return The new promise, or |nullptr| on failure. + */ + static already_AddRefed CreateJSPromise(GlobalObject& aGlobal, + ErrorResult& aError); + + // Allow conversion of |InternalFileInfo| with |ToJSValue|. + friend bool ToJSValue(JSContext* aCx, + const InternalFileInfo& aInternalFileInfo, + JS::MutableHandle aValue); + + /** + * Attempts to read the entire file at |aPath| into a buffer. + * + * @param aFile The location of the file. + * @param aOffset The offset to start reading from. + * @param aMaxBytes If |Some|, then only read up this this number of bytes, + * otherwise attempt to read the whole file. + * @param aDecompress If true, decompress the bytes read from disk before + * returning the result to the caller. + * @param aBufferKind The kind of buffer to allocate. + * + * @return A buffer containing the entire (decompressed) file contents, or an + * error. + */ + static Result ReadSync(nsIFile* aFile, + const uint64_t aOffset, + const Maybe 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 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 WriteSync( + nsIFile* aFile, const Span& aByteArray, + const InternalWriteOpts& aOptions); + + /** + * Attempts to move the file located at |aSourceFile| to |aDestFile|. + * + * @param aSourceFile The location of the file to move. + * @param aDestFile The destination for the file. + * @param aNoOverWrite If true, abort with an error if a file already exists + * at |aDestFile|. Otherwise, the file will be overwritten by the move. + * + * @return Ok if the file was moved successfully, or an error. + */ + static Result 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 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 + static Result 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 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 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 StatSync(nsIFile* aFile); + + /** + * Attempts to update the last access or modification time of the file at + * |aFile|. + * + * @param aFile The location of the file. + * @param SetTimeFn A member function pointer to either + * nsIFile::SetLastAccessedTime or + * nsIFile::SetLastModifiedTime. + * @param aNewTime Some value in milliseconds since Epoch. + * + * @return Timestamp of the file if the operation was successful, or an error. + */ + static Result SetTimeSync(nsIFile* aFile, + SetTimeFn aSetTimeFn, + int64_t aNewTime); + + /** + * Returns the immediate children of the directory at |aFile|, if any. + * + * @param aFile The location of the directory. + * + * @return An array of absolute paths identifying the children of |aFile|. + * If there are no children, an empty array. Otherwise, an error. + */ + static Result, IOError> GetChildrenSync( + nsIFile* aFile, bool aIgnoreAbsent); + + /** + * Set the permissions of the given file. + * + * Windows does not make a distinction between user, group, and other + * permissions like UNICES do. If a permission flag is set for any of user, + * group, or other has a permission, then all users will have that + * permission. + * + * @param aFile The location of the file. + * @param aPermissions The permissions to set, as a UNIX file mode. + * + * @return |Ok| if the permissions were successfully set, or an error. + */ + static Result 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 ExistsSync(nsIFile* aFile); + + /** + * Create a file or directory with a unique path. + * + * @param aFile The location of the file or directory (including prefix) + * @param aFileType One of |nsIFile::NORMAL_FILE_TYPE| or + * |nsIFile::DIRECTORY_TYPE|. + * @param aperms The permissions to create the file or directory with. + * + * @return A unique path. + */ + static Result CreateUniqueSync( + nsIFile* aFile, const uint32_t aFileType, const uint32_t aPermissions); + + /** + * Compute the hash of a file. + * + * @param aFile The file to hash. + * @param aAlgorithm The hashing algorithm to use. + * + * @return The hash of the file, as a hex digest. + */ + static Result ComputeHexDigestSync( + nsIFile* aFile, const HashAlgorithm aAlgorithm); + +#if defined(XP_WIN) + /** + * Return the Windows-specific attributes of the file. + * + * @param aFile The location of the file. + * + * @return The Windows-specific attributes of the file. + */ + static Result GetWindowsAttributesSync(nsIFile* aFile); + + /** + * Set the Windows-specific attributes of the file. + * + * @param aFile The location of the file. + * @param aAttrs The attributes to set on the file. + * + * @return |Ok| if the attributes were successfully set, or an error. + */ + static Result SetWindowsAttributesSync( + nsIFile* aFile, const uint32_t aSetAttrs, const uint32_t aClearAttrs); +#elif defined(XP_MACOSX) + static Result HasMacXAttrSync(nsIFile* aFile, + const nsCString& aAttr); + static Result, IOError> GetMacXAttrSync( + nsIFile* aFile, const nsCString& aAttr); + static Result SetMacXAttrSync(nsIFile* aFile, + const nsCString& aAttr, + const nsTArray& aValue); + static Result DelMacXAttrSync(nsIFile* aFile, + const nsCString& aAttr); +#endif + + static void GetShutdownClient(GlobalObject& aGlobal, + JS::MutableHandle aClient, + ErrorResult& aRv, const ShutdownPhase aPhase); + + enum class EventQueueStatus { + Uninitialized, + Initialized, + Shutdown, + }; + + enum class ShutdownBlockerStatus { + Uninitialized, + Initialized, + Failed, + }; + + /** + * Internal IOUtils state. + */ + class State { + public: + StaticAutoPtr mEventQueue; + EventQueueStatus mQueueStatus = EventQueueStatus::Uninitialized; + ShutdownBlockerStatus mBlockerStatus = ShutdownBlockerStatus::Uninitialized; + + /** + * Set up shutdown hooks to free our internals at shutdown. + * + * NB: Must be called on main thread. + */ + void SetShutdownHooks(); + }; + + using StateMutex = StaticDataMutex; + + /** + * Lock the state mutex and return a handle. If shutdown has not yet + * finished, the internals will be constructed if necessary. + * + * @returns A handle to the internal state, which can be used to retrieve the + * event queue. + * If |Some| is returned, |mEventQueue| is guaranteed to be + * initialized. If shutdown has finished, |Nothing| is returned. + */ + static Maybe GetState(); + + static StateMutex sState; +}; + +/** + * The IOUtils event queue. + */ +class IOUtils::EventQueue final { + friend void IOUtils::State::SetShutdownHooks(); + + public: + EventQueue(); + + EventQueue(const EventQueue&) = delete; + EventQueue(EventQueue&&) = delete; + EventQueue& operator=(const EventQueue&) = delete; + EventQueue& operator=(EventQueue&&) = delete; + + /** + * Dispatch a task on the event queue. + * + * NB: If using this directly from |IOUtils| instead of + * |IOUtils::DispatchAndResolve| *and* the calling thread is a worker, you + * *must* take care to keep the worker thread alive until the |IOPromise| + * resolves or rejects. See the implementation of + * |IOUtils::DispatchAndResolve| or |IOUtils::GetWindowsAttributes| for an + * example. + * + * @param aFunc The task to dispatch on the event queue. + * + * @return A promise that resolves to the task's return value or rejects with + * an error. + */ + template + RefPtr> Dispatch(Fn aFunc); + + Result, nsresult> + GetShutdownBarrier(const ShutdownPhase aPhase); + Result, nsresult> GetShutdownClient( + const ShutdownPhase aPhase); + + private: + nsresult SetShutdownHooks(); + + nsCOMPtr mBackgroundEventTarget; + IOUtils::PhaseArray> mBarriers; +}; + +/** + * An error class used with the |Result| type returned by most private |IOUtils| + * methods. + */ +class IOUtils::IOError { + public: + MOZ_IMPLICIT IOError(nsresult aCode) : mCode(aCode), mMessage(Nothing()) {} + + /** + * Replaces the message associated with this error. + */ + template + 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& Message() const { return mMessage; } + + private: + nsresult mCode; + Maybe mMessage; +}; + +/** + * This is an easier to work with representation of a |mozilla::dom::FileInfo| + * for private use in the IOUtils implementation. + * + * Because web IDL dictionaries are not easily copy/moveable, this class is + * used instead, until converted to the proper |mozilla::dom::FileInfo| before + * returning any results to JavaScript. + */ +struct IOUtils::InternalFileInfo { + nsString mPath; + FileType mType = FileType::Other; + uint64_t mSize = 0; + Maybe mCreationTime; // In ms since epoch. + PRTime mLastAccessed = 0; // In ms since epoch. + PRTime mLastModified = 0; // In ms since epoch. + uint32_t mPermissions = 0; +}; + +/** + * This is an easier to work with representation of a + * |mozilla::dom::WriteOptions| for private use in the |IOUtils| + * implementation. + * + * Because web IDL dictionaries are not easily copy/moveable, this class is + * used instead. + */ +struct IOUtils::InternalWriteOpts { + RefPtr mBackupFile; + RefPtr mTmpFile; + WriteMode mMode; + bool mFlush = false; + bool mCompress = false; + + static Result 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 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, IOError> Compress( + Span aUncompressed); + + /** + * Checks |aFileContents| for the correct file header, and returns the + * decompressed content. + */ + static Result Decompress( + Span aFileContents, IOUtils::BufferKind); +}; + +class IOUtilsShutdownBlocker : public nsIAsyncShutdownBlocker, + public nsIAsyncShutdownCompletionCallback { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIASYNCSHUTDOWNBLOCKER + NS_DECL_NSIASYNCSHUTDOWNCOMPLETIONCALLBACK + + explicit IOUtilsShutdownBlocker(const IOUtils::ShutdownPhase aPhase) + : mPhase(aPhase) {} + + private: + virtual ~IOUtilsShutdownBlocker() = default; + + /** + * Called on the main thread after the event queue has been flushed. + */ + void OnFlush(); + + static constexpr IOUtils::PhaseArray PHASE_NAMES{ + u"profile-before-change", + u"profile-before-change-telemetry", + u"xpcom-will-shutdown", + }; + + // The last shutdown phase before we should shut down the event loop. + static constexpr auto LAST_IO_PHASE = IOUtils::ShutdownPhase::SendTelemetry; + + IOUtils::ShutdownPhase mPhase; + nsCOMPtr mParentClient; +}; + +/** + * A buffer that is allocated inside one of JS heaps so that it can be converted + * to a JSString or Uint8Array object with at most one copy in the worst case. + */ +class IOUtils::JsBuffer final { + public: + /** + * Create a new buffer of the given kind with the requested capacity. + * + * @param aBufferKind The kind of buffer to create (either a string or an + * array). + * @param aCapacity The capacity of the buffer. + * + * @return Either a successfully created buffer or an error if it could not be + * allocated. + */ + static Result 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 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 BeginReading() const { + MOZ_RELEASE_ASSERT(mBuffer.get() || mLength == 0); + return Span(mBuffer.get(), mLength); + } + + /** + * Consume the JsBuffer and convert it into a JSString. + * + * NOTE: This method asserts the buffer was allocated as a string buffer. + * + * @param aBuffer The buffer to convert to a string. After this call, the + * buffer will be invaldated and |IntoString| cannot be called + * again. + * + * @returns A JSString with the contents of |aBuffer|. + */ + static JSString* IntoString(JSContext* aCx, JsBuffer aBuffer); + + /** + * Consume the JsBuffer and convert it into a Uint8Array. + * + * NOTE: This method asserts the buffer was allocated as an array buffer. + * + * @param aBuffer The buffer to convert to an array. After this call, the + * buffer will be invalidated and |IntoUint8Array| cannot be + * called again. + * + * @returns A JSBuffer + */ + static JSObject* IntoUint8Array(JSContext* aCx, JsBuffer aBuffer); + + friend bool ToJSValue(JSContext* aCx, JsBuffer&& aBuffer, + JS::MutableHandle aValue); + + private: + IOUtils::BufferKind mBufferKind; + size_t mCapacity; + size_t mLength; + JS::UniqueChars mBuffer; + + JsBuffer(BufferKind aBufferKind, size_t aCapacity); +}; + +class SyncReadFile : public nsISupports, public nsWrapperCache { + public: + SyncReadFile(nsISupports* aParent, RefPtr&& aStream, + int64_t aSize); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(SyncReadFile) + + nsISupports* GetParentObject() const { return mParent; } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + int64_t Size() const { return mSize; } + void ReadBytesInto(const Uint8Array&, const int64_t, ErrorResult& aRv); + void Close(); + + private: + virtual ~SyncReadFile(); + + nsCOMPtr mParent; + RefPtr mStream; + int64_t mSize = 0; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/system/NetworkGeolocationProvider.jsm b/dom/system/NetworkGeolocationProvider.jsm new file mode 100644 index 0000000000..b20765227a --- /dev/null +++ b/dom/system/NetworkGeolocationProvider.jsm @@ -0,0 +1,511 @@ +/* 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.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + clearTimeout: "resource://gre/modules/Timer.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + LocationHelper: "resource://gre/modules/LocationHelper.jsm", +}); + +// GeolocationPositionError has no interface object, so we can't use that here. +const POSITION_UNAVAILABLE = 2; + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "gLoggingEnabled", + "geo.provider.network.logging.enabled", + false +); + +function LOG(aMsg) { + if (lazy.gLoggingEnabled) { + dump("*** WIFI GEO: " + aMsg + "\n"); + } +} + +function CachedRequest(loc, cellInfo, wifiList) { + this.location = loc; + + let wifis = new Set(); + if (wifiList) { + for (let i = 0; i < wifiList.length; i++) { + wifis.add(wifiList[i].macAddress); + } + } + + // Use only these values for equality + // (the JSON will contain additional values in future) + function makeCellKey(cell) { + return ( + "" + + cell.radio + + ":" + + cell.mobileCountryCode + + ":" + + cell.mobileNetworkCode + + ":" + + cell.locationAreaCode + + ":" + + cell.cellId + ); + } + + let cells = new Set(); + if (cellInfo) { + for (let i = 0; i < cellInfo.length; i++) { + cells.add(makeCellKey(cellInfo[i])); + } + } + + this.hasCells = () => cells.size > 0; + + this.hasWifis = () => wifis.size > 0; + + // if fields match + this.isCellEqual = function(cellInfo) { + if (!this.hasCells()) { + return false; + } + + let len1 = cells.size; + let len2 = cellInfo.length; + + if (len1 != len2) { + LOG("cells not equal len"); + return false; + } + + for (let i = 0; i < len2; i++) { + if (!cells.has(makeCellKey(cellInfo[i]))) { + return false; + } + } + return true; + }; + + // if 50% of the SSIDS match + this.isWifiApproxEqual = function(wifiList) { + if (!this.hasWifis()) { + return false; + } + + // if either list is a 50% subset of the other, they are equal + let common = 0; + for (let i = 0; i < wifiList.length; i++) { + if (wifis.has(wifiList[i].macAddress)) { + common++; + } + } + let kPercentMatch = 0.5; + return common >= Math.max(wifis.size, wifiList.length) * kPercentMatch; + }; + + this.isGeoip = function() { + return !this.hasCells() && !this.hasWifis(); + }; + + this.isCellAndWifi = function() { + return this.hasCells() && this.hasWifis(); + }; + + this.isCellOnly = function() { + return this.hasCells() && !this.hasWifis(); + }; + + this.isWifiOnly = function() { + return this.hasWifis() && !this.hasCells(); + }; +} + +var gCachedRequest = null; +var gDebugCacheReasoning = ""; // for logging the caching logic + +// This function serves two purposes: +// 1) do we have a cached request +// 2) is the cached request better than what newCell and newWifiList will obtain +// If the cached request exists, and we know it to have greater accuracy +// by the nature of its origin (wifi/cell/geoip), use its cached location. +// +// If there is more source info than the cached request had, return false +// In other cases, MLS is known to produce better/worse accuracy based on the +// inputs, so base the decision on that. +function isCachedRequestMoreAccurateThanServerRequest(newCell, newWifiList) { + gDebugCacheReasoning = ""; + let isNetworkRequestCacheEnabled = true; + try { + // Mochitest needs this pref to simulate request failure + isNetworkRequestCacheEnabled = Services.prefs.getBoolPref( + "geo.provider.network.debug.requestCache.enabled" + ); + if (!isNetworkRequestCacheEnabled) { + gCachedRequest = null; + } + } catch (e) {} + + if (!gCachedRequest || !isNetworkRequestCacheEnabled) { + gDebugCacheReasoning = "No cached data"; + return false; + } + + if (!newCell && !newWifiList) { + gDebugCacheReasoning = "New req. is GeoIP."; + return true; + } + + if ( + newCell && + newWifiList && + (gCachedRequest.isCellOnly() || gCachedRequest.isWifiOnly()) + ) { + gDebugCacheReasoning = "New req. is cell+wifi, cache only cell or wifi."; + return false; + } + + if (newCell && gCachedRequest.isWifiOnly()) { + // In order to know if a cell-only request should trump a wifi-only request + // need to know if wifi is low accuracy. >5km would be VERY low accuracy, + // it is worth trying the cell + var isHighAccuracyWifi = gCachedRequest.location.coords.accuracy < 5000; + gDebugCacheReasoning = + "Req. is cell, cache is wifi, isHigh:" + isHighAccuracyWifi; + return isHighAccuracyWifi; + } + + let hasEqualCells = false; + if (newCell) { + hasEqualCells = gCachedRequest.isCellEqual(newCell); + } + + let hasEqualWifis = false; + if (newWifiList) { + hasEqualWifis = gCachedRequest.isWifiApproxEqual(newWifiList); + } + + gDebugCacheReasoning = + "EqualCells:" + hasEqualCells + " EqualWifis:" + hasEqualWifis; + + if (gCachedRequest.isCellOnly()) { + gDebugCacheReasoning += ", Cell only."; + if (hasEqualCells) { + return true; + } + } else if (gCachedRequest.isWifiOnly() && hasEqualWifis) { + gDebugCacheReasoning += ", Wifi only."; + return true; + } else if (gCachedRequest.isCellAndWifi()) { + gDebugCacheReasoning += ", Cache has Cell+Wifi."; + if ( + (hasEqualCells && hasEqualWifis) || + (!newWifiList && hasEqualCells) || + (!newCell && hasEqualWifis) + ) { + return true; + } + } + + return false; +} + +function NetworkGeoCoordsObject(lat, lon, acc) { + this.latitude = lat; + this.longitude = lon; + this.accuracy = acc; + + // Neither GLS nor MLS return the following properties, so set them to NaN + // here. nsGeoPositionCoords will convert NaNs to null for optional properties + // of the JavaScript Coordinates object. + this.altitude = NaN; + this.altitudeAccuracy = NaN; + this.heading = NaN; + this.speed = NaN; +} + +NetworkGeoCoordsObject.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIDOMGeoPositionCoords"]), +}; + +function NetworkGeoPositionObject(lat, lng, acc) { + this.coords = new NetworkGeoCoordsObject(lat, lng, acc); + this.address = null; + this.timestamp = Date.now(); +} + +NetworkGeoPositionObject.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIDOMGeoPosition"]), +}; + +function NetworkGeolocationProvider() { + /* + The _wifiMonitorTimeout controls how long we wait on receiving an update + from the Wifi subsystem. If this timer fires, we believe the Wifi scan has + had a problem and we no longer can use Wifi to position the user this time + around (we will continue to be hopeful that Wifi will recover). + + This timeout value is also used when Wifi scanning is disabled (see + isWifiScanningEnabled). In this case, we use this timer to collect cell/ip + data and xhr it to the location server. + */ + XPCOMUtils.defineLazyPreferenceGetter( + this, + "_wifiMonitorTimeout", + "geo.provider.network.timeToWaitBeforeSending", + 5000 + ); + + XPCOMUtils.defineLazyPreferenceGetter( + this, + "_wifiScanningEnabled", + "geo.provider.network.scan", + true + ); + + this.wifiService = null; + this.timer = null; + this.started = false; +} + +NetworkGeolocationProvider.prototype = { + classID: Components.ID("{77DA64D3-7458-4920-9491-86CC9914F904}"), + 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) { + // Mochitest wants to check this value + if (Services.prefs.getBoolPref("geo.provider.testing")) { + Services.obs.notifyObservers( + null, + "testing-geolocation-high-accuracy", + enable + ); + } + }, + + onChange(accessPoints) { + // we got some wifi data, rearm the timer. + this.resetTimer(); + + let wifiData = null; + if (accessPoints) { + wifiData = lazy.LocationHelper.formatWifiAccessPoints(accessPoints); + } + this.sendLocationRequest(wifiData); + }, + + onError(code) { + LOG("wifi error: " + code); + this.sendLocationRequest(null); + }, + + onStatus(err, statusMessage) { + if (!this.listener) { + return; + } + LOG("onStatus called." + statusMessage); + + if (statusMessage && this.listener.notifyStatus) { + this.listener.notifyStatus(statusMessage); + } + + if (err && this.listener.notifyError) { + this.listener.notifyError(POSITION_UNAVAILABLE, statusMessage); + } + }, + + notify(timer) { + this.onStatus(false, "wifi-timeout"); + this.sendLocationRequest(null); + }, + + /** + * After wifi (and possible cell tower) data has been gathered, this method is + * invoked to perform the request to network geolocation provider. + * The result of each request is sent to all registered listener (@see watch) + * by invoking its respective `update`, `notifyError` or `notifyStatus` + * callbacks. + * `update` is called upon a successful request with its response data; this will be a `NetworkGeoPositionObject` instance. + * `notifyError` is called whenever the request gets an error from the local + * network subsystem, the server or simply times out. + * `notifyStatus` is called for each status change of the request that may be + * of interest to the consumer of this class. Currently the following status + * changes are reported: 'xhr-start', 'xhr-timeout', 'xhr-error' and + * 'xhr-empty'. + * + * @param {Array} wifiData Optional set of publicly available wifi networks + * in the following structure: + * + * [ + * { macAddress: , signalStrength: }, + * { macAddress: , signalStrength: } + * ] + * + */ + async sendLocationRequest(wifiData) { + let data = { cellTowers: undefined, wifiAccessPoints: undefined }; + if (wifiData && wifiData.length >= 2) { + data.wifiAccessPoints = wifiData; + } + + let useCached = isCachedRequestMoreAccurateThanServerRequest( + data.cellTowers, + data.wifiAccessPoints + ); + + LOG("Use request cache:" + useCached + " reason:" + gDebugCacheReasoning); + + if (useCached) { + gCachedRequest.location.timestamp = Date.now(); + if (this.listener) { + this.listener.update(gCachedRequest.location); + } + return; + } + + // From here on, do a network geolocation request // + let url = Services.urlFormatter.formatURLPref("geo.provider.network.url"); + LOG("Sending request"); + + let result; + try { + result = await this.makeRequest(url, wifiData); + LOG( + `geo provider reported: ${result.location.lng}:${result.location.lat}` + ); + let newLocation = new NetworkGeoPositionObject( + result.location.lat, + result.location.lng, + result.accuracy + ); + + if (this.listener) { + this.listener.update(newLocation); + } + + gCachedRequest = new CachedRequest( + newLocation, + data.cellTowers, + data.wifiAccessPoints + ); + } catch (err) { + LOG("Location request hit error: " + err.name); + console.error(err); + if (err.name == "AbortError") { + this.onStatus(true, "xhr-timeout"); + } else { + this.onStatus(true, "xhr-error"); + } + } + }, + + async makeRequest(url, wifiData) { + this.onStatus(false, "xhr-start"); + + let fetchController = new AbortController(); + let fetchOpts = { + method: "POST", + headers: { "Content-Type": "application/json; charset=UTF-8" }, + credentials: "omit", + signal: fetchController.signal, + }; + + if (wifiData) { + fetchOpts.body = JSON.stringify({ wifiAccessPoints: wifiData }); + } + + let timeoutId = lazy.setTimeout( + () => fetchController.abort(), + Services.prefs.getIntPref("geo.provider.network.timeout") + ); + + let req = await fetch(url, fetchOpts); + lazy.clearTimeout(timeoutId); + let result = req.json(); + return result; + }, +}; + +var EXPORTED_SYMBOLS = ["NetworkGeolocationProvider"]; diff --git a/dom/system/OSFileConstants.cpp b/dom/system/OSFileConstants.cpp new file mode 100644 index 0000000000..fa7e9532af --- /dev/null +++ b/dom/system/OSFileConstants.cpp @@ -0,0 +1,983 @@ +/* -*- 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 +# include +# 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 +# endif // !defined(ANDROID) +#endif // defined(XP_UNIX) + +#if defined(XP_LINUX) +# include +#endif // defined(XP_LINUX) + +#if defined(XP_MACOSX) +# include "copyfile.h" +#endif // defined(XP_MACOSX) + +#if defined(XP_WIN) +# include +# include + +# 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 "js/PropertyAndElement.h" // JS_DefineObject, JS_DefineProperty, JS_GetProperty, JS_SetProperty +#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 "mozJSModuleLoader.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 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 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(new Paths); + + // Initialize paths->libDir + nsCOMPtr file; + nsresult rv = + NS_GetSpecialDirectory(NS_XPCOM_LIBRARY_FILE, getter_AddRefs(file)); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr 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 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), + INT_CONSTANT(ENOSYS), +#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), + +#if defined(XP_LINUX) + // prctl options + INT_CONSTANT(PR_CAPBSET_READ), +#endif + +// 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 aObject, + const char* aProperty) { + JS::Rooted 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 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 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 aGlobal) { + if (!mInitialized) { + JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr, + JSMSG_CANT_OPEN, "OSFileConstants", + "initialization has failed"); + return false; + } + + JS::Rooted objOS(aCx); + if (!(objOS = GetOrCreateObjectProperty(aCx, aGlobal, "OS"))) { + return false; + } + JS::Rooted objConstants(aCx); + if (!(objConstants = GetOrCreateObjectProperty(aCx, objOS, "Constants"))) { + return false; + } + + // Build OS.Constants.libc + + JS::Rooted 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 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 objSys(aCx); + if (!(objSys = GetOrCreateObjectProperty(aCx, objConstants, "Sys"))) { + return false; + } + + nsCOMPtr runtime = + do_GetService(XULRUNTIME_SERVICE_CONTRACTID); + if (runtime) { + nsAutoCString os; + DebugOnly rv = runtime->GetOS(os); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + JSString* strVersion = JS_NewStringCopyZ(aCx, os.get()); + if (!strVersion) { + return false; + } + + JS::Rooted valVersion(aCx, JS::StringValue(strVersion)); + if (!JS_SetProperty(aCx, objSys, "Name", valVersion)) { + return false; + } + } + +#if defined(DEBUG) + JS::Rooted valDebug(aCx, JS::TrueValue()); + if (!JS_SetProperty(aCx, objSys, "DEBUG", valDebug)) { + return false; + } +#endif + +#if defined(HAVE_64BIT_BUILD) + JS::Rooted valBits(aCx, JS::Int32Value(64)); +#else + JS::Rooted 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 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; + } + + return true; +} + +NS_IMPL_ISUPPORTS(OSFileConstantsService, nsIOSFileConstantsService, + nsIObserver) + +/* static */ +already_AddRefed OSFileConstantsService::GetOrCreate() { + if (!gInstance) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr service = new OSFileConstantsService(); + nsresult rv = service->InitOSFileConstants(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + gInstance = std::move(service); + ClearOnShutdown(&gInstance); + } + + RefPtr 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; + } + + mozJSModuleLoader* loader = mozJSModuleLoader::Get(); + JS::Rooted 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 GetOrCreate(); + + bool DefineOSFileConstants(JSContext* aCx, JS::Handle aGlobal); + + private: + nsresult InitOSFileConstants(); + + OSFileConstantsService(); + ~OSFileConstantsService(); + + bool mInitialized; + + struct Paths; + UniquePtr 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..eb11b46269 --- /dev/null +++ b/dom/system/PathUtils.cpp @@ -0,0 +1,657 @@ +/* -*- 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 "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsIFile.h" +#include "nsIGlobalObject.h" +#include "nsLocalFile.h" +#include "nsNetUtil.h" +#include "nsString.h" +#include "xpcpublic.h" + +namespace mozilla::dom { + +static constexpr auto ERROR_EMPTY_PATH = + "PathUtils does not support empty paths"_ns; +static constexpr auto ERROR_INITIALIZE_PATH = "Could not initialize path"_ns; +static constexpr auto ERROR_GET_PARENT = "Could not get parent path"_ns; +static constexpr auto ERROR_JOIN = "Could not append to path"_ns; + +static constexpr auto COLON = ": "_ns; + +static void ThrowError(ErrorResult& aErr, const nsresult aResult, + const nsCString& aMessage) { + nsAutoCStringN<32> errName; + GetErrorName(aResult, errName); + + nsAutoCStringN<256> formattedMsg; + formattedMsg.Append(aMessage); + formattedMsg.Append(COLON); + formattedMsg.Append(errName); + + switch (aResult) { + case NS_ERROR_FILE_UNRECOGNIZED_PATH: + aErr.ThrowOperationError(formattedMsg); + break; + + case NS_ERROR_FILE_ACCESS_DENIED: + aErr.ThrowInvalidAccessError(formattedMsg); + break; + + case NS_ERROR_FAILURE: + default: + aErr.ThrowUnknownError(formattedMsg); + break; + } +} + +static bool DoWindowsPathCheck() { +#ifdef XP_WIN +# ifdef DEBUG + return true; +# else // DEBUG + return xpc::IsInAutomation(); +# endif // DEBUG +#else // XP_WIN + return false; +#endif // XP_WIN +} + +/* static */ +nsresult PathUtils::InitFileWithPath(nsIFile* aFile, const nsAString& aPath) { + if (DoWindowsPathCheck()) { + MOZ_RELEASE_ASSERT(!aPath.Contains(u'/'), + "Windows paths cannot include forward slashes"); + } + + MOZ_ASSERT(aFile); + return aFile->InitWithPath(aPath); +} + +StaticDataMutex> 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 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 path = new nsLocalFile(); + if (nsresult rv = InitFileWithPath(path, aPath); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return; + } + + if (nsresult rv = GetLeafNamePreservingRoot(path, aResult); NS_FAILED(rv)) { + ThrowError(aErr, rv, "Could not get leaf name of path"_ns); + return; + } +} + +void PathUtils::Parent(const GlobalObject&, const nsAString& aPath, + const int32_t aDepth, nsString& aResult, + ErrorResult& aErr) { + if (aPath.IsEmpty()) { + aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH); + return; + } + + nsCOMPtr path = new nsLocalFile(); + if (nsresult rv = InitFileWithPath(path, aPath); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return; + } + + if (aDepth <= 0) { + aErr.ThrowNotSupportedError("A depth of at least 1 is required"); + return; + } + + nsCOMPtr parent; + for (int32_t i = 0; path && i < aDepth; i++) { + if (nsresult rv = path->GetParent(getter_AddRefs(parent)); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_GET_PARENT); + return; + } + path = parent; + } + + if (parent) { + MOZ_ALWAYS_SUCCEEDS(parent->GetPath(aResult)); + } else { + aResult = VoidString(); + } +} + +void PathUtils::Join(const GlobalObject&, const Sequence& aComponents, + nsString& aResult, ErrorResult& aErr) { + nsCOMPtr path = Join(Span(aComponents), aErr); + if (aErr.Failed()) { + return; + } + + MOZ_ALWAYS_SUCCEEDS(path->GetPath(aResult)); +} + +already_AddRefed PathUtils::Join( + const Span& aComponents, ErrorResult& aErr) { + if (aComponents.IsEmpty() || aComponents[0].IsEmpty()) { + aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH); + return nullptr; + } + + nsCOMPtr path = new nsLocalFile(); + if (nsresult rv = InitFileWithPath(path, aComponents[0]); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return nullptr; + } + + const auto components = Span(aComponents).Subspan(1); + for (const auto& component : components) { + if (nsresult rv = path->Append(component); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_JOIN); + return nullptr; + } + } + + return path.forget(); +} + +void PathUtils::JoinRelative(const GlobalObject&, const nsAString& aBasePath, + const nsAString& aRelativePath, nsString& aResult, + ErrorResult& aErr) { + if (aRelativePath.IsEmpty()) { + aResult = aBasePath; + return; + } + + nsCOMPtr path = new nsLocalFile(); + if (nsresult rv = InitFileWithPath(path, aBasePath); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return; + } + + if (nsresult rv = path->AppendRelativePath(aRelativePath); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_JOIN); + return; + } + + MOZ_ALWAYS_SUCCEEDS(path->GetPath(aResult)); +} + +void PathUtils::ToExtendedWindowsPath(const GlobalObject&, + const nsAString& aPath, nsString& aResult, + ErrorResult& aErr) { +#ifndef XP_WIN + aErr.ThrowNotAllowedError("Operation is windows specific"_ns); + return; +#else + if (aPath.IsEmpty()) { + aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH); + return; + } + + const nsAString& str1 = Substring(aPath, 1, 1); + const nsAString& str2 = Substring(aPath, 2, aPath.Length() - 2); + + bool isUNC = aPath.Length() >= 2 && + (aPath.First() == '\\' || aPath.First() == '/') && + (str1.EqualsLiteral("\\") || str1.EqualsLiteral("/")); + + constexpr auto pathPrefix = u"\\\\?\\"_ns; + const nsAString& uncPath = pathPrefix + u"UNC\\"_ns + str2; + const nsAString& normalPath = pathPrefix + aPath; + + nsCOMPtr path = new nsLocalFile(); + if (nsresult rv = InitFileWithPath(path, isUNC ? uncPath : normalPath); + NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return; + } + + MOZ_ALWAYS_SUCCEEDS(path->GetPath(aResult)); +#endif +} + +void PathUtils::Normalize(const GlobalObject&, const nsAString& aPath, + nsString& aResult, ErrorResult& aErr) { + if (aPath.IsEmpty()) { + aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH); + return; + } + + nsCOMPtr path = new nsLocalFile(); + if (nsresult rv = InitFileWithPath(path, aPath); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return; + } + + if (nsresult rv = path->Normalize(); NS_FAILED(rv)) { + ThrowError(aErr, rv, "Could not normalize path"_ns); + return; + } + + MOZ_ALWAYS_SUCCEEDS(path->GetPath(aResult)); +} + +void PathUtils::Split(const GlobalObject&, const nsAString& aPath, + nsTArray& aResult, ErrorResult& aErr) { + if (aPath.IsEmpty()) { + aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH); + return; + } + + nsCOMPtr path = new nsLocalFile(); + if (nsresult rv = InitFileWithPath(path, aPath); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return; + } + + while (path) { + auto* component = aResult.EmplaceBack(fallible); + if (!component) { + aErr.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + nsCOMPtr parent; + if (nsresult rv = path->GetParent(getter_AddRefs(parent)); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_GET_PARENT); + return; + } + + // GetLeafPreservingRoot cannot fail if we pass it a parent path. + MOZ_ALWAYS_SUCCEEDS(GetLeafNamePreservingRoot(path, *component, parent)); + + path = parent; + } + + aResult.Reverse(); +} + +void PathUtils::SplitRelative(const GlobalObject& aGlobal, + const nsAString& aPath, + const SplitRelativeOptions& aOptions, + nsTArray& aResult, ErrorResult& aErr) { + if (aPath.IsEmpty()) { + aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH); + return; + } + + if (DoWindowsPathCheck()) { + MOZ_RELEASE_ASSERT(!aPath.Contains(u'/'), + "Windows paths cannot include forward slashes"); + } + + if (IsAbsolute(aGlobal, aPath)) { + aErr.ThrowNotAllowedError( + "PathUtils.splitRelative requires a relative path"_ns); + return; + } + +#ifdef XP_WIN + constexpr auto SEPARATOR = u'\\'; +#else + constexpr auto SEPARATOR = u'/'; +#endif + + constexpr auto PARENT = u".."_ns; + constexpr auto CURRENT = u"."_ns; + + for (const nsAString& pathComponent : + nsCharSeparatedTokenizerTemplate{aPath, + SEPARATOR} + .ToRange()) { + if (!aOptions.mAllowEmpty && pathComponent.IsEmpty()) { + aErr.ThrowNotAllowedError( + "PathUtils.splitRelative: Empty directory components (\"\") not " + "allowed by options"); + return; + } + + if (!aOptions.mAllowParentDir && pathComponent == PARENT) { + aErr.ThrowNotAllowedError( + "PathUtils.splitRelative: Parent directory components (\"..\") not " + "allowed by options"); + return; + } + + if (!aOptions.mAllowCurrentDir && pathComponent == CURRENT) { + aErr.ThrowNotAllowedError( + "PathUtils.splitRelative: Current directory components (\".\") not " + "allowed by options"); + return; + } + + aResult.AppendElement(pathComponent); + } +} + +void PathUtils::ToFileURI(const GlobalObject&, const nsAString& aPath, + nsCString& aResult, ErrorResult& aErr) { + if (aPath.IsEmpty()) { + aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH); + return; + } + + nsCOMPtr path = new nsLocalFile(); + if (nsresult rv = InitFileWithPath(path, aPath); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return; + } + + nsCOMPtr 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; + } +} + +bool PathUtils::IsAbsolute(const GlobalObject&, const nsAString& aPath) { + nsCOMPtr path = new nsLocalFile(); + nsresult rv = InitFileWithPath(path, aPath); + return NS_SUCCEEDED(rv); +} + +void PathUtils::GetProfileDirSync(const GlobalObject&, nsString& aResult, + ErrorResult& aErr) { + MOZ_ASSERT(NS_IsMainThread()); + + auto guard = sDirCache.Lock(); + DirectoryCache::Ensure(guard.ref()) + .GetDirectorySync(aResult, aErr, DirectoryCache::Directory::Profile); +} +void PathUtils::GetLocalProfileDirSync(const GlobalObject&, nsString& aResult, + ErrorResult& aErr) { + MOZ_ASSERT(NS_IsMainThread()); + + auto guard = sDirCache.Lock(); + DirectoryCache::Ensure(guard.ref()) + .GetDirectorySync(aResult, aErr, DirectoryCache::Directory::LocalProfile); +} +void PathUtils::GetTempDirSync(const GlobalObject&, nsString& aResult, + ErrorResult& aErr) { + MOZ_ASSERT(NS_IsMainThread()); + + auto guard = sDirCache.Lock(); + DirectoryCache::Ensure(guard.ref()) + .GetDirectorySync(aResult, aErr, DirectoryCache::Directory::Temp); +} + +void PathUtils::GetOSTempDirSync(const GlobalObject&, nsString& aResult, + ErrorResult& aErr) { + MOZ_ASSERT(NS_IsMainThread()); + + auto guard = sDirCache.Lock(); + DirectoryCache::Ensure(guard.ref()) + .GetDirectorySync(aResult, aErr, DirectoryCache::Directory::OSTemp); +} + +void PathUtils::GetXulLibraryPathSync(const GlobalObject&, nsString& aResult, + ErrorResult& aErr) { + MOZ_ASSERT(NS_IsMainThread()); + + auto guard = sDirCache.Lock(); + DirectoryCache::Ensure(guard.ref()) + .GetDirectorySync(aResult, aErr, DirectoryCache::Directory::XulLibrary); +} + +already_AddRefed PathUtils::GetProfileDirAsync( + const GlobalObject& aGlobal, ErrorResult& aErr) { + MOZ_ASSERT(!NS_IsMainThread()); + + auto guard = sDirCache.Lock(); + return DirectoryCache::Ensure(guard.ref()) + .GetDirectoryAsync(aGlobal, aErr, DirectoryCache::Directory::Profile); +} + +already_AddRefed PathUtils::GetLocalProfileDirAsync( + const GlobalObject& aGlobal, ErrorResult& aErr) { + MOZ_ASSERT(!NS_IsMainThread()); + + auto guard = sDirCache.Lock(); + return DirectoryCache::Ensure(guard.ref()) + .GetDirectoryAsync(aGlobal, aErr, + DirectoryCache::Directory::LocalProfile); +} + +already_AddRefed PathUtils::GetTempDirAsync( + const GlobalObject& aGlobal, ErrorResult& aErr) { + MOZ_ASSERT(!NS_IsMainThread()); + + auto guard = sDirCache.Lock(); + return DirectoryCache::Ensure(guard.ref()) + .GetDirectoryAsync(aGlobal, aErr, DirectoryCache::Directory::Temp); +} + +already_AddRefed PathUtils::GetOSTempDirAsync( + const GlobalObject& aGlobal, ErrorResult& aErr) { + MOZ_ASSERT(!NS_IsMainThread()); + + auto guard = sDirCache.Lock(); + return DirectoryCache::Ensure(guard.ref()) + .GetDirectoryAsync(aGlobal, aErr, DirectoryCache::Directory::OSTemp); +} + +already_AddRefed PathUtils::GetXulLibraryPathAsync( + const GlobalObject& aGlobal, ErrorResult& aErr) { + MOZ_ASSERT(!NS_IsMainThread()); + + auto guard = sDirCache.Lock(); + return DirectoryCache::Ensure(guard.ref()) + .GetDirectoryAsync(aGlobal, aErr, DirectoryCache::Directory::XulLibrary); +} + +PathUtils::DirectoryCache::DirectoryCache() { + for (auto& dir : mDirectories) { + dir.SetIsVoid(true); + } +} + +PathUtils::DirectoryCache& PathUtils::DirectoryCache::Ensure( + Maybe& aCache) { + if (aCache.isNothing()) { + aCache.emplace(); + + auto clearAtShutdown = []() { + RunOnShutdown([]() { + auto cache = PathUtils::sDirCache.Lock(); + cache->reset(); + }); + }; + + if (NS_IsMainThread()) { + clearAtShutdown(); + } else { + NS_DispatchToMainThread( + NS_NewRunnableFunction(__func__, std::move(clearAtShutdown))); + } + } + + return aCache.ref(); +} + +void PathUtils::DirectoryCache::GetDirectorySync( + nsString& aResult, ErrorResult& aErr, const Directory aRequestedDir) { + MOZ_RELEASE_ASSERT(aRequestedDir < Directory::Count); + + if (nsresult rv = PopulateDirectoriesImpl(aRequestedDir); NS_FAILED(rv)) { + nsAutoCStringN<32> errorName; + GetErrorName(rv, errorName); + + nsAutoCStringN<256> msg; + msg.Append("Could not retrieve directory "_ns); + msg.Append(kDirectoryNames[aRequestedDir]); + msg.Append(COLON); + msg.Append(errorName); + + aErr.ThrowUnknownError(msg); + return; + } + + aResult = mDirectories[aRequestedDir]; +} + +already_AddRefed PathUtils::DirectoryCache::GetDirectoryAsync( + const GlobalObject& aGlobal, ErrorResult& aErr, + const Directory aRequestedDir) { + nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr promise = Promise::Create(global, aErr); + if (aErr.Failed()) { + return nullptr; + } + + if (RefPtr p = + PopulateDirectories(aRequestedDir)) { + p->Then( + GetCurrentSerialEventTarget(), __func__, + [promise, aRequestedDir](const Ok&) { + auto cache = PathUtils::sDirCache.Lock(); + cache.ref()->ResolveWithDirectory(promise, aRequestedDir); + }, + [promise](const nsresult& aRv) { promise->MaybeReject(aRv); }); + } else { + ResolveWithDirectory(promise, aRequestedDir); + } + + return promise.forget(); +} + +void PathUtils::DirectoryCache::ResolveWithDirectory( + Promise* aPromise, const Directory aRequestedDir) { + MOZ_RELEASE_ASSERT(aRequestedDir < Directory::Count); + MOZ_RELEASE_ASSERT(!mDirectories[aRequestedDir].IsVoid()); + aPromise->MaybeResolve(mDirectories[aRequestedDir]); +} + +already_AddRefed +PathUtils::DirectoryCache::PopulateDirectories( + const PathUtils::DirectoryCache::Directory aRequestedDir) { + MOZ_RELEASE_ASSERT(aRequestedDir < Directory::Count); + + // If we have already resolved the requested directory, we can return + // immediately. + // Otherwise, if we have already fired off a request to populate the entry, + // so we can return the corresponding promise immediately. caller will queue + // a Thenable onto that promise to resolve/reject the request. + if (!mDirectories[aRequestedDir].IsVoid()) { + return nullptr; + } + if (!mPromises[aRequestedDir].IsEmpty()) { + return mPromises[aRequestedDir].Ensure(__func__); + } + + RefPtr promise = + mPromises[aRequestedDir].Ensure(__func__); + + if (NS_IsMainThread()) { + nsresult rv = PopulateDirectoriesImpl(aRequestedDir); + ResolvePopulateDirectoriesPromise(rv, aRequestedDir); + } else { + nsCOMPtr runnable = + NS_NewRunnableFunction(__func__, [aRequestedDir]() { + auto cache = PathUtils::sDirCache.Lock(); + nsresult rv = cache.ref()->PopulateDirectoriesImpl(aRequestedDir); + cache.ref()->ResolvePopulateDirectoriesPromise(rv, aRequestedDir); + }); + NS_DispatchToMainThread(runnable.forget()); + } + + return promise.forget(); +} + +void PathUtils::DirectoryCache::ResolvePopulateDirectoriesPromise( + nsresult aRv, const PathUtils::DirectoryCache::Directory aRequestedDir) { + MOZ_RELEASE_ASSERT(aRequestedDir < Directory::Count); + + if (NS_SUCCEEDED(aRv)) { + mPromises[aRequestedDir].Resolve(Ok{}, __func__); + } else { + mPromises[aRequestedDir].Reject(aRv, __func__); + } +} + +nsresult PathUtils::DirectoryCache::PopulateDirectoriesImpl( + const PathUtils::DirectoryCache::Directory aRequestedDir) { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + MOZ_RELEASE_ASSERT(aRequestedDir < Directory::Count); + + if (!mDirectories[aRequestedDir].IsVoid()) { + // In between when this promise was dispatched to the main thread and now, + // the directory cache has had this entry populated (via the + // on-main-thread sync method). + return NS_OK; + } + + nsCOMPtr path; + + MOZ_TRY(NS_GetSpecialDirectory(kDirectoryNames[aRequestedDir], + getter_AddRefs(path))); + MOZ_TRY(path->GetPath(mDirectories[aRequestedDir])); + + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/system/PathUtils.h b/dom/system/PathUtils.h new file mode 100644 index 0000000000..cbfceef269 --- /dev/null +++ b/dom/system/PathUtils.h @@ -0,0 +1,267 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_PathUtils__ +#define mozilla_dom_PathUtils__ + +#include "mozilla/DataMutex.h" +#include "mozilla/EnumeratedArray.h" +#include "mozilla/Maybe.h" +#include "mozilla/MozPromise.h" +#include "mozilla/Mutex.h" +#include "mozilla/Result.h" +#include "mozilla/dom/PathUtilsBinding.h" +#include "mozilla/dom/Promise.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsDirectoryServiceDefs.h" +#include "nsString.h" +#include "nsTArray.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class PathUtils final { + public: + /** + * Initialize the given nsIFile with the given path. + * + * This is equivalent to calling nsIFile::InitWithPath() with the caveat that + * on Windows debug or during Windows CI tests, we will crash if the path + * contains a forward slash. + * + * @param aFile The file to initialize. + * @param aPath The path to initialize the file with. + * + * @return The result of calling nsIFile::InitWithPath. + */ + static nsresult InitFileWithPath(nsIFile* aFile, const nsAString& aPath); + + static void Filename(const GlobalObject&, const nsAString& aPath, + nsString& aResult, ErrorResult& aErr); + + static void Parent(const GlobalObject&, const nsAString& aPath, + const int32_t aDepth, nsString& aResult, + ErrorResult& aErr); + + static void Join(const GlobalObject&, const Sequence& aComponents, + nsString& aResult, ErrorResult& aErr); + + /** + * Join a sequence of path components and return an nsIFile with the resulting + * path. + * + * @param aComponents A sequence of path components. The first component must + * be an absolute path. + * @param aErr The error result, if any. + * + * @return An nsIFile with the resulting path, if there were no errors. + * Otherwise, nullptr is returned. + */ + static already_AddRefed Join(const Span& aComponents, + ErrorResult& aErr); + + static void JoinRelative(const GlobalObject&, const nsAString& aBasePath, + const nsAString& aRelativePath, nsString& aResult, + ErrorResult& aErr); + + static void ToExtendedWindowsPath(const GlobalObject&, const nsAString& aPath, + nsString& aResult, ErrorResult& aErr); + + static void Normalize(const GlobalObject&, const nsAString& aPath, + nsString& aResult, ErrorResult& aErr); + + static void Split(const GlobalObject&, const nsAString& aPath, + nsTArray& aResult, ErrorResult& aErr); + + static void SplitRelative(const GlobalObject& aGlobal, const nsAString& aPath, + const SplitRelativeOptions& aOptions, + nsTArray& aResult, ErrorResult& aErr); + + static void ToFileURI(const GlobalObject&, const nsAString& aPath, + nsCString& aResult, ErrorResult& aErr); + + static bool IsAbsolute(const GlobalObject&, const nsAString& aPath); + + static void GetProfileDirSync(const GlobalObject&, nsString& aResult, + ErrorResult& aErr); + static void GetLocalProfileDirSync(const GlobalObject&, nsString& aResult, + ErrorResult& aErr); + static void GetTempDirSync(const GlobalObject&, nsString& aResult, + ErrorResult& aErr); + static void GetOSTempDirSync(const GlobalObject&, nsString& aResult, + ErrorResult& aErr); + static void GetXulLibraryPathSync(const GlobalObject&, nsString& aResult, + ErrorResult& aErr); + + static already_AddRefed GetProfileDirAsync( + const GlobalObject& aGlobal, ErrorResult& aErr); + static already_AddRefed GetLocalProfileDirAsync( + const GlobalObject& aGlobal, ErrorResult& aErr); + static already_AddRefed GetTempDirAsync(const GlobalObject& aGlobal, + ErrorResult& aErr); + static already_AddRefed GetOSTempDirAsync( + const GlobalObject& aGlobal, ErrorResult& aErr); + static already_AddRefed GetXulLibraryPathAsync( + const GlobalObject& aGlobal, ErrorResult& aErr); + + private: + class DirectoryCache; + friend class DirectoryCache; + + static StaticDataMutex> sDirCache; +}; + +/** + * A cache of commonly used directories + */ +class PathUtils::DirectoryCache final { + public: + /** + * A directory that can be requested via |GetDirectorySync| or + * |GetDirectoryAsync|. + */ + enum class Directory { + /** + * The user's profile directory. + */ + Profile, + /** + * The user's local profile directory. + */ + LocalProfile, + /** + * The temporary directory for the process. + */ + Temp, + /** + * The OS temporary directory. + */ + OSTemp, + /** + * The libxul path. + */ + XulLibrary, + /** + * The number of Directory entries. + */ + Count, + }; + + DirectoryCache(); + DirectoryCache(const DirectoryCache&) = delete; + DirectoryCache(DirectoryCache&&) = delete; + DirectoryCache& operator=(const DirectoryCache&) = delete; + DirectoryCache& operator=(DirectoryCache&&) = delete; + + /** + * Ensure the cache is instantiated and schedule its destructor to run at + * shutdown. + * + * If the cache is already instantiated, this is a no-op. + * + * @param aCache The cache to ensure is instantiated. + */ + static DirectoryCache& Ensure(Maybe& aCache); + + void GetDirectorySync(nsString& aResult, ErrorResult& aErr, + const Directory aRequestedDir); + + /** + * Request the path of a specific directory. + * + * If the directory has not been requested before, this may require a trip to + * the main thread to retrieve its path. + * + * @param aGlobalObject The JavaScript global. + * @param aErr The error result. + * @param aRequestedDir The directory for which the path is to be retrieved. + * + * @return A promise that resolves to the path of the requested directory. + */ + already_AddRefed GetDirectoryAsync(const GlobalObject& aGlobalObject, + ErrorResult& aErr, + const Directory aRequestedDir); + + private: + using PopulateDirectoriesPromise = MozPromise; + + /** + * 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 PopulateDirectories( + const Directory aRequestedDir); + + /** + * Initialize the requested directory cache entry. + * + * If |Directory::Temp| is requested, all cache entries will be populated. + * Otherwise, only the profile and local profile cache entries will be + * populated. The profile and local profile cache entries have no additional + * overhead for populating them, but the temp directory requires creating a + * directory on the main thread if it has not already happened. + * + * Must be called on the main thread. + * + * @param aRequestedDir The requested directory. + * + * @return The result of initializing directories. + */ + nsresult PopulateDirectoriesImpl(const Directory aRequestedDir); + + /** + * Resolve the internal PopulateDirectoriesPromise corresponding to + * |aRequestedDir| with the given result. + * + * This will allow all pending queries for the requested directory to resolve + * or be rejected. + * + * @param aRv The return value from PopulateDirectoriesImpl. + * @param aRequestedDir The requested directory cache entry. This is used to + * determine which internal MozPromiseHolder we are + * resolving. + */ + void ResolvePopulateDirectoriesPromise(nsresult aRv, + const Directory aRequestedDir); + + /** + * Resolve the given JS promise with the path of the requested directory + * + * Can only be called once the cache entry for the requested directory is + * populated. + * + * @param aPromise The JS promise to resolve. + * @param aRequestedDir The requested directory cache entry. + */ + void ResolveWithDirectory(Promise* aPromise, const Directory aRequestedDir); + + template + using DirectoryArray = EnumeratedArray; + + DirectoryArray mDirectories; + DirectoryArray> mPromises; + + static constexpr DirectoryArray kDirectoryNames{ + NS_APP_USER_PROFILE_50_DIR, NS_APP_USER_PROFILE_LOCAL_50_DIR, + NS_APP_CONTENT_PROCESS_TEMP_DIR, NS_OS_TEMP_DIR, + NS_XPCOM_LIBRARY_FILE, + }; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/system/android/AndroidLocationProvider.cpp b/dom/system/android/AndroidLocationProvider.cpp new file mode 100644 index 0000000000..8861c30298 --- /dev/null +++ b/dom/system/android/AndroidLocationProvider.cpp @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "Geolocation.h" +#include "GeolocationPosition.h" +#include "AndroidLocationProvider.h" +#include "mozilla/java/GeckoAppShellWrappers.h" + +using namespace mozilla; + +extern nsIGeolocationUpdate* gLocationCallback; + +NS_IMPL_ISUPPORTS(AndroidLocationProvider, nsIGeolocationProvider) + +AndroidLocationProvider::AndroidLocationProvider() {} + +AndroidLocationProvider::~AndroidLocationProvider() { + NS_IF_RELEASE(gLocationCallback); +} + +NS_IMETHODIMP +AndroidLocationProvider::Startup() { + if (java::GeckoAppShell::EnableLocationUpdates(true)) { + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +AndroidLocationProvider::Watch(nsIGeolocationUpdate* aCallback) { + NS_IF_RELEASE(gLocationCallback); + gLocationCallback = aCallback; + NS_IF_ADDREF(gLocationCallback); + return NS_OK; +} + +NS_IMETHODIMP +AndroidLocationProvider::Shutdown() { + if (java::GeckoAppShell::EnableLocationUpdates(false)) { + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +AndroidLocationProvider::SetHighAccuracy(bool enable) { + java::GeckoAppShell::EnableLocationHighAccuracy(enable); + return NS_OK; +} diff --git a/dom/system/android/AndroidLocationProvider.h b/dom/system/android/AndroidLocationProvider.h new file mode 100644 index 0000000000..e0d38f6c8f --- /dev/null +++ b/dom/system/android/AndroidLocationProvider.h @@ -0,0 +1,23 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef AndroidLocationProvider_h +#define AndroidLocationProvider_h + +#include "nsIGeolocationProvider.h" + +class AndroidLocationProvider final : public nsIGeolocationProvider { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONPROVIDER + + AndroidLocationProvider(); + + private: + ~AndroidLocationProvider(); +}; + +#endif /* AndroidLocationProvider_h */ diff --git a/dom/system/android/moz.build b/dom/system/android/moz.build new file mode 100644 index 0000000000..04dffba024 --- /dev/null +++ b/dom/system/android/moz.build @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +SOURCES += [ + "AndroidLocationProvider.cpp", + "nsHapticFeedback.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" +LOCAL_INCLUDES += [ + "/dom/geolocation", +] diff --git a/dom/system/android/nsHapticFeedback.cpp b/dom/system/android/nsHapticFeedback.cpp new file mode 100644 index 0000000000..87c77d8334 --- /dev/null +++ b/dom/system/android/nsHapticFeedback.cpp @@ -0,0 +1,18 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsHapticFeedback.h" +#include "mozilla/java/GeckoAppShellWrappers.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(nsHapticFeedback, nsIHapticFeedback) + +NS_IMETHODIMP +nsHapticFeedback::PerformSimpleAction(int32_t aType) { + java::GeckoAppShell::PerformHapticFeedback(aType == LongPress); + return NS_OK; +} diff --git a/dom/system/android/nsHapticFeedback.h b/dom/system/android/nsHapticFeedback.h new file mode 100644 index 0000000000..e55062058b --- /dev/null +++ b/dom/system/android/nsHapticFeedback.h @@ -0,0 +1,16 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIHapticFeedback.h" + +class nsHapticFeedback final : public nsIHapticFeedback { + private: + ~nsHapticFeedback() {} + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHAPTICFEEDBACK +}; diff --git a/dom/system/components.conf b/dom/system/components.conf new file mode 100644 index 0000000000..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/GeoclueLocationProvider.cpp b/dom/system/linux/GeoclueLocationProvider.cpp new file mode 100644 index 0000000000..7e5006141f --- /dev/null +++ b/dom/system/linux/GeoclueLocationProvider.cpp @@ -0,0 +1,1060 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Author: Maciej S. Szmigiero + */ + +#include "GeoclueLocationProvider.h" + +#include +#include +#include "mozilla/FloatingPoint.h" +#include "mozilla/GRefPtr.h" +#include "mozilla/GUniquePtr.h" +#include "mozilla/Logging.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_geo.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/WeakPtr.h" +#include "mozilla/XREAppData.h" +#include "mozilla/dom/GeolocationPosition.h" +#include "mozilla/dom/GeolocationPositionErrorBinding.h" +#include "MLSFallback.h" +#include "nsAppRunner.h" +#include "nsCOMPtr.h" +#include "nsIDOMGeoPosition.h" +#include "nsINamed.h" +#include "nsITimer.h" +#include "nsStringFwd.h" +#include "prtime.h" + +namespace mozilla::dom { + +static LazyLogModule gGCLocationLog("GeoclueLocation"); + +#define GCL_LOG(level, ...) \ + MOZ_LOG(gGCLocationLog, mozilla::LogLevel::level, (__VA_ARGS__)) + +static const char* const kGeoclueBusName = "org.freedesktop.GeoClue2"; +static const char* const kGCManagerPath = "/org/freedesktop/GeoClue2/Manager"; +static const char* const kGCManagerInterface = + "org.freedesktop.GeoClue2.Manager"; +static const char* const kGCClientInterface = "org.freedesktop.GeoClue2.Client"; +static const char* const kGCLocationInterface = + "org.freedesktop.GeoClue2.Location"; +static const char* const kDBPropertySetMethod = + "org.freedesktop.DBus.Properties.Set"; + +/* + * Minimum altitude reported as valid (in meters), + * https://en.wikipedia.org/wiki/List_of_places_on_land_with_elevations_below_sea_level + * says that lowest land in the world is at -430 m, so let's use -500 m here. + */ +static const double kGCMinAlt = -500; + +/* + * Matches "enum GClueAccuracyLevel" values, see: + * https://www.freedesktop.org/software/geoclue/docs/geoclue-gclue-enums.html#GClueAccuracyLevel + */ +enum class GCAccuracyLevel { + None = 0, + Country = 1, + City = 4, + Neighborhood = 5, + Street = 6, + Exact = 8, +}; + +/* + * Whether to reuse D-Bus proxies between uses of this provider. + * Usually a good thing, can be disabled for debug purposes. + */ +static const bool kGCReuseDBusProxy = true; + +class GCLocProviderPriv final : public nsIGeolocationProvider, + public SupportsWeakPtr { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONPROVIDER + + GCLocProviderPriv(); + + void UpdateLastPosition(); + + private: + class LocationTimerCallback final : public nsITimerCallback, public nsINamed { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSITIMERCALLBACK + + explicit LocationTimerCallback(GCLocProviderPriv* aParent) + : mParent(aParent) {} + + NS_IMETHOD GetName(nsACString& aName) override { + aName.AssignLiteral("GCLocProvider::LocationTimerCallback"); + return NS_OK; + } + + private: + ~LocationTimerCallback() = default; + WeakPtr mParent; + }; + + enum class Accuracy { Unset, Low, High }; + // States: + // Uninit: The default / initial state, with no client proxy yet. + // Initing: Takes care of establishing the client connection (GetClient / + // ConnectClient / SetDesktopID). + // SettingAccuracy: Does SetAccuracy operation, knows it should just go idle + // after finishing it. + // SettingAccuracyForStart: Does SetAccuracy operation, knows it then needs + // to do a Start operation after finishing it. + // Idle: Fully initialized, but not running state (quiescent). + // Starting: Starts the client by calling the Start D-Bus method. + // Started: Normal running state. + // Stopping: Stops the client by calling the Stop D-Bus method, knows it + // should just go idle after finishing it. + // StoppingForRestart: Stops the client by calling the Stop D-Bus method as + // a part of a Stop -> Start sequence (with possibly + // an accuracy update between these method calls). + // + // Valid state transitions are: + // (any state) -> Uninit: Transition when a D-Bus call failed or + // provided invalid data. + // + // Watch() startup path: + // Uninit -> Initing: Transition after getting the very first Watch() + // request + // or any such request while not having the client proxy. + // Initing -> SettingAccuracyForStart: Transition after getting a successful + // SetDesktopID response. + // SettingAccuracyForStart -> Starting: Transition after getting a + // successful + // SetAccuracy response. + // Idle -> Starting: Transition after getting a Watch() request while in + // fully + // initialized, but not running state. + // SettingAccuracy -> SettingAccuracyForStart: Transition after getting a + // Watch() + // request in the middle of + // setting accuracy during idle + // status. + // Stopping -> StoppingForRestart: Transition after getting a Watch() + // request + // in the middle of doing a Stop D-Bus call + // for idle status. + // StoppingForRestart -> Starting: Transition after getting a successful + // Stop response as a part of a Stop -> + // Start sequence while the previously set + // accuracy is still correct. + // StoppingForRestart -> SettingAccuracyForStart: Transition after getting + // a successful Stop response + // as a part of a Stop -> + // Start sequence but the set + // accuracy needs updating. + // Starting -> Started: Transition after getting a successful Start + // response. + // + // Shutdown() path: + // (any state) -> Uninit: Transition when not reusing the client proxy for + // any reason. + // Started -> Stopping: Transition from normal running state when reusing + // the client proxy. + // SettingAccuracyForStart -> SettingAccuracy: Transition when doing + // a shutdown in the middle of + // setting accuracy for a start + // when reusing the client + // proxy. + // SettingAccuracy -> Idle: Transition after getting a successful + // SetAccuracy + // response. + // StoppingForRestart -> Stopping: Transition when doing shutdown + // in the middle of a Stop -> Start sequence + // when reusing the client proxy. + // Stopping -> Idle: Transition after getting a successful Stop response. + // + // SetHighAccuracy() path: + // Started -> StoppingForRestart: Transition when accuracy needs updating + // on a running client. + // (the rest of the flow in StoppingForRestart state is the same as when + // being in this state in the Watch() startup path) + enum class ClientState { + Uninit, + Initing, + SettingAccuracy, + SettingAccuracyForStart, + Idle, + Starting, + Started, + Stopping, + StoppingForRestart + }; + + ~GCLocProviderPriv(); + + static bool AlwaysHighAccuracy(); + + void SetState(ClientState aNewState, const char* aNewStateStr); + + void Update(nsIDOMGeoPosition* aPosition); + MOZ_CAN_RUN_SCRIPT void NotifyError(int aError); + MOZ_CAN_RUN_SCRIPT void DBusProxyError(const GError* aGError, + bool aResetManager = false); + + MOZ_CAN_RUN_SCRIPT static void GetClientResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData); + void ConnectClient(const gchar* aClientPath); + MOZ_CAN_RUN_SCRIPT static void ConnectClientResponse(GObject* aObject, + GAsyncResult* aResult, + gpointer aUserData); + void SetDesktopID(); + MOZ_CAN_RUN_SCRIPT static void SetDesktopIDResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData); + void SetAccuracy(); + MOZ_CAN_RUN_SCRIPT static void SetAccuracyResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData); + void StartClient(); + MOZ_CAN_RUN_SCRIPT static void StartClientResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData); + void StopClient(bool aForRestart); + MOZ_CAN_RUN_SCRIPT static void StopClientResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData); + void StopClientNoWait(); + void MaybeRestartForAccuracy(); + + MOZ_CAN_RUN_SCRIPT static void GCManagerOwnerNotify(GObject* aObject, + GParamSpec* aPSpec, + gpointer aUserData); + + static void GCClientSignal(GDBusProxy* aProxy, gchar* aSenderName, + gchar* aSignalName, GVariant* aParameters, + gpointer aUserData); + void ConnectLocation(const gchar* aLocationPath); + static bool GetLocationProperty(GDBusProxy* aProxyLocation, + const gchar* aName, double* aOut); + static void ConnectLocationResponse(GObject* aObject, GAsyncResult* aResult, + gpointer aUserData); + + void SetLocationTimer(); + void StopLocationTimer(); + + bool InDBusCall(); + bool InDBusStoppingCall(); + bool InDBusStoppedCall(); + + void DeleteManager(); + void DoShutdown(bool aDeleteClient, bool aDeleteManager); + void DoShutdownClearCallback(bool aDestroying); + + nsresult FallbackToMLS(); + void StopMLSFallback(); + + void WatchStart(); + + Accuracy mAccuracyWanted = Accuracy::Unset; + Accuracy mAccuracySet = Accuracy::Unset; + RefPtr mProxyManager; + RefPtr mProxyClient; + RefPtr mCancellable; + nsCOMPtr mCallback; + ClientState mClientState = ClientState::Uninit; + RefPtr mLastPosition; + RefPtr mLocationTimer; + RefPtr mMLSFallback; +}; + +// +// GCLocProviderPriv +// + +#define GCLP_SETSTATE(this, state) this->SetState(ClientState::state, #state) + +GCLocProviderPriv::GCLocProviderPriv() { + if (AlwaysHighAccuracy()) { + mAccuracyWanted = Accuracy::High; + } else { + mAccuracyWanted = Accuracy::Low; + } +} + +GCLocProviderPriv::~GCLocProviderPriv() { DoShutdownClearCallback(true); } + +bool GCLocProviderPriv::AlwaysHighAccuracy() { + return StaticPrefs::geo_provider_geoclue_always_high_accuracy(); +} + +void GCLocProviderPriv::SetState(ClientState aNewState, + const char* aNewStateStr) { + if (mClientState == aNewState) { + return; + } + + GCL_LOG(Debug, "changing state to %s", aNewStateStr); + mClientState = aNewState; +} + +void GCLocProviderPriv::Update(nsIDOMGeoPosition* aPosition) { + if (!mCallback) { + return; + } + + mCallback->Update(aPosition); +} + +void GCLocProviderPriv::UpdateLastPosition() { + MOZ_DIAGNOSTIC_ASSERT(mLastPosition, "No last position to update"); + StopLocationTimer(); + Update(mLastPosition); +} + +nsresult GCLocProviderPriv::FallbackToMLS() { + GCL_LOG(Debug, "trying to fall back to MLS"); + StopMLSFallback(); + + RefPtr fallback = new MLSFallback(0); + MOZ_TRY(fallback->Startup(mCallback)); + + GCL_LOG(Debug, "Started up MLS fallback"); + mMLSFallback = std::move(fallback); + return NS_OK; +} + +void GCLocProviderPriv::StopMLSFallback() { + if (!mMLSFallback) { + return; + } + GCL_LOG(Debug, "Clearing MLS fallback"); + if (mMLSFallback) { + mMLSFallback->Shutdown(); + mMLSFallback = nullptr; + } +} + +void GCLocProviderPriv::NotifyError(int aError) { + if (!mCallback) { + return; + } + + // We errored out, try to fall back to MLS. + if (NS_SUCCEEDED(FallbackToMLS())) { + return; + } + + nsCOMPtr callback = mCallback; + callback->NotifyError(aError); +} + +void GCLocProviderPriv::DBusProxyError(const GError* aGError, + bool aResetManager) { + // that G_DBUS_ERROR below is actually a function call, not a constant + GQuark gdbusDomain = G_DBUS_ERROR; + int error = GeolocationPositionError_Binding::POSITION_UNAVAILABLE; + if (aGError) { + if (g_error_matches(aGError, gdbusDomain, G_DBUS_ERROR_TIMEOUT) || + g_error_matches(aGError, gdbusDomain, G_DBUS_ERROR_TIMED_OUT)) { + error = GeolocationPositionError_Binding::TIMEOUT; + } else if (g_error_matches(aGError, gdbusDomain, + G_DBUS_ERROR_LIMITS_EXCEEDED) || + g_error_matches(aGError, gdbusDomain, + G_DBUS_ERROR_ACCESS_DENIED) || + g_error_matches(aGError, gdbusDomain, + G_DBUS_ERROR_AUTH_FAILED)) { + error = GeolocationPositionError_Binding::PERMISSION_DENIED; + } + } + + DoShutdown(true, aResetManager); + NotifyError(error); +} + +void GCLocProviderPriv::GetClientResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData) { + GUniquePtr error; + RefPtr variant = dont_AddRef( + g_dbus_proxy_call_finish(aProxy, aResult, getter_Transfers(error))); + if (!variant) { + // if cancelled |self| might no longer be there + if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + GCL_LOG(Error, "Failed to get client: %s\n", error->message); + RefPtr self = static_cast(aUserData); + self->DBusProxyError(error.get(), true); + } + return; + } + + RefPtr self = static_cast(aUserData); + MOZ_DIAGNOSTIC_ASSERT(self->mClientState == ClientState::Initing, + "Client in a wrong state"); + + auto signalError = MakeScopeExit([&]() MOZ_CAN_RUN_SCRIPT_BOUNDARY { + self->DBusProxyError(nullptr, true); + }); + + if (!g_variant_is_of_type(variant, G_VARIANT_TYPE_TUPLE)) { + GCL_LOG(Error, "Unexpected get client call return type: %s\n", + g_variant_get_type_string(variant)); + return; + } + + if (g_variant_n_children(variant) < 1) { + GCL_LOG(Error, + "Not enough params in get client call return: %" G_GSIZE_FORMAT + "\n", + g_variant_n_children(variant)); + return; + } + + variant = dont_AddRef(g_variant_get_child_value(variant, 0)); + if (!g_variant_is_of_type(variant, G_VARIANT_TYPE_OBJECT_PATH)) { + GCL_LOG(Error, "Unexpected get client call return type inside tuple: %s\n", + g_variant_get_type_string(variant)); + return; + } + + const gchar* clientPath = g_variant_get_string(variant, nullptr); + GCL_LOG(Debug, "Client path: %s\n", clientPath); + + signalError.release(); + self->ConnectClient(clientPath); +} + +void GCLocProviderPriv::ConnectClient(const gchar* aClientPath) { + MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Initing, + "Client in a wrong state"); + MOZ_ASSERT(mCancellable, "Watch() wasn't successfully called"); + g_dbus_proxy_new_for_bus( + G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, nullptr, kGeoclueBusName, + aClientPath, kGCClientInterface, mCancellable, + reinterpret_cast(ConnectClientResponse), this); +} + +void GCLocProviderPriv::ConnectClientResponse(GObject* aObject, + GAsyncResult* aResult, + gpointer aUserData) { + GUniquePtr error; + RefPtr proxyClient = + dont_AddRef(g_dbus_proxy_new_finish(aResult, getter_Transfers(error))); + if (!proxyClient) { + // if cancelled |self| might no longer be there + if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + GCL_LOG(Error, "Failed to connect to client: %s\n", error->message); + RefPtr self = static_cast(aUserData); + self->DBusProxyError(error.get()); + } + return; + } + RefPtr self = static_cast(aUserData); + self->mProxyClient = std::move(proxyClient); + + MOZ_DIAGNOSTIC_ASSERT(self->mClientState == ClientState::Initing, + "Client in a wrong state"); + + GCL_LOG(Info, "Client interface connected\n"); + + g_signal_connect(self->mProxyClient, "g-signal", G_CALLBACK(GCClientSignal), + self); + self->SetDesktopID(); +} + +void GCLocProviderPriv::SetDesktopID() { + MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Initing, + "Client in a wrong state"); + MOZ_DIAGNOSTIC_ASSERT(mProxyClient && mCancellable, + "Watch() wasn't successfully called"); + + nsAutoCString appName; + gAppData->GetDBusAppName(appName); + g_dbus_proxy_call(mProxyClient, kDBPropertySetMethod, + g_variant_new("(ssv)", kGCClientInterface, "DesktopId", + g_variant_new_string(appName.get())), + G_DBUS_CALL_FLAGS_NONE, -1, mCancellable, + reinterpret_cast(SetDesktopIDResponse), + this); +} + +void GCLocProviderPriv::SetDesktopIDResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData) { + GUniquePtr error; + + RefPtr variant = dont_AddRef( + g_dbus_proxy_call_finish(aProxy, aResult, getter_Transfers(error))); + if (!variant) { + // if cancelled |self| might no longer be there + if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + GCL_LOG(Error, "Failed to set DesktopId: %s\n", error->message); + RefPtr self = static_cast(aUserData); + self->DBusProxyError(error.get()); + } + return; + } + + RefPtr self = static_cast(aUserData); + MOZ_DIAGNOSTIC_ASSERT(self->mClientState == ClientState::Initing, + "Client in a wrong state"); + + GCLP_SETSTATE(self, Idle); + self->SetAccuracy(); +} + +void GCLocProviderPriv::SetAccuracy() { + MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Idle, + "Client in a wrong state"); + MOZ_DIAGNOSTIC_ASSERT(mProxyClient && mCancellable, + "Watch() wasn't successfully called"); + MOZ_ASSERT(mAccuracyWanted != Accuracy::Unset, "Invalid accuracy"); + + guint32 accuracy; + if (mAccuracyWanted == Accuracy::High) { + accuracy = (guint32)GCAccuracyLevel::Exact; + } else { + accuracy = (guint32)GCAccuracyLevel::City; + } + + mAccuracySet = mAccuracyWanted; + GCLP_SETSTATE(this, SettingAccuracyForStart); + g_dbus_proxy_call( + mProxyClient, kDBPropertySetMethod, + g_variant_new("(ssv)", kGCClientInterface, "RequestedAccuracyLevel", + g_variant_new_uint32(accuracy)), + G_DBUS_CALL_FLAGS_NONE, -1, mCancellable, + reinterpret_cast(SetAccuracyResponse), this); +} + +void GCLocProviderPriv::SetAccuracyResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData) { + GUniquePtr error; + RefPtr variant = dont_AddRef( + g_dbus_proxy_call_finish(aProxy, aResult, getter_Transfers(error))); + if (!variant) { + // if cancelled |self| might no longer be there + if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + GCL_LOG(Error, "Failed to set requested accuracy level: %s\n", + error->message); + RefPtr self = static_cast(aUserData); + self->DBusProxyError(error.get()); + } + return; + } + + RefPtr self = static_cast(aUserData); + MOZ_DIAGNOSTIC_ASSERT( + self->mClientState == ClientState::SettingAccuracyForStart || + self->mClientState == ClientState::SettingAccuracy, + "Client in a wrong state"); + bool wantStart = self->mClientState == ClientState::SettingAccuracyForStart; + GCLP_SETSTATE(self, Idle); + + if (wantStart) { + self->StartClient(); + } +} + +void GCLocProviderPriv::StartClient() { + MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Idle, + "Client in a wrong state"); + MOZ_DIAGNOSTIC_ASSERT(mProxyClient && mCancellable, + "Watch() wasn't successfully called"); + GCLP_SETSTATE(this, Starting); + g_dbus_proxy_call( + mProxyClient, "Start", nullptr, G_DBUS_CALL_FLAGS_NONE, -1, mCancellable, + reinterpret_cast(StartClientResponse), this); +} + +void GCLocProviderPriv::StartClientResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData) { + GUniquePtr error; + + RefPtr variant = dont_AddRef( + g_dbus_proxy_call_finish(aProxy, aResult, getter_Transfers(error))); + if (!variant) { + // if cancelled |self| might no longer be there + if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + GCL_LOG(Error, "Failed to start client: %s\n", error->message); + RefPtr self = static_cast(aUserData); + /* + * A workaround for + * https://gitlab.freedesktop.org/geoclue/geoclue/-/issues/143 We need to + * get a new client instance once the agent finally connects to the + * Geoclue service, otherwise every Start request on the old client + * interface will be denied. We need to reconnect to the Manager interface + * to achieve this since otherwise GetClient call will simply return the + * old client instance. + */ + bool resetManager = g_error_matches(error.get(), G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED); + self->DBusProxyError(error.get(), resetManager); + } + return; + } + + RefPtr self = static_cast(aUserData); + MOZ_DIAGNOSTIC_ASSERT(self->mClientState == ClientState::Starting, + "Client in a wrong state"); + GCLP_SETSTATE(self, Started); + self->MaybeRestartForAccuracy(); +} + +void GCLocProviderPriv::StopClient(bool aForRestart) { + MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Started, + "Client in a wrong state"); + MOZ_DIAGNOSTIC_ASSERT(mProxyClient && mCancellable, + "Watch() wasn't successfully called"); + + if (aForRestart) { + GCLP_SETSTATE(this, StoppingForRestart); + } else { + GCLP_SETSTATE(this, Stopping); + } + + g_dbus_proxy_call( + mProxyClient, "Stop", nullptr, G_DBUS_CALL_FLAGS_NONE, -1, mCancellable, + reinterpret_cast(StopClientResponse), this); +} + +void GCLocProviderPriv::StopClientResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData) { + GUniquePtr error; + RefPtr variant = dont_AddRef( + g_dbus_proxy_call_finish(aProxy, aResult, getter_Transfers(error))); + if (!variant) { + // if cancelled |self| might no longer be there + if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + GCL_LOG(Error, "Failed to stop client: %s\n", error->message); + RefPtr self = static_cast(aUserData); + self->DBusProxyError(error.get()); + } + return; + } + + RefPtr self = static_cast(aUserData); + MOZ_DIAGNOSTIC_ASSERT(self->InDBusStoppingCall(), "Client in a wrong state"); + bool wantRestart = self->mClientState == ClientState::StoppingForRestart; + GCLP_SETSTATE(self, Idle); + + if (!wantRestart) { + return; + } + + if (self->mAccuracyWanted != self->mAccuracySet) { + self->SetAccuracy(); + } else { + self->StartClient(); + } +} + +void GCLocProviderPriv::StopClientNoWait() { + MOZ_DIAGNOSTIC_ASSERT(mProxyClient, "Watch() wasn't successfully called"); + g_dbus_proxy_call(mProxyClient, "Stop", nullptr, G_DBUS_CALL_FLAGS_NONE, -1, + nullptr, nullptr, nullptr); +} + +void GCLocProviderPriv::MaybeRestartForAccuracy() { + if (mAccuracyWanted == mAccuracySet) { + return; + } + + if (mClientState != ClientState::Started) { + return; + } + + // Setting a new accuracy requires restarting the client + StopClient(true); +} + +void GCLocProviderPriv::GCManagerOwnerNotify(GObject* aObject, + GParamSpec* aPSpec, + gpointer aUserData) { + RefPtr self = static_cast(aUserData); + GUniquePtr managerOwner( + g_dbus_proxy_get_name_owner(self->mProxyManager)); + if (!managerOwner) { + GCL_LOG(Info, "The Manager interface has lost its owner\n"); + self->DBusProxyError(nullptr, true); + } +} + +void GCLocProviderPriv::GCClientSignal(GDBusProxy* aProxy, gchar* aSenderName, + gchar* aSignalName, + GVariant* aParameters, + gpointer aUserData) { + if (g_strcmp0(aSignalName, "LocationUpdated")) { + return; + } + + if (!g_variant_is_of_type(aParameters, G_VARIANT_TYPE_TUPLE)) { + GCL_LOG(Error, "Unexpected location updated signal params type: %s\n", + g_variant_get_type_string(aParameters)); + return; + } + + if (g_variant_n_children(aParameters) < 2) { + GCL_LOG(Error, + "Not enough params in location updated signal: %" G_GSIZE_FORMAT + "\n", + g_variant_n_children(aParameters)); + return; + } + + RefPtr variant = + dont_AddRef(g_variant_get_child_value(aParameters, 1)); + if (!g_variant_is_of_type(variant, G_VARIANT_TYPE_OBJECT_PATH)) { + GCL_LOG(Error, + "Unexpected location updated signal new location path type: %s\n", + g_variant_get_type_string(variant)); + return; + } + + RefPtr self = static_cast(aUserData); + const gchar* locationPath = g_variant_get_string(variant, nullptr); + GCL_LOG(Verbose, "New location path: %s\n", locationPath); + self->ConnectLocation(locationPath); +} + +void GCLocProviderPriv::ConnectLocation(const gchar* aLocationPath) { + MOZ_ASSERT(mCancellable, "Startup() wasn't successfully called"); + g_dbus_proxy_new_for_bus( + G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, nullptr, kGeoclueBusName, + aLocationPath, kGCLocationInterface, mCancellable, + reinterpret_cast(ConnectLocationResponse), this); +} + +bool GCLocProviderPriv::GetLocationProperty(GDBusProxy* aProxyLocation, + const gchar* aName, double* aOut) { + RefPtr property = + dont_AddRef(g_dbus_proxy_get_cached_property(aProxyLocation, aName)); + if (!g_variant_is_of_type(property, G_VARIANT_TYPE_DOUBLE)) { + GCL_LOG(Error, "Unexpected location property %s type: %s\n", aName, + g_variant_get_type_string(property)); + return false; + } + + *aOut = g_variant_get_double(property); + return true; +} + +void GCLocProviderPriv::ConnectLocationResponse(GObject* aObject, + GAsyncResult* aResult, + gpointer aUserData) { + GUniquePtr error; + RefPtr proxyLocation = + dont_AddRef(g_dbus_proxy_new_finish(aResult, getter_Transfers(error))); + if (!proxyLocation) { + if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + GCL_LOG(Warning, "Failed to connect to location: %s\n", error->message); + } + return; + } + + RefPtr self = static_cast(aUserData); + /* + * nsGeoPositionCoords will convert NaNs to null for optional properties of + * the JavaScript Coordinates object. + */ + double lat = UnspecifiedNaN(); + double lon = UnspecifiedNaN(); + double alt = UnspecifiedNaN(); + double hError = UnspecifiedNaN(); + const double vError = UnspecifiedNaN(); + double heading = UnspecifiedNaN(); + double speed = UnspecifiedNaN(); + struct { + const gchar* name; + double* out; + } props[] = { + {"Latitude", &lat}, {"Longitude", &lon}, {"Altitude", &alt}, + {"Accuracy", &hError}, {"Heading", &heading}, {"Speed", &speed}, + }; + + for (auto& prop : props) { + if (!GetLocationProperty(proxyLocation, prop.name, prop.out)) { + return; + } + } + + if (alt < kGCMinAlt) { + alt = UnspecifiedNaN(); + } + if (speed < 0) { + speed = UnspecifiedNaN(); + } + if (heading < 0 || IsNaN(speed) || speed == 0) { + heading = UnspecifiedNaN(); + } + + GCL_LOG(Info, "New location: %f %f +-%fm @ %gm; hdg %f spd %fm/s\n", lat, lon, + hError, alt, heading, speed); + + self->mLastPosition = + new nsGeoPosition(lat, lon, alt, hError, vError, heading, speed, + PR_Now() / PR_USEC_PER_MSEC); + self->UpdateLastPosition(); +} + +void GCLocProviderPriv::SetLocationTimer() { + MOZ_DIAGNOSTIC_ASSERT(mLastPosition, "no last position to report"); + + StopLocationTimer(); + + RefPtr timerCallback = new LocationTimerCallback(this); + NS_NewTimerWithCallback(getter_AddRefs(mLocationTimer), timerCallback, 1000, + nsITimer::TYPE_ONE_SHOT); +} + +void GCLocProviderPriv::StopLocationTimer() { + if (!mLocationTimer) { + return; + } + + mLocationTimer->Cancel(); + mLocationTimer = nullptr; +} + +// Did we made some D-Bus call and are still waiting for its response? +bool GCLocProviderPriv::InDBusCall() { + return mClientState == ClientState::Initing || + mClientState == ClientState::SettingAccuracy || + mClientState == ClientState::SettingAccuracyForStart || + mClientState == ClientState::Starting || + mClientState == ClientState::Stopping || + mClientState == ClientState::StoppingForRestart; +} + +bool GCLocProviderPriv::InDBusStoppingCall() { + return mClientState == ClientState::Stopping || + mClientState == ClientState::StoppingForRestart; +} + +/* + * Did we made some D-Bus call while stopped and + * are still waiting for its response? + */ +bool GCLocProviderPriv::InDBusStoppedCall() { + return mClientState == ClientState::SettingAccuracy || + mClientState == ClientState::SettingAccuracyForStart; +} + +void GCLocProviderPriv::DeleteManager() { + if (!mProxyManager) { + return; + } + + g_signal_handlers_disconnect_matched(mProxyManager, G_SIGNAL_MATCH_DATA, 0, 0, + nullptr, nullptr, this); + mProxyManager = nullptr; +} + +void GCLocProviderPriv::DoShutdown(bool aDeleteClient, bool aDeleteManager) { + MOZ_DIAGNOSTIC_ASSERT( + !aDeleteManager || aDeleteClient, + "deleting manager proxy requires deleting client one, too"); + + // Invalidate the cached last position + StopLocationTimer(); + mLastPosition = nullptr; + + /* + * Do we need to delete the D-Bus proxy (or proxies)? + * Either because that's what our caller wanted, or because we are set to + * never reuse them, or because we are in a middle of some D-Bus call while + * having the service running (and so not being able to issue an immediate + * Stop call). + */ + if (aDeleteClient || !kGCReuseDBusProxy || + (InDBusCall() && !InDBusStoppingCall() && !InDBusStoppedCall())) { + if (mClientState == ClientState::Started) { + StopClientNoWait(); + GCLP_SETSTATE(this, Idle); + } + if (mProxyClient) { + g_signal_handlers_disconnect_matched(mProxyClient, G_SIGNAL_MATCH_DATA, 0, + 0, nullptr, nullptr, this); + } + if (mCancellable) { + g_cancellable_cancel(mCancellable); + mCancellable = nullptr; + } + mProxyClient = nullptr; + + if (aDeleteManager || !kGCReuseDBusProxy) { + DeleteManager(); + } + + GCLP_SETSTATE(this, Uninit); + } else if (mClientState == ClientState::Started) { + StopClient(false); + } else if (mClientState == ClientState::SettingAccuracyForStart) { + GCLP_SETSTATE(this, SettingAccuracy); + } else if (mClientState == ClientState::StoppingForRestart) { + GCLP_SETSTATE(this, Stopping); + } +} + +void GCLocProviderPriv::DoShutdownClearCallback(bool aDestroying) { + mCallback = nullptr; + StopMLSFallback(); + DoShutdown(aDestroying, aDestroying); +} + +NS_IMPL_ISUPPORTS(GCLocProviderPriv, nsIGeolocationProvider) + +// nsIGeolocationProvider +// + +/* + * The Startup() method should only succeed if Geoclue is available on D-Bus + * so it can be used for determining whether to continue with this geolocation + * provider in Geolocation.cpp + */ +NS_IMETHODIMP +GCLocProviderPriv::Startup() { + if (mProxyManager) { + return NS_OK; + } + + MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Uninit, + "Client in a initialized state but no manager"); + + GUniquePtr error; + mProxyManager = dont_AddRef(g_dbus_proxy_new_for_bus_sync( + G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, nullptr, kGeoclueBusName, + kGCManagerPath, kGCManagerInterface, nullptr, getter_Transfers(error))); + if (!mProxyManager) { + GCL_LOG(Info, "Cannot connect to the Manager interface: %s\n", + error->message); + return NS_ERROR_FAILURE; + } + + g_signal_connect(mProxyManager, "notify::g-name-owner", + G_CALLBACK(GCManagerOwnerNotify), this); + + GUniquePtr managerOwner(g_dbus_proxy_get_name_owner(mProxyManager)); + if (!managerOwner) { + GCL_LOG(Info, "The Manager interface has no owner\n"); + DeleteManager(); + return NS_ERROR_FAILURE; + } + + GCL_LOG(Info, "Manager interface connected successfully\n"); + + return NS_OK; +} + +void GCLocProviderPriv::WatchStart() { + if (mClientState == ClientState::Idle) { + StartClient(); + } else if (mClientState == ClientState::Started) { + if (mLastPosition && !mLocationTimer) { + GCL_LOG(Verbose, + "Will report the existing location if new one doesn't come up\n"); + SetLocationTimer(); + } + } else if (mClientState == ClientState::SettingAccuracy) { + GCLP_SETSTATE(this, SettingAccuracyForStart); + } else if (mClientState == ClientState::Stopping) { + GCLP_SETSTATE(this, StoppingForRestart); + } +} + +NS_IMETHODIMP +GCLocProviderPriv::Watch(nsIGeolocationUpdate* aCallback) { + mCallback = aCallback; + + if (!mCancellable) { + mCancellable = dont_AddRef(g_cancellable_new()); + } + + if (mClientState != ClientState::Uninit) { + WatchStart(); + return NS_OK; + } + + if (!mProxyManager) { + GCL_LOG(Debug, "watch request falling back to MLS"); + return FallbackToMLS(); + } + + StopMLSFallback(); + + GCLP_SETSTATE(this, Initing); + g_dbus_proxy_call(mProxyManager, "GetClient", nullptr, G_DBUS_CALL_FLAGS_NONE, + -1, mCancellable, + reinterpret_cast(GetClientResponse), + this); + + return NS_OK; +} + +NS_IMETHODIMP +GCLocProviderPriv::Shutdown() { + DoShutdownClearCallback(false); + return NS_OK; +} + +NS_IMETHODIMP +GCLocProviderPriv::SetHighAccuracy(bool aHigh) { + GCL_LOG(Verbose, "Want %s accuracy\n", aHigh ? "high" : "low"); + if (!aHigh && AlwaysHighAccuracy()) { + GCL_LOG(Verbose, "Forcing high accuracy due to pref\n"); + aHigh = true; + } + + mAccuracyWanted = aHigh ? Accuracy::High : Accuracy::Low; + MaybeRestartForAccuracy(); + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(GCLocProviderPriv::LocationTimerCallback, nsITimerCallback, + nsINamed) + +NS_IMETHODIMP +GCLocProviderPriv::LocationTimerCallback::Notify(nsITimer* aTimer) { + if (mParent) { + RefPtr parent(mParent); + parent->UpdateLastPosition(); + } + + return NS_OK; +} + +GeoclueLocationProvider::GeoclueLocationProvider() { + mPriv = new GCLocProviderPriv; +} + +// nsISupports +// + +NS_IMPL_ISUPPORTS(GeoclueLocationProvider, nsIGeolocationProvider) + +// nsIGeolocationProvider +// + +NS_IMETHODIMP +GeoclueLocationProvider::Startup() { return mPriv->Startup(); } + +NS_IMETHODIMP +GeoclueLocationProvider::Watch(nsIGeolocationUpdate* aCallback) { + return mPriv->Watch(aCallback); +} + +NS_IMETHODIMP +GeoclueLocationProvider::Shutdown() { return mPriv->Shutdown(); } + +NS_IMETHODIMP +GeoclueLocationProvider::SetHighAccuracy(bool aHigh) { + return mPriv->SetHighAccuracy(aHigh); +} + +} // namespace mozilla::dom diff --git a/dom/system/linux/GeoclueLocationProvider.h b/dom/system/linux/GeoclueLocationProvider.h new file mode 100644 index 0000000000..908cd25e37 --- /dev/null +++ b/dom/system/linux/GeoclueLocationProvider.h @@ -0,0 +1,32 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#ifndef GeoclueLocationProvider_h +#define GeoclueLocationProvider_h + +#include "mozilla/RefPtr.h" +#include "nsIGeolocationProvider.h" + +namespace mozilla::dom { + +class GCLocProviderPriv; + +class GeoclueLocationProvider final : public nsIGeolocationProvider { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONPROVIDER + + GeoclueLocationProvider(); + + private: + ~GeoclueLocationProvider() = default; + + RefPtr mPriv; +}; + +} // namespace mozilla::dom + +#endif /* GeoclueLocationProvider_h */ diff --git a/dom/system/linux/GpsdLocationProvider.cpp b/dom/system/linux/GpsdLocationProvider.cpp new file mode 100644 index 0000000000..0a3f5d7660 --- /dev/null +++ b/dom/system/linux/GpsdLocationProvider.cpp @@ -0,0 +1,446 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "GpsdLocationProvider.h" +#include +#include +#include "MLSFallback.h" +#include "mozilla/Atomics.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/LazyIdleThread.h" +#include "mozilla/dom/GeolocationPositionErrorBinding.h" +#include "GeolocationPosition.h" +#include "nsProxyRelease.h" +#include "nsThreadUtils.h" +#include "prtime.h" + +namespace mozilla { +namespace dom { + +// +// MLSGeolocationUpdate +// + +/** + * |MLSGeolocationUpdate| provides a fallback if gpsd is not supported. + */ +class GpsdLocationProvider::MLSGeolocationUpdate final + : public nsIGeolocationUpdate { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONUPDATE + + explicit MLSGeolocationUpdate(nsIGeolocationUpdate* aCallback); + + protected: + ~MLSGeolocationUpdate() = default; + + private: + nsCOMPtr 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 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& aLocationProvider, + nsIDOMGeoPosition* aPosition) + : Runnable("GpsdU"), + mLocationProvider(aLocationProvider), + mPosition(aPosition) { + MOZ_ASSERT(mLocationProvider); + MOZ_ASSERT(mPosition); + } + + // nsIRunnable + // + + NS_IMETHOD Run() override { + mLocationProvider->Update(mPosition); + return NS_OK; + } + + private: + nsMainThreadPtrHandle mLocationProvider; + RefPtr mPosition; +}; + +// +// NotifyErrorRunnable +// + +class GpsdLocationProvider::NotifyErrorRunnable final : public Runnable { + public: + NotifyErrorRunnable( + const nsMainThreadPtrHandle& aLocationProvider, + int aError) + : Runnable("GpsdNE"), + mLocationProvider(aLocationProvider), + mError(aError) { + MOZ_ASSERT(mLocationProvider); + } + + // nsIRunnable + // + + NS_IMETHOD Run() override { + mLocationProvider->NotifyError(mError); + return NS_OK; + } + + private: + nsMainThreadPtrHandle 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& aLocationProvider) + : Runnable("GpsdP"), + mLocationProvider(aLocationProvider), + mRunning(true) { + MOZ_ASSERT(mLocationProvider); + } + + static bool IsSupported() { + return GPSD_API_MAJOR_VERSION >= 5 && GPSD_API_MAJOR_VERSION <= 12; + } + + bool IsRunning() const { return mRunning; } + + void StopRunning() { mRunning = false; } + + // nsIRunnable + // + + NS_IMETHOD Run() override { + int err; + + switch (GPSD_API_MAJOR_VERSION) { + case 5 ... 12: + err = PollLoop5(); + break; + default: + err = GeolocationPositionError_Binding::POSITION_UNAVAILABLE; + break; + } + + if (err) { + NS_DispatchToMainThread( + MakeAndAddRef(mLocationProvider, err)); + } + + mLocationProvider = nullptr; + + return NS_OK; + } + + protected: + int PollLoop5() { +#if GPSD_API_MAJOR_VERSION >= 5 && GPSD_API_MAJOR_VERSION <= 12 + static const int GPSD_WAIT_TIMEOUT_US = + 1000000; /* us to wait for GPS data */ + + struct gps_data_t gpsData; + + auto res = gps_open(nullptr, nullptr, &gpsData); + + if (res < 0) { + return ErrnoToError(errno); + } + + gps_stream(&gpsData, WATCH_ENABLE | WATCH_JSON, NULL); + + int err = 0; + + // nsGeoPositionCoords will convert NaNs to null for optional properties of + // the JavaScript Coordinates object. + double lat = 0; + double lon = 0; + double alt = UnspecifiedNaN(); + double hError = 0; + double vError = UnspecifiedNaN(); + double heading = UnspecifiedNaN(); + double speed = UnspecifiedNaN(); + + while (IsRunning()) { + errno = 0; + auto hasGpsData = gps_waiting(&gpsData, GPSD_WAIT_TIMEOUT_US); + + if (errno) { + err = ErrnoToError(errno); + break; + } + if (!hasGpsData) { + continue; /* woke up from timeout */ + } + +# if GPSD_API_MAJOR_VERSION >= 7 + res = gps_read(&gpsData, nullptr, 0); +# else + + res = gps_read(&gpsData); +# endif + + if (res < 0) { + err = ErrnoToError(errno); + break; + } else if (!res) { + continue; /* no data available */ + } + +# if GPSD_API_MAJOR_VERSION < 10 + if (gpsData.status == STATUS_NO_FIX) { + continue; + } +# endif + + switch (gpsData.fix.mode) { + case MODE_3D: + double galt; + +# if GPSD_API_MAJOR_VERSION >= 9 + galt = gpsData.fix.altMSL; +# else + galt = gpsData.fix.altitude; +# endif + if (!IsNaN(galt)) { + alt = galt; + } + [[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.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( + 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 mLocationProvider; + Atomic 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(); + 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 = + MakeAndAddRef(nsMainThreadPtrHandle( + new nsMainThreadPtrHolder("GpsdLP", this))); + + // Use existing poll thread... + RefPtr pollThread = mPollThread; + + // ... or create a new one. + if (!pollThread) { + pollThread = MakeAndAddRef(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(); + 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 mCallback; + RefPtr mPollThread; + RefPtr mPollRunnable; + RefPtr mMLSProvider; +}; + +} // namespace dom +} // namespace mozilla + +#endif /* GpsLocationProvider_h */ diff --git a/dom/system/linux/PortalLocationProvider.cpp b/dom/system/linux/PortalLocationProvider.cpp new file mode 100644 index 0000000000..6ebb1854dc --- /dev/null +++ b/dom/system/linux/PortalLocationProvider.cpp @@ -0,0 +1,351 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "PortalLocationProvider.h" +#include "MLSFallback.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/Logging.h" +#include "mozilla/dom/GeolocationPositionErrorBinding.h" +#include "GeolocationPosition.h" +#include "prtime.h" +#include "mozilla/GUniquePtr.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/XREAppData.h" + +#include +#include + +extern const mozilla::XREAppData* gAppData; + +namespace mozilla::dom { + +#ifdef MOZ_LOGGING +static LazyLogModule sPortalLog("Portal"); +# define LOG_PORTAL(...) MOZ_LOG(sPortalLog, LogLevel::Debug, (__VA_ARGS__)) +#else +# define LOG_PORTAL(...) +#endif /* MOZ_LOGGING */ + +const char kDesktopBusName[] = "org.freedesktop.portal.Desktop"; +const char kSessionInterfaceName[] = "org.freedesktop.portal.Session"; + +/** + * |MLSGeolocationUpdate| provides a fallback if Portal is not supported. + */ +class PortalLocationProvider::MLSGeolocationUpdate final + : public nsIGeolocationUpdate { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONUPDATE + + explicit MLSGeolocationUpdate(nsIGeolocationUpdate* aCallback); + + protected: + ~MLSGeolocationUpdate() = default; + + private: + const nsCOMPtr mCallback; +}; + +PortalLocationProvider::MLSGeolocationUpdate::MLSGeolocationUpdate( + nsIGeolocationUpdate* aCallback) + : mCallback(aCallback) { + MOZ_ASSERT(mCallback); +} + +NS_IMPL_ISUPPORTS(PortalLocationProvider::MLSGeolocationUpdate, + nsIGeolocationUpdate); + +// nsIGeolocationUpdate +// + +NS_IMETHODIMP +PortalLocationProvider::MLSGeolocationUpdate::Update( + nsIDOMGeoPosition* aPosition) { + nsCOMPtr coords; + aPosition->GetCoords(getter_AddRefs(coords)); + if (!coords) { + return NS_ERROR_FAILURE; + } + LOG_PORTAL("MLS is updating position\n"); + return mCallback->Update(aPosition); +} + +NS_IMETHODIMP +PortalLocationProvider::MLSGeolocationUpdate::NotifyError(uint16_t aError) { + nsCOMPtr callback(mCallback); + return callback->NotifyError(aError); +} + +// +// PortalLocationProvider +// + +PortalLocationProvider::PortalLocationProvider() = default; + +PortalLocationProvider::~PortalLocationProvider() { + if (mDBUSLocationProxy || mRefreshTimer || mMLSProvider) { + NS_WARNING( + "PortalLocationProvider: Shutdown() had not been called before " + "destructor."); + Shutdown(); + } +} + +void PortalLocationProvider::Update(nsIDOMGeoPosition* aPosition) { + if (!mCallback) { + return; // not initialized or already shut down + } + + if (mMLSProvider) { + LOG_PORTAL( + "Update from location portal received: Cancelling fallback MLS " + "provider\n"); + mMLSProvider->Shutdown(); + mMLSProvider = nullptr; + } + + LOG_PORTAL("Send updated location to the callback %p", mCallback.get()); + mCallback->Update(aPosition); + + aPosition->GetCoords(getter_AddRefs(mLastGeoPositionCoords)); + // Schedule sending repetitive updates because we don't get more until + // position is changed from portal. That would lead to timeout on the + // Firefox side. + SetRefreshTimer(5000); +} + +void PortalLocationProvider::NotifyError(int aError) { + LOG_PORTAL("*****NotifyError %d\n", aError); + if (!mCallback) { + return; // not initialized or already shut down + } + + if (!mMLSProvider) { + /* With Portal failed, we restart MLS. It will be canceled once we + * get another location from Portal. Start it immediately. + */ + mMLSProvider = MakeAndAddRef(0); + mMLSProvider->Startup(new MLSGeolocationUpdate(mCallback)); + } + + nsCOMPtr callback(mCallback); + callback->NotifyError(aError); +} + +NS_IMPL_ISUPPORTS(PortalLocationProvider, nsIGeolocationProvider) + +static void location_updated_signal_cb(GDBusProxy* proxy, gchar* sender_name, + gchar* signal_name, GVariant* parameters, + gpointer user_data) { + LOG_PORTAL("Signal: %s received from: %s\n", sender_name, signal_name); + + if (g_strcmp0(signal_name, "LocationUpdated")) { + LOG_PORTAL("Unexpected signal %s received", signal_name); + return; + } + + auto* locationProvider = static_cast(user_data); + RefPtr response_data; + gchar* session_handle; + g_variant_get(parameters, "(o@a{sv})", &session_handle, + response_data.StartAssignment()); + if (!response_data) { + LOG_PORTAL("Missing response data from portal\n"); + locationProvider->NotifyError( + GeolocationPositionError_Binding::POSITION_UNAVAILABLE); + return; + } + LOG_PORTAL("Session handle: %s Response data: %s\n", session_handle, + GUniquePtr(g_variant_print(response_data, TRUE)).get()); + g_free(session_handle); + + double lat = 0; + double lon = 0; + if (!g_variant_lookup(response_data, "Latitude", "d", &lat) || + !g_variant_lookup(response_data, "Longitude", "d", &lon)) { + locationProvider->NotifyError( + GeolocationPositionError_Binding::POSITION_UNAVAILABLE); + return; + } + + double alt = UnspecifiedNaN(); + g_variant_lookup(response_data, "Altitude", "d", &alt); + double vError = 0; + double hError = UnspecifiedNaN(); + g_variant_lookup(response_data, "Accuracy", "d", &hError); + double heading = UnspecifiedNaN(); + g_variant_lookup(response_data, "Heading", "d", &heading); + double speed = UnspecifiedNaN(); + g_variant_lookup(response_data, "Speed", "d", &speed); + + locationProvider->Update(new nsGeoPosition(lat, lon, alt, hError, vError, + heading, speed, + PR_Now() / PR_USEC_PER_MSEC)); +} + +NS_IMETHODIMP +PortalLocationProvider::Startup() { + LOG_PORTAL("Starting location portal"); + if (mDBUSLocationProxy) { + LOG_PORTAL("Proxy already started.\n"); + return NS_OK; + } + + // Create dbus proxy for the Location portal + GUniquePtr error; + mDBUSLocationProxy = dont_AddRef(g_dbus_proxy_new_for_bus_sync( + G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, + nullptr, /* GDBusInterfaceInfo */ + kDesktopBusName, "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Location", nullptr, /* GCancellable */ + getter_Transfers(error))); + if (!mDBUSLocationProxy) { + g_printerr("Error creating location dbus proxy: %s\n", error->message); + return NS_OK; // fallback to MLS + } + + // Listen to signals which will be send to us with the location data + mDBUSSignalHandler = + g_signal_connect(mDBUSLocationProxy, "g-signal", + G_CALLBACK(location_updated_signal_cb), this); + + // Call CreateSession of the location portal + GVariantBuilder builder; + + nsAutoCString appName; + gAppData->GetDBusAppName(appName); + g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add(&builder, "{sv}", "session_handle_token", + g_variant_new_string(appName.get())); + + RefPtr result = dont_AddRef(g_dbus_proxy_call_sync( + mDBUSLocationProxy, "CreateSession", g_variant_new("(a{sv})", &builder), + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, getter_Transfers(error))); + + g_variant_builder_clear(&builder); + + if (!result) { + g_printerr("Error calling CreateSession method: %s\n", error->message); + return NS_OK; // fallback to MLS + } + + // Start to listen to the location changes + g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); + + // TODO Use wayland:handle as described in + // https://flatpak.github.io/xdg-desktop-portal/#parent_window + const gchar* parent_window = ""; + gchar* portalSession; + g_variant_get_child(result, 0, "o", &portalSession); + mPortalSession.reset(portalSession); + + result = g_dbus_proxy_call_sync( + mDBUSLocationProxy, "Start", + g_variant_new("(osa{sv})", mPortalSession.get(), parent_window, &builder), + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, getter_Transfers(error)); + + g_variant_builder_clear(&builder); + + if (!result) { + g_printerr("Error calling Start method: %s\n", error->message); + return NS_OK; // fallback to MLS + } + return NS_OK; +} + +NS_IMETHODIMP +PortalLocationProvider::Watch(nsIGeolocationUpdate* aCallback) { + mCallback = aCallback; + + if (mLastGeoPositionCoords) { + // We cannot immediately call the Update there becase the window is not + // yet ready for that. + LOG_PORTAL( + "Update location in 1ms because we have the valid coords cached."); + SetRefreshTimer(1); + return NS_OK; + } + + /* The MLS fallback will kick in after 12 seconds if portal + * doesn't provide location information within time. Once we + * see the first message from portal, the fallback will be + * disabled in |Update|. + */ + mMLSProvider = MakeAndAddRef(12000); + mMLSProvider->Startup(new MLSGeolocationUpdate(aCallback)); + + return NS_OK; +} + +NS_IMETHODIMP PortalLocationProvider::GetName(nsACString& aName) { + aName.AssignLiteral("PortalLocationProvider"); + return NS_OK; +} + +void PortalLocationProvider::SetRefreshTimer(int aDelay) { + LOG_PORTAL("SetRefreshTimer for %p to %d ms\n", this, aDelay); + if (!mRefreshTimer) { + NS_NewTimerWithCallback(getter_AddRefs(mRefreshTimer), this, aDelay, + nsITimer::TYPE_ONE_SHOT); + } else { + mRefreshTimer->Cancel(); + mRefreshTimer->InitWithCallback(this, aDelay, nsITimer::TYPE_ONE_SHOT); + } +} + +NS_IMETHODIMP +PortalLocationProvider::Notify(nsITimer* timer) { + // We need to reschedule the timer because we won't get any update + // from portal until the location is changed. That would cause + // watchPosition to fail with TIMEOUT error. + SetRefreshTimer(5000); + if (mLastGeoPositionCoords) { + LOG_PORTAL("Update location callback with latest coords."); + mCallback->Update( + new nsGeoPosition(mLastGeoPositionCoords, PR_Now() / PR_USEC_PER_MSEC)); + } + return NS_OK; +} + +NS_IMETHODIMP +PortalLocationProvider::Shutdown() { + LOG_PORTAL("Shutdown location provider"); + if (mRefreshTimer) { + mRefreshTimer->Cancel(); + mRefreshTimer = nullptr; + } + mLastGeoPositionCoords = nullptr; + if (mDBUSLocationProxy) { + g_signal_handler_disconnect(mDBUSLocationProxy, mDBUSSignalHandler); + LOG_PORTAL("calling Close method to the session interface...\n"); + RefPtr message = dont_AddRef(g_dbus_message_new_method_call( + kDesktopBusName, mPortalSession.get(), kSessionInterfaceName, "Close")); + mPortalSession = nullptr; + if (message) { + GUniquePtr error; + GDBusConnection* connection = + g_dbus_proxy_get_connection(mDBUSLocationProxy); + g_dbus_connection_send_message( + connection, message, G_DBUS_SEND_MESSAGE_FLAGS_NONE, + /*out_serial=*/nullptr, getter_Transfers(error)); + if (error) { + g_printerr("Failed to close the session: %s\n", error->message); + } + } + mDBUSLocationProxy = nullptr; + } + if (mMLSProvider) { + mMLSProvider->Shutdown(); + mMLSProvider = nullptr; + } + return NS_OK; +} + +NS_IMETHODIMP +PortalLocationProvider::SetHighAccuracy(bool aHigh) { return NS_OK; } + +} // namespace mozilla::dom diff --git a/dom/system/linux/PortalLocationProvider.h b/dom/system/linux/PortalLocationProvider.h new file mode 100644 index 0000000000..e7ead0ab5c --- /dev/null +++ b/dom/system/linux/PortalLocationProvider.h @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef PortalLocationProvider_h +#define PortalLocationProvider_h + +#include "nsCOMPtr.h" +#include "mozilla/GRefPtr.h" +#include "mozilla/GUniquePtr.h" +#include "Geolocation.h" +#include "nsIGeolocationProvider.h" +#include + +class MLSFallback; + +namespace mozilla::dom { + +class PortalLocationProvider final : public nsIGeolocationProvider, + public nsITimerCallback, + public nsINamed { + class MLSGeolocationUpdate; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONPROVIDER + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + + PortalLocationProvider(); + + void Update(nsIDOMGeoPosition* aPosition); + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void NotifyError(int aError); + + private: + ~PortalLocationProvider(); + void SetRefreshTimer(int aDelay); + + RefPtr mDBUSLocationProxy; + gulong mDBUSSignalHandler = 0; + + GUniquePtr mPortalSession; + nsCOMPtr mCallback; + RefPtr mMLSProvider; + nsCOMPtr mLastGeoPositionCoords; + nsCOMPtr mRefreshTimer; +}; + +} // namespace mozilla::dom + +#endif /* GpsLocationProvider_h */ diff --git a/dom/system/linux/moz.build b/dom/system/linux/moz.build new file mode 100644 index 0000000000..f33e81eb13 --- /dev/null +++ b/dom/system/linux/moz.build @@ -0,0 +1,24 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +if CONFIG["MOZ_GPSD"]: + SOURCES += ["GpsdLocationProvider.cpp"] + + CXXFLAGS += CONFIG["MOZ_GPSD_CFLAGS"] + + OS_LIBS += CONFIG["MOZ_GPSD_LIBS"] + + LOCAL_INCLUDES += ["/dom/geolocation"] + + +if CONFIG["MOZ_ENABLE_DBUS"]: + SOURCES += ["GeoclueLocationProvider.cpp"] + SOURCES += ["PortalLocationProvider.cpp"] + LOCAL_INCLUDES += ["/dom/geolocation"] + CXXFLAGS += CONFIG["MOZ_DBUS_GLIB_CFLAGS"] + CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] + +FINAL_LIBRARY = "xul" diff --git a/dom/system/mac/CoreLocationLocationProvider.h b/dom/system/mac/CoreLocationLocationProvider.h new file mode 100644 index 0000000000..27b990cf9a --- /dev/null +++ b/dom/system/mac/CoreLocationLocationProvider.h @@ -0,0 +1,61 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsCOMPtr.h" +#include "nsIGeolocationProvider.h" +#include "mozilla/Attributes.h" + +/* + * The CoreLocationObjects class contains the CoreLocation objects + * we'll need. + * + * Declaring them directly in CoreLocationLocationProvider + * would require Objective-C++ syntax, which would contaminate all + * files that include this header and require them to be Objective-C++ + * as well. + * + * The solution then is to forward-declare CoreLocationObjects here and + * hold a pointer to it in CoreLocationLocationProvider, and only actually + * define it in CoreLocationLocationProvider.mm, thus making it safe + * for Geolocation.cpp, which is C++-only, to include this header. + */ +class CoreLocationObjects; +class MLSFallback; + +class CoreLocationLocationProvider : public nsIGeolocationProvider { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONPROVIDER + + CoreLocationLocationProvider(); + // MOZ_CAN_RUN_SCRIPT_BOUNDARY because we can't mark Objective-C methods as + // MOZ_CAN_RUN_SCRIPT as far as I can tell, and this method is called from + // Objective-C. + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void NotifyError(uint16_t aErrorCode); + void Update(nsIDOMGeoPosition* aSomewhere); + void CreateMLSFallbackProvider(); + void CancelMLSFallbackProvider(); + + private: + virtual ~CoreLocationLocationProvider() = default; + + CoreLocationObjects* mCLObjects; + nsCOMPtr mCallback; + RefPtr 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 +#include +#include +#include + +#include +#include + +#include "nsObjCExceptions.h" + +using namespace mozilla; + +static const CLLocationAccuracy kHIGH_ACCURACY = kCLLocationAccuracyBest; +static const CLLocationAccuracy kDEFAULT_ACCURACY = kCLLocationAccuracyNearestTenMeters; + +@interface LocationDelegate : NSObject { + 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 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(); + altitudeAccuracy = UnspecifiedNaN(); + } + + double speed = location.speed >= 0 ? location.speed : UnspecifiedNaN(); + + double heading = location.course >= 0 ? location.course : UnspecifiedNaN(); + + // nsGeoPositionCoords will convert NaNs to null for optional properties of + // the JavaScript Coordinates object. + nsCOMPtr 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 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(); + + 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 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 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 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..240c56a3f2 --- /dev/null +++ b/dom/system/moz.build @@ -0,0 +1,122 @@ +# -*- 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("IOUtils*"): + BUG_COMPONENT = ("Toolkit", "OS.File") + +with Files("PathUtils*"): + BUG_COMPONENT = ("Toolkit", "OS.File") + +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/ioutils/**"): + BUG_COMPONENT = ("Toolkit", "OS.File") + +with Files("tests/mochitest.ini"): + BUG_COMPONENT = ("Core", "DOM: Device Interfaces") + +with Files("test/*pathutils*"): + BUG_COMPONENT = ("Toolkit", "OS.File") + +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..5b8250ae7e --- /dev/null +++ b/dom/system/nsDeviceSensors.cpp @@ -0,0 +1,556 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Hal.h" +#include "mozilla/HalSensor.h" + +#include "nsContentUtils.h" +#include "nsDeviceSensors.h" + +#include "nsGlobalWindowInner.h" +#include "nsPIDOMWindow.h" +#include "nsIScriptObjectPrincipal.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_device.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/DeviceLightEvent.h" +#include "mozilla/dom/DeviceOrientationEvent.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/UserProximityEvent.h" +#include "mozilla/ErrorResult.h" + +#include + +using namespace mozilla; +using namespace mozilla::dom; +using namespace hal; + +class nsIDOMWindow; + +#undef near + +#define DEFAULT_SENSOR_POLL 100 + +static const nsTArray::index_type NoIndex = + nsTArray::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* windows = new nsTArray(); + 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(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 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 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 sop = do_QueryInterface(aWindow); + nsCOMPtr 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& 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 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 pwindow = + do_QueryInterface(windowListeners[i]); + if (WindowCannotReceiveSensorEvent(pwindow)) { + continue; + } + + if (nsCOMPtr doc = pwindow->GetDoc()) { + nsCOMPtr target = + do_QueryInterface(windowListeners[i]); + if (type == nsIDeviceSensorData::TYPE_ACCELERATION || + type == nsIDeviceSensorData::TYPE_LINEAR_ACCELERATION || + type == nsIDeviceSensorData::TYPE_GYROSCOPE) { + FireDOMMotionEvent(doc, target, type, timestamp, x, y, z); + } else if (type == nsIDeviceSensorData::TYPE_ORIENTATION) { + FireDOMOrientationEvent(target, x, y, z, Orientation::kAbsolute); + } else if (type == nsIDeviceSensorData::TYPE_ROTATION_VECTOR) { + const Orientation orient = RotationVectorToOrientation(x, y, z, w); + FireDOMOrientationEvent(target, orient.alpha, orient.beta, orient.gamma, + Orientation::kAbsolute); + } else if (type == nsIDeviceSensorData::TYPE_GAME_ROTATION_VECTOR) { + const Orientation orient = RotationVectorToOrientation(x, y, z, w); + FireDOMOrientationEvent(target, orient.alpha, orient.beta, orient.gamma, + Orientation::kRelative); + } else if (type == nsIDeviceSensorData::TYPE_PROXIMITY) { + MaybeFireDOMUserProximityEvent(target, x, z); + } else if (type == nsIDeviceSensorData::TYPE_LIGHT) { + FireDOMLightEvent(target, x); + } + } + } +} + +void nsDeviceSensors::FireDOMLightEvent(mozilla::dom::EventTarget* aTarget, + double aValue) { + DeviceLightEventInit init; + init.mBubbles = true; + init.mCancelable = false; + init.mValue = round(aValue); + RefPtr event = + DeviceLightEvent::Constructor(aTarget, u"devicelight"_ns, init); + + event->SetTrusted(true); + + aTarget->DispatchEvent(*event); +} + +void nsDeviceSensors::MaybeFireDOMUserProximityEvent( + mozilla::dom::EventTarget* aTarget, double aValue, double aMax) { + bool near = (aValue < aMax); + if (mIsUserProximityNear != near) { + mIsUserProximityNear = near; + FireDOMUserProximityEvent(aTarget, mIsUserProximityNear); + } +} + +void nsDeviceSensors::FireDOMUserProximityEvent( + mozilla::dom::EventTarget* aTarget, bool aNear) { + UserProximityEventInit init; + init.mBubbles = true; + init.mCancelable = false; + init.mNear = aNear; + RefPtr 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 event = + DeviceOrientationEvent::Constructor(aEventTarget, aType, init); + event->SetTrusted(true); + aEventTarget->DispatchEvent(*event); + }; + + Dispatch(aTarget, aIsAbsolute ? u"deviceorientationabsolute"_ns + : u"deviceorientation"_ns); + + // This is used to determine whether relative events have been dispatched + // during the current session, in which case we don't dispatch the additional + // compatibility events. + static bool sIsDispatchingRelativeEvents = false; + sIsDispatchingRelativeEvents = sIsDispatchingRelativeEvents || !aIsAbsolute; + + // Android devices with SENSOR_GAME_ROTATION_VECTOR support dispatch + // relative events for "deviceorientation" by default, while other platforms + // and devices without such support dispatch absolute events by default. + if (aIsAbsolute && !sIsDispatchingRelativeEvents) { + // For absolute events on devices without support for relative events, + // we need to additionally dispatch type "deviceorientation" to keep + // backwards-compatibility. + Dispatch(aTarget, u"deviceorientation"_ns); + } +} + +void nsDeviceSensors::FireDOMMotionEvent(Document* doc, EventTarget* target, + uint32_t type, PRTime timestamp, + double x, double y, double z) { + // Attempt to coalesce events + TimeDuration sensorPollDuration = + TimeDuration::FromMilliseconds(DEFAULT_SENSOR_POLL); + bool fireEvent = + (TimeStamp::Now() > mLastDOMMotionEventTime + sensorPollDuration) || + StaticPrefs::device_sensors_test_events(); + + switch (type) { + case nsIDeviceSensorData::TYPE_LINEAR_ACCELERATION: + if (!mLastAcceleration) { + mLastAcceleration.emplace(); + } + mLastAcceleration->mX.SetValue(x); + mLastAcceleration->mY.SetValue(y); + mLastAcceleration->mZ.SetValue(z); + break; + case nsIDeviceSensorData::TYPE_ACCELERATION: + if (!mLastAccelerationIncludingGravity) { + mLastAccelerationIncludingGravity.emplace(); + } + mLastAccelerationIncludingGravity->mX.SetValue(x); + mLastAccelerationIncludingGravity->mY.SetValue(y); + mLastAccelerationIncludingGravity->mZ.SetValue(z); + break; + case nsIDeviceSensorData::TYPE_GYROSCOPE: + if (!mLastRotationRate) { + mLastRotationRate.emplace(); + } + mLastRotationRate->mAlpha.SetValue(x); + mLastRotationRate->mBeta.SetValue(y); + mLastRotationRate->mGamma.SetValue(z); + break; + } + + if (fireEvent) { + if (!mLastAcceleration) { + mLastAcceleration.emplace(); + } + if (!mLastAccelerationIncludingGravity) { + mLastAccelerationIncludingGravity.emplace(); + } + if (!mLastRotationRate) { + mLastRotationRate.emplace(); + } + } else if (!mLastAcceleration || !mLastAccelerationIncludingGravity || + !mLastRotationRate) { + return; + } + + IgnoredErrorResult ignored; + RefPtr event = + doc->CreateEvent(u"DeviceMotionEvent"_ns, CallerType::System, ignored); + if (!event) { + return; + } + + DeviceMotionEvent* me = static_cast(event.get()); + + me->InitDeviceMotionEvent( + u"devicemotion"_ns, true, false, *mLastAcceleration, + *mLastAccelerationIncludingGravity, *mLastRotationRate, + Nullable(DEFAULT_SENSOR_POLL), Nullable(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 window = do_QueryInterface(aWindow); + nsCOMPtr doc; + if (window) { + doc = window->GetExtantDoc(); + } + + switch (aType) { + case nsIDeviceSensorData::TYPE_LINEAR_ACCELERATION: + case nsIDeviceSensorData::TYPE_ACCELERATION: + case nsIDeviceSensorData::TYPE_GYROSCOPE: + // checks "device.sensors.motion.enabled" pref + if (!StaticPrefs::device_sensors_motion_enabled()) { + return false; + } + if (doc) { + doc->WarnOnceAbout(DeprecatedOperations::eMotionEvent); + } + break; + case nsIDeviceSensorData::TYPE_GAME_ROTATION_VECTOR: + case nsIDeviceSensorData::TYPE_ORIENTATION: + case nsIDeviceSensorData::TYPE_ROTATION_VECTOR: + // checks "device.sensors.orientation.enabled" pref + if (!StaticPrefs::device_sensors_orientation_enabled()) { + return false; + } + if (doc) { + doc->WarnOnceAbout(DeprecatedOperations::eOrientationEvent); + } + break; + case nsIDeviceSensorData::TYPE_PROXIMITY: + // checks "device.sensors.proximity.enabled" pref + if (!StaticPrefs::device_sensors_proximity_enabled()) { + return false; + } + if (doc) { + doc->WarnOnceAbout(DeprecatedOperations::eProximityEvent, true); + } + break; + case nsIDeviceSensorData::TYPE_LIGHT: + // checks "device.sensors.ambientLight.enabled" pref + if (!StaticPrefs::device_sensors_ambientLight_enabled()) { + return false; + } + if (doc) { + doc->WarnOnceAbout(DeprecatedOperations::eAmbientLightEvent, true); + } + break; + default: + MOZ_ASSERT_UNREACHABLE("Device sensor type not recognised"); + return false; + } + + if (!window) { + return true; + } + return !nsGlobalWindowInner::Cast(window)->ShouldResistFingerprinting(); +} diff --git a/dom/system/nsDeviceSensors.h b/dom/system/nsDeviceSensors.h new file mode 100644 index 0000000000..dd079088e6 --- /dev/null +++ b/dom/system/nsDeviceSensors.h @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsDeviceSensors_h +#define nsDeviceSensors_h + +#include "nsIDeviceSensors.h" +#include "nsCOMArray.h" +#include "nsTArray.h" +#include "nsCOMPtr.h" +#include "mozilla/dom/DeviceMotionEvent.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/HalSensor.h" + +class nsIDOMWindow; + +namespace mozilla::dom { +class Document; +class EventTarget; +} // namespace mozilla::dom + +class nsDeviceSensors : public nsIDeviceSensors, + public mozilla::hal::ISensorObserver { + using DeviceAccelerationInit = mozilla::dom::DeviceAccelerationInit; + using DeviceRotationRateInit = mozilla::dom::DeviceRotationRateInit; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIDEVICESENSORS + + nsDeviceSensors(); + + void Notify(const mozilla::hal::SensorData& aSensorData) override; + + private: + virtual ~nsDeviceSensors(); + + // sensor -> window listener + nsTArray*> mWindowListeners; + + void FireDOMLightEvent(mozilla::dom::EventTarget* aTarget, double value); + + void MaybeFireDOMUserProximityEvent(mozilla::dom::EventTarget* aTarget, + double aValue, double aMax); + + void FireDOMUserProximityEvent(mozilla::dom::EventTarget* aTarget, + bool aNear); + + void FireDOMOrientationEvent(mozilla::dom::EventTarget* target, double aAlpha, + double aBeta, double aGamma, bool aIsAbsolute); + + void FireDOMMotionEvent(mozilla::dom::Document* domDoc, + mozilla::dom::EventTarget* target, uint32_t type, + PRTime timestamp, double x, double y, double z); + + inline bool IsSensorEnabled(uint32_t aType) { + return mWindowListeners[aType]->Length() > 0; + } + + bool IsSensorAllowedByPref(uint32_t aType, nsIDOMWindow* aWindow); + + mozilla::TimeStamp mLastDOMMotionEventTime; + bool mIsUserProximityNear; + mozilla::Maybe mLastAcceleration; + mozilla::Maybe mLastAccelerationIncludingGravity; + mozilla::Maybe 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& 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 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 promiseHandle; + nsresult rv = GetPromise(aCx, promiseHandle); + if (NS_FAILED(rv)) { + return rv; + } + + promiseHandle->MaybeResolve(true /* access authorized */); + promiseHandle.forget(aPromiseOut); + return NS_OK; +} + +NS_IMETHODIMP +nsOSPermissionRequestBase::MaybeRequestScreenCapturePermission() { + return NS_OK; +} diff --git a/dom/system/nsOSPermissionRequestBase.h b/dom/system/nsOSPermissionRequestBase.h new file mode 100644 index 0000000000..e1037b6047 --- /dev/null +++ b/dom/system/nsOSPermissionRequestBase.h @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOSPermissionRequestBase_h__ +#define nsOSPermissionRequestBase_h__ + +#include "nsIOSPermissionRequest.h" +#include "nsWeakReference.h" + +namespace mozilla::dom { +class Promise; +} // namespace mozilla::dom + +using mozilla::dom::Promise; + +/* + * The base implementation of nsIOSPermissionRequest to be subclassed on + * platforms that require permission requests for access to resources such + * as media captures devices. This implementation always returns results + * indicating access is permitted. + */ +class nsOSPermissionRequestBase : public nsIOSPermissionRequest, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOSPERMISSIONREQUEST + + nsOSPermissionRequestBase() = default; + + protected: + nsresult GetPromise(JSContext* aCx, RefPtr& aPromiseOut); + virtual ~nsOSPermissionRequestBase() = default; +}; + +#endif diff --git a/dom/system/tests/chrome.ini b/dom/system/tests/chrome.ini new file mode 100644 index 0000000000..ffdd4393a4 --- /dev/null +++ b/dom/system/tests/chrome.ini @@ -0,0 +1,9 @@ +[DEFAULT] +support-files = + worker_constants.js + +[test_constants.xhtml] +[test_pathutils.html] +[test_pathutils_worker.xhtml] +support-files = + pathutils_worker.js diff --git a/dom/system/tests/file_bug1197901.html b/dom/system/tests/file_bug1197901.html new file mode 100644 index 0000000000..e4ca9fd380 --- /dev/null +++ b/dom/system/tests/file_bug1197901.html @@ -0,0 +1,16 @@ +
Sensor events testing
+ diff --git a/dom/system/tests/ioutils/chrome.ini b/dom/system/tests/ioutils/chrome.ini new file mode 100644 index 0000000000..a961f45631 --- /dev/null +++ b/dom/system/tests/ioutils/chrome.ini @@ -0,0 +1,23 @@ +[DEFAULT] +support-files = + file_ioutils_test_fixtures.js + file_ioutils_worker.js + +[test_ioutils.html] +[test_ioutils_compute_hex_digest.html] +[test_ioutils_copy_move.html] +[test_ioutils_create_unique.html] +[test_ioutils_dir_iteration.html] +[test_ioutils_getfile.html] +[test_ioutils_mac_xattr.html] +skip-if = (os != "mac") +[test_ioutils_mkdir.html] +[test_ioutils_read_write.html] +[test_ioutils_read_write_json.html] +[test_ioutils_read_write_utf8.html] +[test_ioutils_remove.html] +[test_ioutils_stat_set_modification_time.html] +[test_ioutils_worker.xhtml] +[test_ioutils_set_permissions.html] +[test_ioutils_windows_file_attributes.html] +skip-if = (os != 'win') 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..aa28183823 --- /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 */ + +"use strict"; + +/* import-globals-from /testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js */ +importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js"); +/* import-globals-from /toolkit/modules/ObjectUtils.jsm */ +importScripts("resource://gre/modules/ObjectUtils.jsm"); + +importScripts("file_ioutils_test_fixtures.js"); + +self.onmessage = async function(msg) { + const tmpDir = await PathUtils.getTempDir(); + + // IOUtils functionality is the same when called from the main thread, or a + // web worker. These tests are a modified subset of the main thread tests, and + // serve as a confidence check that the implementation is thread-safe. + await test_api_is_available_on_worker(); + await test_full_read_and_write(); + await test_move_file(); + await test_copy_file(); + await test_make_directory(); + + finish(); + info("test_ioutils_worker.xhtml: Test finished"); + + async function test_api_is_available_on_worker() { + ok(self.IOUtils, "IOUtils is present in web workers"); + } + + async function test_full_read_and_write() { + // Write a file. + const tmpFileName = PathUtils.join(tmpDir, "test_ioutils_numbers.tmp"); + const bytes = Uint8Array.of(...new Array(50).keys()); + const bytesWritten = await IOUtils.write(tmpFileName, bytes); + is(bytesWritten, 50, "IOUtils::write can write entire byte array to file"); + + // Read it back. + let fileContents = await IOUtils.read(tmpFileName); + ok( + 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 = PathUtils.join(tmpDir, "test_move_file_src.tmp"); + const dest = PathUtils.join(tmpDir, "test_move_file_dest.tmp"); + const bytes = Uint8Array.of(...new Array(50).keys()); + await IOUtils.write(src, bytes); + + await IOUtils.move(src, dest); + ok( + !(await fileExists(src)) && (await fileExists(dest)), + "IOUtils::move can move files from a worker" + ); + + await cleanup(dest); + } + + async function test_copy_file() { + const tmpFileName = PathUtils.join(tmpDir, "test_ioutils_orig.tmp"); + const destFileName = PathUtils.join(tmpDir, "test_ioutils_copy.tmp"); + await createFile(tmpFileName, "original"); + + await IOUtils.copy(tmpFileName, destFileName); + ok( + (await fileExists(tmpFileName)) && + (await fileHasTextContents(destFileName, "original")), + "IOUtils::copy can copy source to dest in same directory" + ); + + await cleanup(tmpFileName, destFileName); + } + + async function test_make_directory() { + const dir = PathUtils.join(tmpDir, "test_make_dir.tmp.d"); + await IOUtils.makeDirectory(dir); + const stat = await IOUtils.stat(dir); + is( + stat.type, + "directory", + "IOUtils::makeDirectory can make a new directory from a worker" + ); + + await cleanup(dir); + } +}; 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 @@ + + + + + + + Test the IOUtils file I/O API + + + + + + +

+ +

+
+
+
diff --git a/dom/system/tests/ioutils/test_ioutils_compute_hex_digest.html b/dom/system/tests/ioutils/test_ioutils_compute_hex_digest.html
new file mode 100644
index 0000000000..1c27a28822
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_compute_hex_digest.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+  
+  Test the IOUtils file I/O API
+  
+  
+  
+  
+
+
+
+  

+ +

+
+
+
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..ba55eb8463
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_copy_move.html
@@ -0,0 +1,359 @@
+
+
+
+
+
+  
+  Test the IOUtils file I/O API
+  
+  
+  
+  
+
+
+
+  

+ +

+
+
+
diff --git a/dom/system/tests/ioutils/test_ioutils_create_unique.html b/dom/system/tests/ioutils/test_ioutils_create_unique.html
new file mode 100644
index 0000000000..e447964343
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_create_unique.html
@@ -0,0 +1,88 @@
+
+
+
+
+
+  
+  Test the IOUtils file I/O API
+  
+  
+  
+  
+
+
+
+  

+ +

+
+
+
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..93e2c60039
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_dir_iteration.html
@@ -0,0 +1,94 @@
+
+
+
+
+
+  
+  Test the IOUtils file I/O API
+  
+  
+  
+  
+
+
+
+  

+ +

+
+
+
diff --git a/dom/system/tests/ioutils/test_ioutils_getfile.html b/dom/system/tests/ioutils/test_ioutils_getfile.html
new file mode 100644
index 0000000000..d5295f469f
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_getfile.html
@@ -0,0 +1,86 @@
+
+
+
+
+
+  
+  Test the IOUtils file I/O API
+  
+  
+  
+
+
+
+  

+ +

+
+
+
diff --git a/dom/system/tests/ioutils/test_ioutils_mac_xattr.html b/dom/system/tests/ioutils/test_ioutils_mac_xattr.html
new file mode 100644
index 0000000000..cd6e7aeb5d
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_mac_xattr.html
@@ -0,0 +1,90 @@
+
+
+
+
+
+  
+  Test the IOUtils file I/O API
+  
+  
+  
+  
+
+
+
+  

+ +

+
+
+
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..c1a073dea6
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_mkdir.html
@@ -0,0 +1,133 @@
+
+
+
+
+
+  
+  Test the IOUtils file I/O API
+  
+  
+  
+  
+
+
+
+  

+ +

+
+
+
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..2b36f7de6f
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_read_write.html
@@ -0,0 +1,550 @@
+
+
+
+
+
+  
+  Test the IOUtils file I/O API
+  
+  
+  
+  
+
+
+
+  

+ +

+
+
+
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..e356e50c47
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_read_write_json.html
@@ -0,0 +1,165 @@
+
+
+
+
+
+  
+  Test the IOUtils file I/O API
+  
+  
+  
+  
+
+
+
+  

+ +

+
+
+
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..9ce4ff615a
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html
@@ -0,0 +1,387 @@
+
+
+
+
+
+  
+  Test the IOUtils file I/O API
+  
+  
+  
+  
+
+
+
+  

+ +

+
+
+
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..e6aaf924f2
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_remove.html
@@ -0,0 +1,90 @@
+
+
+
+
+
+  
+  Test the IOUtils file I/O API
+  
+  
+  
+  
+
+
+
+  

+ +

+
+
+
diff --git a/dom/system/tests/ioutils/test_ioutils_set_permissions.html b/dom/system/tests/ioutils/test_ioutils_set_permissions.html
new file mode 100644
index 0000000000..36f7dab72a
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_set_permissions.html
@@ -0,0 +1,84 @@
+
+
+
+
+
+  
+  Test the IOUtils file I/O API
+  
+  
+  
+  
+
+
+
+  

+ +

+
+
+
diff --git a/dom/system/tests/ioutils/test_ioutils_stat_set_modification_time.html b/dom/system/tests/ioutils/test_ioutils_stat_set_modification_time.html
new file mode 100644
index 0000000000..8f8328bd81
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_stat_set_modification_time.html
@@ -0,0 +1,241 @@
+
+
+
+
+
+  
+  Test the IOUtils file I/O API
+  
+  
+  
+  
+
+
+
+  

+ +

+
+
+
diff --git a/dom/system/tests/ioutils/test_ioutils_windows_file_attributes.html b/dom/system/tests/ioutils/test_ioutils_windows_file_attributes.html
new file mode 100644
index 0000000000..b452b4f6db
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_windows_file_attributes.html
@@ -0,0 +1,135 @@
+
+
+
+
+
+  
+  Test the IOUtils file I/O API
+  
+  
+  
+  
+
+
+
+  

+ +

+
+
+
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 @@
+
+
+
+
+  
+
+  
+    

+ +

+  
+  
diff --git a/dom/system/tests/mochitest.ini b/dom/system/tests/mochitest.ini new file mode 100644 index 0000000000..ec18d0016d --- /dev/null +++ b/dom/system/tests/mochitest.ini @@ -0,0 +1,10 @@ +[DEFAULT] +tags = condprof +scheme = https + +support-files = + file_bug1197901.html + +[test_bug1197901.html] +skip-if = os == "android" + condprof #: "Only focused window should get the sensor events" diff --git a/dom/system/tests/pathutils_worker.js b/dom/system/tests/pathutils_worker.js new file mode 100644 index 0000000000..da51334d09 --- /dev/null +++ b/dom/system/tests/pathutils_worker.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-worker */ + +"use strict"; + +/* import-globals-from /testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js */ +importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js"); + +self.onmessage = async function(message) { + let expected = message.data; + info("ON message"); + info(JSON.stringify(expected)); + const profileDir = await PathUtils.getProfileDir(); + is( + profileDir, + expected.profileDir, + "PathUtils.profileDir() in a worker should match PathUtils.profileDir on main thread" + ); + + const localProfileDir = await PathUtils.getLocalProfileDir(); + is( + localProfileDir, + expected.localProfileDir, + "PathUtils.getLocalProfileDir() in a worker should match PathUtils.localProfileDir on main thread" + ); + + const tempDir = await PathUtils.getTempDir(); + is( + tempDir, + expected.tempDir, + "PathUtils.getTempDir() in a worker should match PathUtils.tempDir on main thread" + ); + + const osTempDir = await PathUtils.getOSTempDir(); + is( + osTempDir, + expected.osTempDir, + "PathUtils.getOSTempDir() in a worker should match PathUtils.osTempDir on main thread" + ); + + finish(); +}; diff --git a/dom/system/tests/test_bug1197901.html b/dom/system/tests/test_bug1197901.html new file mode 100644 index 0000000000..7e1866ffa3 --- /dev/null +++ b/dom/system/tests/test_bug1197901.html @@ -0,0 +1,96 @@ + + + + + + Test for Bug 1197901 + + + + + +Mozilla Bug +

+ +
+
+ + + + diff --git a/dom/system/tests/test_constants.xhtml b/dom/system/tests/test_constants.xhtml new file mode 100644 index 0000000000..82c25ce1b0 --- /dev/null +++ b/dom/system/tests/test_constants.xhtml @@ -0,0 +1,140 @@ + + + + + + + +

+ +

+  
+  
diff --git a/dom/system/tests/test_pathutils.html b/dom/system/tests/test_pathutils.html new file mode 100644 index 0000000000..aefd764c17 --- /dev/null +++ b/dom/system/tests/test_pathutils.html @@ -0,0 +1,604 @@ + + + + + + + PathUtils tests + + + + + + + + + + diff --git a/dom/system/tests/test_pathutils_worker.xhtml b/dom/system/tests/test_pathutils_worker.xhtml new file mode 100644 index 0000000000..f8b04bf130 --- /dev/null +++ b/dom/system/tests/test_pathutils_worker.xhtml @@ -0,0 +1,39 @@ + + + + + + + + + +

+ +

+  
+  
diff --git a/dom/system/tests/worker_constants.js b/dom/system/tests/worker_constants.js new file mode 100644 index 0000000000..befc5e4239 --- /dev/null +++ b/dom/system/tests/worker_constants.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-env mozilla/chrome-worker */ +/* global OS */ + +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..3c548cfb21 --- /dev/null +++ b/dom/system/windows/WindowsLocationProvider.cpp @@ -0,0 +1,297 @@ +/* -*- 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/Logging.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/Telemetry.h" +#include "mozilla/dom/GeolocationPositionErrorBinding.h" + +namespace mozilla::dom { + +LazyLogModule gWindowsLocationProviderLog("WindowsLocationProvider"); +#define LOG(...) \ + MOZ_LOG(gWindowsLocationProviderLog, LogLevel::Debug, (__VA_ARGS__)) + +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 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 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 mCallback; + RefPtr 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(this); + } else if (iid == IID_ILocationEvents) { + *ppv = static_cast(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 callback(mCallback); + callback->NotifyError(err); + return S_OK; +} + +STDMETHODIMP +LocationEvent::OnLocationChanged(REFIID aReportType, ILocationReport* aReport) { + if (aReportType != IID_ILatLongReport) { + return S_OK; + } + + RefPtr 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(); + latLongReport->GetAltitude(&alt); + + DOUBLE herror = 0.0; + latLongReport->GetErrorRadius(&herror); + + DOUBLE verror = UnspecifiedNaN(); + latLongReport->GetAltitudeError(&verror); + + double heading = UnspecifiedNaN(); + double speed = UnspecifiedNaN(); + + // nsGeoPositionCoords will convert NaNs to null for optional properties of + // the JavaScript Coordinates object. + RefPtr 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() { + LOG("WindowsLocationProvider::WindowsLocationProvider(%p)\n", this); +} + +WindowsLocationProvider::~WindowsLocationProvider() { + LOG("WindowsLocationProvider::~WindowsLocationProvider(%p, %p)\n", this, + mLocation.get()); +} + +NS_IMETHODIMP +WindowsLocationProvider::Startup() { + LOG("WindowsLocationProvider::Startup(%p, %p)\n", this, mLocation.get()); + if (mLocation) { + return NS_OK; + } + + RefPtr 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) { + LOG("WindowsLocationProvider::Watch(%p, %p, %d)\n", this, mLocation.get(), + mWatching); + if (mLocation) { + if (mWatching) { + return NS_OK; + } + RefPtr event = new LocationEvent(aCallback, this); + if (SUCCEEDED(mLocation->RegisterForReport(event, IID_ILatLongReport, 0))) { + mWatching = true; + return NS_OK; + } + } + + // Cannot use Location API. We will use MLS instead. + LOG(" > MLS fallback\n"); + mLocation = nullptr; + + return CreateAndWatchMLSProvider(aCallback); +} + +NS_IMETHODIMP +WindowsLocationProvider::Shutdown() { + LOG("WindowsLocationProvider::Shutdown(%p, %p)\n", this, mLocation.get()); + if (mLocation) { + if (mWatching) { + mLocation->UnregisterForReport(IID_ILatLongReport); + } + mLocation = nullptr; + mWatching = false; + } + + 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; +} + +#undef LOG + +} // namespace mozilla::dom diff --git a/dom/system/windows/WindowsLocationProvider.h b/dom/system/windows/WindowsLocationProvider.h new file mode 100644 index 0000000000..779236d003 --- /dev/null +++ b/dom/system/windows/WindowsLocationProvider.h @@ -0,0 +1,50 @@ +/* -*- 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 + +class MLSFallback; + +namespace mozilla::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 mCallback; + virtual ~MLSUpdate() {} + }; + + private: + ~WindowsLocationProvider(); + + RefPtr mLocation; + RefPtr mMLSProvider; + bool mWatching = false; +}; + +} // namespace mozilla::dom + +#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 +}; -- cgit v1.2.3