diff options
Diffstat (limited to '')
70 files changed, 13616 insertions, 0 deletions
diff --git a/dom/system/IOUtils.cpp b/dom/system/IOUtils.cpp new file mode 100644 index 0000000000..08e2173452 --- /dev/null +++ b/dom/system/IOUtils.cpp @@ -0,0 +1,2898 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "IOUtils.h" + +#include <cstdint> + +#include "ErrorList.h" +#include "TypedArray.h" +#include "js/ArrayBuffer.h" +#include "js/ColumnNumber.h" // JS::ColumnNumberOneOrigin +#include "js/JSON.h" +#include "js/Utility.h" +#include "js/experimental/TypedData.h" +#include "jsfriendapi.h" +#include "mozilla/Assertions.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/Compression.h" +#include "mozilla/Encoding.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/FileUtils.h" +#include "mozilla/Maybe.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/Services.h" +#include "mozilla/Span.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/TextUtils.h" +#include "mozilla/Try.h" +#include "mozilla/Unused.h" +#include "mozilla/Utf8.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/IOUtilsBinding.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/WorkerRef.h" +#include "mozilla/ipc/LaunchError.h" +#include "PathUtils.h" +#include "nsCOMPtr.h" +#include "nsError.h" +#include "nsFileStreams.h" +#include "nsIDirectoryEnumerator.h" +#include "nsIFile.h" +#include "nsIGlobalObject.h" +#include "nsIInputStream.h" +#include "nsISupports.h" +#include "nsLocalFile.h" +#include "nsNetUtil.h" +#include "nsNSSComponent.h" +#include "nsPrintfCString.h" +#include "nsReadableUtils.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsTArray.h" +#include "nsThreadManager.h" +#include "nsXULAppAPI.h" +#include "prerror.h" +#include "prio.h" +#include "prtime.h" +#include "prtypes.h" +#include "ScopedNSSTypes.h" +#include "secoidt.h" + +#if defined(XP_UNIX) && !defined(ANDROID) +# include "nsSystemInfo.h" +#endif + +#if defined(XP_WIN) +# include "nsILocalFileWin.h" +#elif defined(XP_MACOSX) +# include "nsILocalFileMac.h" +#endif + +#ifdef XP_UNIX +# include "base/process_util.h" +#endif + +#define REJECT_IF_INIT_PATH_FAILED(_file, _path, _promise) \ + do { \ + if (nsresult _rv = PathUtils::InitFileWithPath((_file), (_path)); \ + NS_FAILED(_rv)) { \ + (_promise)->MaybeRejectWithOperationError( \ + FormatErrorMessage(_rv, "Could not parse path (%s)", \ + NS_ConvertUTF16toUTF8(_path).get())); \ + return; \ + } \ + } while (0) + +static constexpr auto SHUTDOWN_ERROR = + "IOUtils: Shutting down and refusing additional I/O tasks"_ns; + +namespace mozilla::dom { + +// static helper functions + +/** + * Platform-specific (e.g. Windows, Unix) implementations of XPCOM APIs may + * report I/O errors inconsistently. For convenience, this function will attempt + * to match a |nsresult| against known results which imply a file cannot be + * found. + * + * @see nsLocalFileWin.cpp + * @see nsLocalFileUnix.cpp + */ +static bool IsFileNotFound(nsresult aResult) { + return aResult == NS_ERROR_FILE_NOT_FOUND; +} +/** + * Like |IsFileNotFound|, but checks for known results that suggest a file + * is not a directory. + */ +static bool IsNotDirectory(nsresult aResult) { + return aResult == NS_ERROR_FILE_DESTINATION_NOT_DIR || + aResult == NS_ERROR_FILE_NOT_DIRECTORY; +} + +/** + * Formats an error message and appends the error name to the end. + */ +template <typename... Args> +static nsCString FormatErrorMessage(nsresult aError, const char* const aMessage, + Args... aArgs) { + nsPrintfCString msg(aMessage, aArgs...); + + if (const char* errName = GetStaticErrorName(aError)) { + msg.AppendPrintf(": %s", errName); + } else { + // In the exceptional case where there is no error name, print the literal + // integer value of the nsresult as an upper case hex value so it can be + // located easily in searchfox. + msg.AppendPrintf(": 0x%" PRIX32, static_cast<uint32_t>(aError)); + } + + return std::move(msg); +} + +static nsCString FormatErrorMessage(nsresult aError, + const char* const aMessage) { + const char* errName = GetStaticErrorName(aError); + if (errName) { + return nsPrintfCString("%s: %s", aMessage, errName); + } + // In the exceptional case where there is no error name, print the literal + // integer value of the nsresult as an upper case hex value so it can be + // located easily in searchfox. + return nsPrintfCString("%s: 0x%" PRIX32, aMessage, + static_cast<uint32_t>(aError)); +} + +[[nodiscard]] inline bool ToJSValue( + JSContext* aCx, const IOUtils::InternalFileInfo& aInternalFileInfo, + JS::MutableHandle<JS::Value> aValue) { + FileInfo info; + info.mPath.Construct(aInternalFileInfo.mPath); + info.mType.Construct(aInternalFileInfo.mType); + info.mSize.Construct(aInternalFileInfo.mSize); + + if (aInternalFileInfo.mCreationTime.isSome()) { + info.mCreationTime.Construct(aInternalFileInfo.mCreationTime.ref()); + } + info.mLastAccessed.Construct(aInternalFileInfo.mLastAccessed); + info.mLastModified.Construct(aInternalFileInfo.mLastModified); + + info.mPermissions.Construct(aInternalFileInfo.mPermissions); + + return ToJSValue(aCx, info, aValue); +} + +template <typename T> +static void ResolveJSPromise(Promise* aPromise, T&& aValue) { + if constexpr (std::is_same_v<T, Ok>) { + aPromise->MaybeResolveWithUndefined(); + } else if constexpr (std::is_same_v<T, nsTArray<uint8_t>>) { + TypedArrayCreator<Uint8Array> array(aValue); + aPromise->MaybeResolve(array); + } else { + aPromise->MaybeResolve(std::forward<T>(aValue)); + } +} + +static void RejectJSPromise(Promise* aPromise, const IOUtils::IOError& aError) { + const auto& errMsg = aError.Message(); + + switch (aError.Code()) { + case NS_ERROR_FILE_UNRESOLVABLE_SYMLINK: + [[fallthrough]]; // to NS_ERROR_FILE_INVALID_PATH + case NS_ERROR_FILE_NOT_FOUND: + [[fallthrough]]; // to NS_ERROR_FILE_INVALID_PATH + case NS_ERROR_FILE_INVALID_PATH: + aPromise->MaybeRejectWithNotFoundError(errMsg.refOr("File not found"_ns)); + break; + case NS_ERROR_FILE_IS_LOCKED: + [[fallthrough]]; // to NS_ERROR_FILE_ACCESS_DENIED + case NS_ERROR_FILE_ACCESS_DENIED: + aPromise->MaybeRejectWithNotAllowedError( + errMsg.refOr("Access was denied to the target file"_ns)); + break; + case NS_ERROR_FILE_TOO_BIG: + aPromise->MaybeRejectWithNotReadableError( + errMsg.refOr("Target file is too big"_ns)); + break; + case NS_ERROR_FILE_NO_DEVICE_SPACE: + aPromise->MaybeRejectWithNotReadableError( + errMsg.refOr("Target device is full"_ns)); + break; + case NS_ERROR_FILE_ALREADY_EXISTS: + aPromise->MaybeRejectWithNoModificationAllowedError( + errMsg.refOr("Target file already exists"_ns)); + break; + case NS_ERROR_FILE_COPY_OR_MOVE_FAILED: + aPromise->MaybeRejectWithOperationError( + errMsg.refOr("Failed to copy or move the target file"_ns)); + break; + case NS_ERROR_FILE_READ_ONLY: + aPromise->MaybeRejectWithReadOnlyError( + errMsg.refOr("Target file is read only"_ns)); + break; + case NS_ERROR_FILE_NOT_DIRECTORY: + [[fallthrough]]; // to NS_ERROR_FILE_DESTINATION_NOT_DIR + case NS_ERROR_FILE_DESTINATION_NOT_DIR: + aPromise->MaybeRejectWithInvalidAccessError( + errMsg.refOr("Target file is not a directory"_ns)); + break; + case NS_ERROR_FILE_IS_DIRECTORY: + aPromise->MaybeRejectWithInvalidAccessError( + errMsg.refOr("Target file is a directory"_ns)); + break; + case NS_ERROR_FILE_UNKNOWN_TYPE: + aPromise->MaybeRejectWithInvalidAccessError( + errMsg.refOr("Target file is of unknown type"_ns)); + break; + case NS_ERROR_FILE_NAME_TOO_LONG: + aPromise->MaybeRejectWithOperationError( + errMsg.refOr("Target file path is too long"_ns)); + break; + case NS_ERROR_FILE_UNRECOGNIZED_PATH: + aPromise->MaybeRejectWithOperationError( + errMsg.refOr("Target file path is not recognized"_ns)); + break; + case NS_ERROR_FILE_DIR_NOT_EMPTY: + aPromise->MaybeRejectWithOperationError( + errMsg.refOr("Target directory is not empty"_ns)); + break; + case NS_ERROR_FILE_DEVICE_FAILURE: + [[fallthrough]]; // to NS_ERROR_FILE_FS_CORRUPTED + case NS_ERROR_FILE_FS_CORRUPTED: + aPromise->MaybeRejectWithNotReadableError( + errMsg.refOr("Target file system may be corrupt or unavailable"_ns)); + break; + case NS_ERROR_FILE_CORRUPTED: + aPromise->MaybeRejectWithNotReadableError( + errMsg.refOr("Target file could not be read and may be corrupt"_ns)); + break; + case NS_ERROR_ILLEGAL_INPUT: + [[fallthrough]]; // NS_ERROR_ILLEGAL_VALUE + case NS_ERROR_ILLEGAL_VALUE: + aPromise->MaybeRejectWithDataError( + errMsg.refOr("Argument is not allowed"_ns)); + break; + case NS_ERROR_NOT_AVAILABLE: + aPromise->MaybeRejectWithNotFoundError(errMsg.refOr("Unavailable"_ns)); + break; + case NS_ERROR_ABORT: + aPromise->MaybeRejectWithAbortError(errMsg.refOr("Operation aborted"_ns)); + break; + default: + aPromise->MaybeRejectWithUnknownError(FormatErrorMessage( + aError.Code(), errMsg.refOr("Unexpected error"_ns).get())); + } +} + +static void RejectShuttingDown(Promise* aPromise) { + RejectJSPromise(aPromise, + IOUtils::IOError(NS_ERROR_ABORT).WithMessage(SHUTDOWN_ERROR)); +} + +static bool AssertParentProcessWithCallerLocationImpl(GlobalObject& aGlobal, + nsCString& reason) { + if (MOZ_LIKELY(XRE_IsParentProcess())) { + return true; + } + + AutoJSAPI jsapi; + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + MOZ_ALWAYS_TRUE(global); + MOZ_ALWAYS_TRUE(jsapi.Init(global)); + + JSContext* cx = jsapi.cx(); + + JS::AutoFilename scriptFilename; + uint32_t lineNo = 0; + JS::ColumnNumberOneOrigin colNo; + + NS_ENSURE_TRUE( + JS::DescribeScriptedCaller(cx, &scriptFilename, &lineNo, &colNo), false); + + NS_ENSURE_TRUE(scriptFilename.get(), false); + + reason.AppendPrintf(" Called from %s:%d:%d.", scriptFilename.get(), lineNo, + colNo.oneOriginValue()); + return false; +} + +static void AssertParentProcessWithCallerLocation(GlobalObject& aGlobal) { + nsCString reason = "IOUtils can only be used in the parent process."_ns; + if (!AssertParentProcessWithCallerLocationImpl(aGlobal, reason)) { + MOZ_CRASH_UNSAFE_PRINTF("%s", reason.get()); + } +} + +// IOUtils implementation +/* static */ +IOUtils::StateMutex IOUtils::sState{"IOUtils::sState"}; + +/* static */ +template <typename Fn> +already_AddRefed<Promise> IOUtils::WithPromiseAndState(GlobalObject& aGlobal, + ErrorResult& aError, + Fn aFn) { + AssertParentProcessWithCallerLocation(aGlobal); + + RefPtr<Promise> promise = CreateJSPromise(aGlobal, aError); + if (!promise) { + return nullptr; + } + + if (auto state = GetState()) { + aFn(promise, state.ref()); + } else { + RejectShuttingDown(promise); + } + return promise.forget(); +} + +/* static */ +template <typename OkT, typename Fn> +void IOUtils::DispatchAndResolve(IOUtils::EventQueue* aQueue, Promise* aPromise, + Fn aFunc) { + RefPtr<StrongWorkerRef> workerRef; + if (!NS_IsMainThread()) { + // We need to manually keep the worker alive until the promise returned by + // Dispatch() resolves or rejects. + workerRef = StrongWorkerRef::CreateForcibly(GetCurrentThreadWorkerPrivate(), + __func__); + } + + if (RefPtr<IOPromise<OkT>> p = aQueue->Dispatch<OkT, Fn>(std::move(aFunc))) { + p->Then( + GetCurrentSerialEventTarget(), __func__, + [workerRef, promise = RefPtr(aPromise)](OkT&& ok) { + ResolveJSPromise(promise, std::forward<OkT>(ok)); + }, + [workerRef, promise = RefPtr(aPromise)](const IOError& err) { + RejectJSPromise(promise, err); + }); + } +} + +/* static */ +already_AddRefed<Promise> IOUtils::Read(GlobalObject& aGlobal, + const nsAString& aPath, + const ReadOptions& aOptions, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + Maybe<uint32_t> toRead = Nothing(); + if (!aOptions.mMaxBytes.IsNull()) { + if (aOptions.mMaxBytes.Value() == 0) { + // Resolve with an empty buffer. + nsTArray<uint8_t> arr(0); + promise->MaybeResolve(TypedArrayCreator<Uint8Array>(arr)); + return; + } + toRead.emplace(aOptions.mMaxBytes.Value()); + } + + DispatchAndResolve<JsBuffer>( + state->mEventQueue, promise, + [file = std::move(file), offset = aOptions.mOffset, toRead, + decompress = aOptions.mDecompress]() { + return ReadSync(file, offset, toRead, decompress, + BufferKind::Uint8Array); + }); + }); +} + +/* static */ +RefPtr<SyncReadFile> IOUtils::OpenFileForSyncReading(GlobalObject& aGlobal, + const nsAString& aPath, + ErrorResult& aRv) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + + // This API is only exposed to workers, so we should not be on the main + // thread here. + MOZ_RELEASE_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<nsIFile> file = new nsLocalFile(); + if (nsresult rv = PathUtils::InitFileWithPath(file, aPath); NS_FAILED(rv)) { + aRv.ThrowOperationError(FormatErrorMessage( + rv, "Could not parse path (%s)", NS_ConvertUTF16toUTF8(aPath).get())); + return nullptr; + } + + RefPtr<nsFileRandomAccessStream> stream = new nsFileRandomAccessStream(); + if (nsresult rv = + stream->Init(file, PR_RDONLY | nsIFile::OS_READAHEAD, 0666, 0); + NS_FAILED(rv)) { + aRv.ThrowOperationError( + FormatErrorMessage(rv, "Could not open the file at %s", + NS_ConvertUTF16toUTF8(aPath).get())); + return nullptr; + } + + int64_t size = 0; + if (nsresult rv = stream->GetSize(&size); NS_FAILED(rv)) { + aRv.ThrowOperationError(FormatErrorMessage( + rv, "Could not get the stream size for the file at %s", + NS_ConvertUTF16toUTF8(aPath).get())); + return nullptr; + } + + return new SyncReadFile(aGlobal.GetAsSupports(), std::move(stream), size); +} + +/* static */ +already_AddRefed<Promise> IOUtils::ReadUTF8(GlobalObject& aGlobal, + const nsAString& aPath, + const ReadUTF8Options& aOptions, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve<JsBuffer>( + state->mEventQueue, promise, + [file = std::move(file), decompress = aOptions.mDecompress]() { + return ReadUTF8Sync(file, decompress); + }); + }); +} + +/* static */ +already_AddRefed<Promise> IOUtils::ReadJSON(GlobalObject& aGlobal, + const nsAString& aPath, + const ReadUTF8Options& aOptions, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + RefPtr<StrongWorkerRef> workerRef; + if (!NS_IsMainThread()) { + // We need to manually keep the worker alive until the promise + // returned by Dispatch() resolves or rejects. + workerRef = StrongWorkerRef::CreateForcibly( + GetCurrentThreadWorkerPrivate(), __func__); + } + + state->mEventQueue + ->template Dispatch<JsBuffer>( + [file, decompress = aOptions.mDecompress]() { + return ReadUTF8Sync(file, decompress); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [workerRef, promise = RefPtr{promise}, + file](JsBuffer&& aBuffer) { + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(promise->GetGlobalObject()))) { + promise->MaybeRejectWithUnknownError( + "Could not initialize JS API"); + return; + } + JSContext* cx = jsapi.cx(); + + JS::Rooted<JSString*> jsonStr( + cx, + IOUtils::JsBuffer::IntoString(cx, std::move(aBuffer))); + if (!jsonStr) { + RejectJSPromise(promise, IOError(NS_ERROR_OUT_OF_MEMORY)); + return; + } + + JS::Rooted<JS::Value> val(cx); + if (!JS_ParseJSON(cx, jsonStr, &val)) { + JS::Rooted<JS::Value> exn(cx); + if (JS_GetPendingException(cx, &exn)) { + JS_ClearPendingException(cx); + promise->MaybeReject(exn); + } else { + RejectJSPromise( + promise, + IOError(NS_ERROR_DOM_UNKNOWN_ERR) + .WithMessage( + "ParseJSON threw an uncatchable exception " + "while parsing file(%s)", + file->HumanReadablePath().get())); + } + + return; + } + + promise->MaybeResolve(val); + }, + [workerRef, promise = RefPtr{promise}](const IOError& aErr) { + RejectJSPromise(promise, aErr); + }); + }); +} + +/* static */ +already_AddRefed<Promise> IOUtils::Write(GlobalObject& aGlobal, + const nsAString& aPath, + const Uint8Array& aData, + const WriteOptions& aOptions, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + Maybe<Buffer<uint8_t>> buf = aData.CreateFromData<Buffer<uint8_t>>(); + if (buf.isNothing()) { + promise->MaybeRejectWithOperationError( + "Out of memory: Could not allocate buffer while writing to file"); + return; + } + + auto opts = InternalWriteOpts::FromBinding(aOptions); + if (opts.isErr()) { + RejectJSPromise(promise, opts.unwrapErr()); + return; + } + + DispatchAndResolve<uint32_t>( + state->mEventQueue, promise, + [file = std::move(file), buf = buf.extract(), + opts = opts.unwrap()]() { return WriteSync(file, buf, opts); }); + }); +} + +/* static */ +already_AddRefed<Promise> IOUtils::WriteUTF8(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aString, + const WriteOptions& aOptions, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + auto opts = InternalWriteOpts::FromBinding(aOptions); + if (opts.isErr()) { + RejectJSPromise(promise, opts.unwrapErr()); + return; + } + + DispatchAndResolve<uint32_t>( + state->mEventQueue, promise, + [file = std::move(file), str = nsCString(aString), + opts = opts.unwrap()]() { + return WriteSync(file, AsBytes(Span(str)), opts); + }); + }); +} + +/* static */ +already_AddRefed<Promise> IOUtils::WriteJSON(GlobalObject& aGlobal, + const nsAString& aPath, + JS::Handle<JS::Value> aValue, + const WriteOptions& aOptions, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + auto opts = InternalWriteOpts::FromBinding(aOptions); + if (opts.isErr()) { + RejectJSPromise(promise, opts.unwrapErr()); + return; + } + + if (opts.inspect().mMode == WriteMode::Append || + opts.inspect().mMode == WriteMode::AppendOrCreate) { + promise->MaybeRejectWithNotSupportedError( + "IOUtils.writeJSON does not support appending to files."_ns); + return; + } + + JSContext* cx = aGlobal.Context(); + JS::Rooted<JS::Value> rootedValue(cx, aValue); + nsString string; + if (!nsContentUtils::StringifyJSON(cx, aValue, string, + UndefinedIsNullStringLiteral)) { + JS::Rooted<JS::Value> exn(cx, JS::UndefinedValue()); + if (JS_GetPendingException(cx, &exn)) { + JS_ClearPendingException(cx); + promise->MaybeReject(exn); + } else { + RejectJSPromise( + promise, + IOError(NS_ERROR_DOM_UNKNOWN_ERR) + .WithMessage("Could not serialize object to JSON")); + } + return; + } + + DispatchAndResolve<uint32_t>( + state->mEventQueue, promise, + [file = std::move(file), string = std::move(string), + opts = opts.unwrap()]() -> Result<uint32_t, IOError> { + nsAutoCString utf8Str; + if (!CopyUTF16toUTF8(string, utf8Str, fallible)) { + return Err(IOError(NS_ERROR_OUT_OF_MEMORY)); + } + return WriteSync(file, AsBytes(Span(utf8Str)), opts); + }); + }); +} + +/* static */ +already_AddRefed<Promise> IOUtils::Move(GlobalObject& aGlobal, + const nsAString& aSourcePath, + const nsAString& aDestPath, + const MoveOptions& aOptions, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr<nsIFile> sourceFile = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(sourceFile, aSourcePath, promise); + + nsCOMPtr<nsIFile> destFile = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(destFile, aDestPath, promise); + + DispatchAndResolve<Ok>( + state->mEventQueue, promise, + [sourceFile = std::move(sourceFile), destFile = std::move(destFile), + noOverwrite = aOptions.mNoOverwrite]() { + return MoveSync(sourceFile, destFile, noOverwrite); + }); + }); +} + +/* static */ +already_AddRefed<Promise> IOUtils::Remove(GlobalObject& aGlobal, + const nsAString& aPath, + const RemoveOptions& aOptions, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve<Ok>( + state->mEventQueue, promise, + [file = std::move(file), ignoreAbsent = aOptions.mIgnoreAbsent, + recursive = aOptions.mRecursive, + retryReadonly = aOptions.mRetryReadonly]() { + return RemoveSync(file, ignoreAbsent, recursive, retryReadonly); + }); + }); +} + +/* static */ +already_AddRefed<Promise> IOUtils::MakeDirectory( + GlobalObject& aGlobal, const nsAString& aPath, + const MakeDirectoryOptions& aOptions, ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve<Ok>(state->mEventQueue, promise, + [file = std::move(file), + createAncestors = aOptions.mCreateAncestors, + ignoreExisting = aOptions.mIgnoreExisting, + permissions = aOptions.mPermissions]() { + return MakeDirectorySync(file, createAncestors, + ignoreExisting, + permissions); + }); + }); +} + +already_AddRefed<Promise> IOUtils::Stat(GlobalObject& aGlobal, + const nsAString& aPath, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve<InternalFileInfo>( + state->mEventQueue, promise, + [file = std::move(file)]() { return StatSync(file); }); + }); +} + +/* static */ +already_AddRefed<Promise> IOUtils::Copy(GlobalObject& aGlobal, + const nsAString& aSourcePath, + const nsAString& aDestPath, + const CopyOptions& aOptions, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr<nsIFile> sourceFile = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(sourceFile, aSourcePath, promise); + + nsCOMPtr<nsIFile> destFile = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(destFile, aDestPath, promise); + + DispatchAndResolve<Ok>( + state->mEventQueue, promise, + [sourceFile = std::move(sourceFile), destFile = std::move(destFile), + noOverwrite = aOptions.mNoOverwrite, + recursive = aOptions.mRecursive]() { + return CopySync(sourceFile, destFile, noOverwrite, recursive); + }); + }); +} + +/* static */ +already_AddRefed<Promise> IOUtils::SetAccessTime( + GlobalObject& aGlobal, const nsAString& aPath, + const Optional<int64_t>& aAccess, ErrorResult& aError) { + return SetTime(aGlobal, aPath, aAccess, &nsIFile::SetLastAccessedTime, + aError); +} + +/* static */ +already_AddRefed<Promise> IOUtils::SetModificationTime( + GlobalObject& aGlobal, const nsAString& aPath, + const Optional<int64_t>& aModification, ErrorResult& aError) { + return SetTime(aGlobal, aPath, aModification, &nsIFile::SetLastModifiedTime, + aError); +} + +/* static */ +already_AddRefed<Promise> IOUtils::SetTime(GlobalObject& aGlobal, + const nsAString& aPath, + const Optional<int64_t>& aNewTime, + IOUtils::SetTimeFn aSetTimeFn, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + int64_t newTime = aNewTime.WasPassed() ? aNewTime.Value() + : PR_Now() / PR_USEC_PER_MSEC; + DispatchAndResolve<int64_t>( + state->mEventQueue, promise, + [file = std::move(file), aSetTimeFn, newTime]() { + return SetTimeSync(file, aSetTimeFn, newTime); + }); + }); +} + +/* static */ +already_AddRefed<Promise> IOUtils::GetChildren( + GlobalObject& aGlobal, const nsAString& aPath, + const GetChildrenOptions& aOptions, ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve<nsTArray<nsString>>( + state->mEventQueue, promise, + [file = std::move(file), ignoreAbsent = aOptions.mIgnoreAbsent]() { + return GetChildrenSync(file, ignoreAbsent); + }); + }); +} + +/* static */ +already_AddRefed<Promise> IOUtils::SetPermissions(GlobalObject& aGlobal, + const nsAString& aPath, + uint32_t aPermissions, + const bool aHonorUmask, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { +#if defined(XP_UNIX) && !defined(ANDROID) + if (aHonorUmask) { + aPermissions &= ~nsSystemInfo::gUserUmask; + } +#endif + + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve<Ok>( + state->mEventQueue, promise, + [file = std::move(file), permissions = aPermissions]() { + return SetPermissionsSync(file, permissions); + }); + }); +} + +/* static */ +already_AddRefed<Promise> IOUtils::Exists(GlobalObject& aGlobal, + const nsAString& aPath, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve<bool>( + state->mEventQueue, promise, + [file = std::move(file)]() { return ExistsSync(file); }); + }); +} + +/* static */ +already_AddRefed<Promise> IOUtils::CreateUniqueFile(GlobalObject& aGlobal, + const nsAString& aParent, + const nsAString& aPrefix, + const uint32_t aPermissions, + ErrorResult& aError) { + return CreateUnique(aGlobal, aParent, aPrefix, nsIFile::NORMAL_FILE_TYPE, + aPermissions, aError); +} + +/* static */ +already_AddRefed<Promise> IOUtils::CreateUniqueDirectory( + GlobalObject& aGlobal, const nsAString& aParent, const nsAString& aPrefix, + const uint32_t aPermissions, ErrorResult& aError) { + return CreateUnique(aGlobal, aParent, aPrefix, nsIFile::DIRECTORY_TYPE, + aPermissions, aError); +} + +/* static */ +already_AddRefed<Promise> IOUtils::CreateUnique(GlobalObject& aGlobal, + const nsAString& aParent, + const nsAString& aPrefix, + const uint32_t aFileType, + const uint32_t aPermissions, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aParent, promise); + + if (nsresult rv = file->Append(aPrefix); NS_FAILED(rv)) { + RejectJSPromise(promise, + IOError(rv).WithMessage( + "Could not append prefix `%s' to parent `%s'", + NS_ConvertUTF16toUTF8(aPrefix).get(), + file->HumanReadablePath().get())); + return; + } + + DispatchAndResolve<nsString>( + state->mEventQueue, promise, + [file = std::move(file), aPermissions, aFileType]() { + return CreateUniqueSync(file, aFileType, aPermissions); + }); + }); +} + +/* static */ +already_AddRefed<Promise> IOUtils::ComputeHexDigest( + GlobalObject& aGlobal, const nsAString& aPath, + const HashAlgorithm aAlgorithm, ErrorResult& aError) { + const bool nssInitialized = EnsureNSSInitializedChromeOrContent(); + + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + if (!nssInitialized) { + RejectJSPromise(promise, + IOError(NS_ERROR_UNEXPECTED) + .WithMessage("Could not initialize NSS")); + return; + } + + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve<nsCString>(state->mEventQueue, promise, + [file = std::move(file), aAlgorithm]() { + return ComputeHexDigestSync(file, + aAlgorithm); + }); + }); +} + +#if defined(XP_WIN) + +/* static */ +already_AddRefed<Promise> IOUtils::GetWindowsAttributes(GlobalObject& aGlobal, + const nsAString& aPath, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + RefPtr<StrongWorkerRef> workerRef; + if (!NS_IsMainThread()) { + // We need to manually keep the worker alive until the promise + // returned by Dispatch() resolves or rejects. + workerRef = StrongWorkerRef::CreateForcibly( + GetCurrentThreadWorkerPrivate(), __func__); + } + + state->mEventQueue + ->template Dispatch<uint32_t>([file = std::move(file)]() { + return GetWindowsAttributesSync(file); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [workerRef, promise = RefPtr{promise}](const uint32_t aAttrs) { + WindowsFileAttributes attrs; + + attrs.mReadOnly.Construct(aAttrs & FILE_ATTRIBUTE_READONLY); + attrs.mHidden.Construct(aAttrs & FILE_ATTRIBUTE_HIDDEN); + attrs.mSystem.Construct(aAttrs & FILE_ATTRIBUTE_SYSTEM); + + promise->MaybeResolve(attrs); + }, + [workerRef, promise = RefPtr{promise}](const IOError& aErr) { + RejectJSPromise(promise, aErr); + }); + }); +} + +/* static */ +already_AddRefed<Promise> IOUtils::SetWindowsAttributes( + GlobalObject& aGlobal, const nsAString& aPath, + const WindowsFileAttributes& aAttrs, ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + uint32_t setAttrs = 0; + uint32_t clearAttrs = 0; + + if (aAttrs.mReadOnly.WasPassed()) { + if (aAttrs.mReadOnly.Value()) { + setAttrs |= FILE_ATTRIBUTE_READONLY; + } else { + clearAttrs |= FILE_ATTRIBUTE_READONLY; + } + } + + if (aAttrs.mHidden.WasPassed()) { + if (aAttrs.mHidden.Value()) { + setAttrs |= FILE_ATTRIBUTE_HIDDEN; + } else { + clearAttrs |= FILE_ATTRIBUTE_HIDDEN; + } + } + + if (aAttrs.mSystem.WasPassed()) { + if (aAttrs.mSystem.Value()) { + setAttrs |= FILE_ATTRIBUTE_SYSTEM; + } else { + clearAttrs |= FILE_ATTRIBUTE_SYSTEM; + } + } + + DispatchAndResolve<Ok>( + state->mEventQueue, promise, + [file = std::move(file), setAttrs, clearAttrs]() { + return SetWindowsAttributesSync(file, setAttrs, clearAttrs); + }); + }); +} + +#elif defined(XP_MACOSX) + +/* static */ +already_AddRefed<Promise> IOUtils::HasMacXAttr(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aAttr, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve<bool>( + state->mEventQueue, promise, + [file = std::move(file), attr = nsCString(aAttr)]() { + return HasMacXAttrSync(file, attr); + }); + }); +} + +/* static */ +already_AddRefed<Promise> IOUtils::GetMacXAttr(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aAttr, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve<nsTArray<uint8_t>>( + state->mEventQueue, promise, + [file = std::move(file), attr = nsCString(aAttr)]() { + return GetMacXAttrSync(file, attr); + }); + }); +} + +/* static */ +already_AddRefed<Promise> IOUtils::SetMacXAttr(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aAttr, + const Uint8Array& aValue, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + nsTArray<uint8_t> value; + + if (!aValue.AppendDataTo(value)) { + RejectJSPromise( + promise, + IOError(NS_ERROR_OUT_OF_MEMORY) + .WithMessage( + "Could not allocate buffer to set extended attribute")); + return; + } + + DispatchAndResolve<Ok>(state->mEventQueue, promise, + [file = std::move(file), attr = nsCString(aAttr), + value = std::move(value)] { + return SetMacXAttrSync(file, attr, value); + }); + }); +} + +/* static */ +already_AddRefed<Promise> IOUtils::DelMacXAttr(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aAttr, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + nsCOMPtr<nsIFile> file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve<Ok>( + state->mEventQueue, promise, + [file = std::move(file), attr = nsCString(aAttr)] { + return DelMacXAttrSync(file, attr); + }); + }); +} + +#endif + +/* static */ +already_AddRefed<Promise> IOUtils::GetFile( + GlobalObject& aGlobal, const Sequence<nsString>& aComponents, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + ErrorResult joinErr; + nsCOMPtr<nsIFile> file = PathUtils::Join(aComponents, joinErr); + if (joinErr.Failed()) { + promise->MaybeReject(std::move(joinErr)); + return; + } + + nsCOMPtr<nsIFile> parent; + if (nsresult rv = file->GetParent(getter_AddRefs(parent)); + NS_FAILED(rv)) { + RejectJSPromise(promise, IOError(rv).WithMessage( + "Could not get parent directory")); + return; + } + + state->mEventQueue + ->template Dispatch<Ok>([parent = std::move(parent)]() { + return MakeDirectorySync(parent, /* aCreateAncestors = */ true, + /* aIgnoreExisting = */ true, 0755); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [file = std::move(file), promise = RefPtr(promise)](const Ok&) { + promise->MaybeResolve(file); + }, + [promise = RefPtr(promise)](const IOError& err) { + RejectJSPromise(promise, err); + }); + }); +} + +/* static */ +already_AddRefed<Promise> IOUtils::GetDirectory( + GlobalObject& aGlobal, const Sequence<nsString>& aComponents, + ErrorResult& aError) { + return WithPromiseAndState( + aGlobal, aError, [&](Promise* promise, auto& state) { + ErrorResult joinErr; + nsCOMPtr<nsIFile> dir = PathUtils::Join(aComponents, joinErr); + if (joinErr.Failed()) { + promise->MaybeReject(std::move(joinErr)); + return; + } + + state->mEventQueue + ->template Dispatch<Ok>([dir]() { + return MakeDirectorySync(dir, /* aCreateAncestors = */ true, + /* aIgnoreExisting = */ true, 0755); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [dir, promise = RefPtr(promise)](const Ok&) { + promise->MaybeResolve(dir); + }, + [promise = RefPtr(promise)](const IOError& err) { + RejectJSPromise(promise, err); + }); + }); +} + +/* static */ +already_AddRefed<Promise> IOUtils::CreateJSPromise(GlobalObject& aGlobal, + ErrorResult& aError) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<Promise> promise = Promise::Create(global, aError); + if (aError.Failed()) { + return nullptr; + } + MOZ_ASSERT(promise); + return do_AddRef(promise); +} + +/* static */ +Result<IOUtils::JsBuffer, IOUtils::IOError> IOUtils::ReadSync( + nsIFile* aFile, const uint64_t aOffset, const Maybe<uint32_t> aMaxBytes, + const bool aDecompress, IOUtils::BufferKind aBufferKind) { + MOZ_ASSERT(!NS_IsMainThread()); + + if (aMaxBytes.isSome() && aDecompress) { + return Err( + IOError(NS_ERROR_ILLEGAL_INPUT) + .WithMessage( + "The `maxBytes` and `decompress` options are not compatible")); + } + + if (aOffset > static_cast<uint64_t>(INT64_MAX)) { + return Err(IOError(NS_ERROR_ILLEGAL_INPUT) + .WithMessage("Requested offset is too large (%" PRIu64 + " > %" PRId64 ")", + aOffset, INT64_MAX)); + } + + const int64_t offset = static_cast<int64_t>(aOffset); + + RefPtr<nsFileRandomAccessStream> stream = new nsFileRandomAccessStream(); + if (nsresult rv = + stream->Init(aFile, PR_RDONLY | nsIFile::OS_READAHEAD, 0666, 0); + NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage("Could not open the file at %s", + aFile->HumanReadablePath().get())); + } + + uint32_t bufSize = 0; + + if (aMaxBytes.isNothing()) { + // Limitation: We cannot read more than the maximum size of a TypedArray + // (UINT32_MAX bytes). Reject if we have been requested to + // perform too large of a read. + + int64_t rawStreamSize = -1; + if (nsresult rv = stream->GetSize(&rawStreamSize); NS_FAILED(rv)) { + return Err(IOError(NS_ERROR_FILE_ACCESS_DENIED) + .WithMessage("Could not get info for the file at %s", + aFile->HumanReadablePath().get())); + } + MOZ_RELEASE_ASSERT(rawStreamSize >= 0); + + uint64_t streamSize = static_cast<uint64_t>(rawStreamSize); + if (aOffset >= streamSize) { + bufSize = 0; + } else { + if (streamSize - offset > static_cast<int64_t>(UINT32_MAX)) { + return Err(IOError(NS_ERROR_FILE_TOO_BIG) + .WithMessage( + "Could not read the file at %s with offset %" PRIu32 + " because it is too large(size=%" PRIu64 " bytes)", + aFile->HumanReadablePath().get(), offset, + streamSize)); + } + + bufSize = static_cast<uint32_t>(streamSize - offset); + } + } else { + bufSize = aMaxBytes.value(); + } + + if (offset > 0) { + if (nsresult rv = stream->Seek(PR_SEEK_SET, offset); NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Could not seek to position %" PRId64 " in file %s", offset, + aFile->HumanReadablePath().get())); + } + } + + JsBuffer buffer = JsBuffer::CreateEmpty(aBufferKind); + + if (bufSize > 0) { + auto result = JsBuffer::Create(aBufferKind, bufSize); + if (result.isErr()) { + return result.propagateErr(); + } + buffer = result.unwrap(); + Span<char> toRead = buffer.BeginWriting(); + + // Read the file from disk. + uint32_t totalRead = 0; + while (totalRead != bufSize) { + // Read no more than INT32_MAX on each call to stream->Read, otherwise it + // returns an error. + uint32_t bytesToReadThisChunk = + std::min<uint32_t>(bufSize - totalRead, INT32_MAX); + uint32_t bytesRead = 0; + if (nsresult rv = + stream->Read(toRead.Elements(), bytesToReadThisChunk, &bytesRead); + NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Encountered an unexpected error while reading file(%s)", + aFile->HumanReadablePath().get())); + } + if (bytesRead == 0) { + break; + } + totalRead += bytesRead; + toRead = toRead.From(bytesRead); + } + + buffer.SetLength(totalRead); + } + + // Decompress the file contents, if required. + if (aDecompress) { + return MozLZ4::Decompress(AsBytes(buffer.BeginReading()), aBufferKind); + } + + return std::move(buffer); +} + +/* static */ +Result<IOUtils::JsBuffer, IOUtils::IOError> IOUtils::ReadUTF8Sync( + nsIFile* aFile, bool aDecompress) { + auto result = ReadSync(aFile, 0, Nothing{}, aDecompress, BufferKind::String); + if (result.isErr()) { + return result.propagateErr(); + } + + JsBuffer buffer = result.unwrap(); + if (!IsUtf8(buffer.BeginReading())) { + return Err( + IOError(NS_ERROR_FILE_CORRUPTED) + .WithMessage( + "Could not read file(%s) because it is not UTF-8 encoded", + aFile->HumanReadablePath().get())); + } + + return buffer; +} + +/* static */ +Result<uint32_t, IOUtils::IOError> IOUtils::WriteSync( + nsIFile* aFile, const Span<const uint8_t>& aByteArray, + const IOUtils::InternalWriteOpts& aOptions) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsIFile* backupFile = aOptions.mBackupFile; + nsIFile* tempFile = aOptions.mTmpFile; + + bool exists = false; + MOZ_TRY(aFile->Exists(&exists)); + + if (exists && aOptions.mMode == WriteMode::Create) { + return Err(IOError(NS_ERROR_FILE_ALREADY_EXISTS) + .WithMessage("Refusing to overwrite the file at %s\n" + "Specify `mode: \"overwrite\"` to allow " + "overwriting the destination", + aFile->HumanReadablePath().get())); + } + + // If backupFile was specified, perform the backup as a move. + if (exists && backupFile) { + // We copy `destFile` here to a new `nsIFile` because + // `nsIFile::MoveToFollowingLinks` will update the path of the file. If we + // did not do this, we would end up having `destFile` point to the same + // location as `backupFile`. Then, when we went to write to `destFile`, we + // would end up overwriting `backupFile` and never actually write to the + // file we were supposed to. + nsCOMPtr<nsIFile> toMove; + MOZ_ALWAYS_SUCCEEDS(aFile->Clone(getter_AddRefs(toMove))); + + bool noOverwrite = aOptions.mMode == WriteMode::Create; + + if (MoveSync(toMove, backupFile, noOverwrite).isErr()) { + return Err(IOError(NS_ERROR_FILE_COPY_OR_MOVE_FAILED) + .WithMessage("Failed to backup the source file(%s) to %s", + aFile->HumanReadablePath().get(), + backupFile->HumanReadablePath().get())); + } + } + + // If tempFile was specified, we will write to there first, then perform a + // move to ensure the file ends up at the final requested destination. + nsIFile* writeFile; + + if (tempFile) { + writeFile = tempFile; + } else { + writeFile = aFile; + } + + int32_t flags = PR_WRONLY; + + switch (aOptions.mMode) { + case WriteMode::Overwrite: + flags |= PR_TRUNCATE | PR_CREATE_FILE; + break; + + case WriteMode::Append: + flags |= PR_APPEND; + break; + + case WriteMode::AppendOrCreate: + flags |= PR_APPEND | PR_CREATE_FILE; + break; + + case WriteMode::Create: + flags |= PR_CREATE_FILE | PR_EXCL; + break; + + default: + MOZ_CRASH("IOUtils: unknown write mode"); + } + + if (aOptions.mFlush) { + flags |= PR_SYNC; + } + + // Try to perform the write and ensure that the file is closed before + // continuing. + uint32_t totalWritten = 0; + { + // Compress the byte array if required. + nsTArray<uint8_t> compressed; + Span<const char> bytes; + if (aOptions.mCompress) { + auto rv = MozLZ4::Compress(aByteArray); + if (rv.isErr()) { + return rv.propagateErr(); + } + compressed = rv.unwrap(); + bytes = Span(reinterpret_cast<const char*>(compressed.Elements()), + compressed.Length()); + } else { + bytes = Span(reinterpret_cast<const char*>(aByteArray.Elements()), + aByteArray.Length()); + } + + RefPtr<nsFileOutputStream> stream = new nsFileOutputStream(); + if (nsresult rv = stream->Init(writeFile, flags, 0666, 0); NS_FAILED(rv)) { + // Normalize platform-specific errors for opening a directory to an access + // denied error. + if (rv == nsresult::NS_ERROR_FILE_IS_DIRECTORY) { + rv = NS_ERROR_FILE_ACCESS_DENIED; + } + return Err( + IOError(rv).WithMessage("Could not open the file at %s for writing", + writeFile->HumanReadablePath().get())); + } + + // nsFileRandomAccessStream::Write uses PR_Write under the hood, which + // accepts a *int32_t* for the chunk size. + uint32_t chunkSize = INT32_MAX; + Span<const char> pendingBytes = bytes; + + while (pendingBytes.Length() > 0) { + if (pendingBytes.Length() < chunkSize) { + chunkSize = pendingBytes.Length(); + } + + uint32_t bytesWritten = 0; + if (nsresult rv = + stream->Write(pendingBytes.Elements(), chunkSize, &bytesWritten); + NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Could not write chunk (size = %" PRIu32 + ") to file %s. The file may be corrupt.", + chunkSize, writeFile->HumanReadablePath().get())); + } + pendingBytes = pendingBytes.From(bytesWritten); + totalWritten += bytesWritten; + } + } + + // If tempFile was passed, check destFile against writeFile and, if they + // differ, the operation is finished by performing a move. + if (tempFile) { + nsAutoStringN<256> destPath; + nsAutoStringN<256> writePath; + + MOZ_ALWAYS_SUCCEEDS(aFile->GetPath(destPath)); + MOZ_ALWAYS_SUCCEEDS(writeFile->GetPath(writePath)); + + // nsIFile::MoveToFollowingLinks will only update the path of the file if + // the move succeeds. + if (destPath != writePath) { + if (aOptions.mTmpFile) { + bool isDir = false; + if (nsresult rv = aFile->IsDirectory(&isDir); + NS_FAILED(rv) && !IsFileNotFound(rv)) { + return Err(IOError(rv).WithMessage("Could not stat the file at %s", + aFile->HumanReadablePath().get())); + } + + // If we attempt to write to a directory *without* a temp file, we get a + // permission error. + // + // However, if we are writing to a temp file first, when we copy the + // temp file over the destination file, we actually end up copying it + // inside the directory, which is not what we want. In this case, we are + // just going to bail out early. + if (isDir) { + return Err( + IOError(NS_ERROR_FILE_ACCESS_DENIED) + .WithMessage("Could not open the file at %s for writing", + aFile->HumanReadablePath().get())); + } + } + + if (MoveSync(writeFile, aFile, /* aNoOverwrite = */ false).isErr()) { + return Err( + IOError(NS_ERROR_FILE_COPY_OR_MOVE_FAILED) + .WithMessage( + "Could not move temporary file(%s) to destination(%s)", + writeFile->HumanReadablePath().get(), + aFile->HumanReadablePath().get())); + } + } + } + return totalWritten; +} + +/* static */ +Result<Ok, IOUtils::IOError> IOUtils::MoveSync(nsIFile* aSourceFile, + nsIFile* aDestFile, + bool aNoOverwrite) { + MOZ_ASSERT(!NS_IsMainThread()); + + // Ensure the source file exists before continuing. If it doesn't exist, + // subsequent operations can fail in different ways on different platforms. + bool srcExists = false; + MOZ_TRY(aSourceFile->Exists(&srcExists)); + if (!srcExists) { + return Err( + IOError(NS_ERROR_FILE_NOT_FOUND) + .WithMessage( + "Could not move source file(%s) because it does not exist", + aSourceFile->HumanReadablePath().get())); + } + + return CopyOrMoveSync(&nsIFile::MoveToFollowingLinks, "move", aSourceFile, + aDestFile, aNoOverwrite); +} + +/* static */ +Result<Ok, IOUtils::IOError> IOUtils::CopySync(nsIFile* aSourceFile, + nsIFile* aDestFile, + bool aNoOverwrite, + bool aRecursive) { + MOZ_ASSERT(!NS_IsMainThread()); + + // Ensure the source file exists before continuing. If it doesn't exist, + // subsequent operations can fail in different ways on different platforms. + bool srcExists; + MOZ_TRY(aSourceFile->Exists(&srcExists)); + if (!srcExists) { + return Err( + IOError(NS_ERROR_FILE_NOT_FOUND) + .WithMessage( + "Could not copy source file(%s) because it does not exist", + aSourceFile->HumanReadablePath().get())); + } + + // If source is a directory, fail immediately unless the recursive option is + // true. + bool srcIsDir = false; + MOZ_TRY(aSourceFile->IsDirectory(&srcIsDir)); + if (srcIsDir && !aRecursive) { + return Err( + IOError(NS_ERROR_FILE_COPY_OR_MOVE_FAILED) + .WithMessage( + "Refused to copy source directory(%s) to the destination(%s)\n" + "Specify the `recursive: true` option to allow copying " + "directories", + aSourceFile->HumanReadablePath().get(), + aDestFile->HumanReadablePath().get())); + } + + return CopyOrMoveSync(&nsIFile::CopyToFollowingLinks, "copy", aSourceFile, + aDestFile, aNoOverwrite); +} + +/* static */ +template <typename CopyOrMoveFn> +Result<Ok, IOUtils::IOError> IOUtils::CopyOrMoveSync(CopyOrMoveFn aMethod, + const char* aMethodName, + nsIFile* aSource, + nsIFile* aDest, + bool aNoOverwrite) { + MOZ_ASSERT(!NS_IsMainThread()); + + // Case 1: Destination is an existing directory. Copy/move source into dest. + bool destIsDir = false; + bool destExists = true; + + nsresult rv = aDest->IsDirectory(&destIsDir); + if (NS_SUCCEEDED(rv) && destIsDir) { + rv = (aSource->*aMethod)(aDest, u""_ns); + if (NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Could not %s source file(%s) to destination directory(%s)", + aMethodName, aSource->HumanReadablePath().get(), + aDest->HumanReadablePath().get())); + } + return Ok(); + } + + if (NS_FAILED(rv)) { + if (!IsFileNotFound(rv)) { + // It's ok if the dest file doesn't exist. Case 2 handles this below. + // Bail out early for any other kind of error though. + return Err(IOError(rv)); + } + destExists = false; + } + + // Case 2: Destination is a file which may or may not exist. + // Try to copy or rename the source to the destination. + // If the destination exists and the source is not a regular file, + // then this may fail. + if (aNoOverwrite && destExists) { + return Err( + IOError(NS_ERROR_FILE_ALREADY_EXISTS) + .WithMessage( + "Could not %s source file(%s) to destination(%s) because the " + "destination already exists and overwrites are not allowed\n" + "Specify the `noOverwrite: false` option to mitigate this " + "error", + aMethodName, aSource->HumanReadablePath().get(), + aDest->HumanReadablePath().get())); + } + if (destExists && !destIsDir) { + // If the source file is a directory, but the target is a file, abort early. + // Different implementations of |CopyTo| and |MoveTo| seem to handle this + // error case differently (or not at all), so we explicitly handle it here. + bool srcIsDir = false; + MOZ_TRY(aSource->IsDirectory(&srcIsDir)); + if (srcIsDir) { + return Err(IOError(NS_ERROR_FILE_DESTINATION_NOT_DIR) + .WithMessage("Could not %s the source directory(%s) to " + "the destination(%s) because the destination " + "is not a directory", + aMethodName, + aSource->HumanReadablePath().get(), + aDest->HumanReadablePath().get())); + } + } + + nsCOMPtr<nsIFile> destDir; + nsAutoString destName; + MOZ_TRY(aDest->GetLeafName(destName)); + MOZ_TRY(aDest->GetParent(getter_AddRefs(destDir))); + + // We know `destName` is a file and therefore must have a parent directory. + MOZ_RELEASE_ASSERT(destDir); + + // NB: if destDir doesn't exist, then |CopyToFollowingLinks| or + // |MoveToFollowingLinks| will create it. + rv = (aSource->*aMethod)(destDir, destName); + if (NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Could not %s the source file(%s) to the destination(%s)", aMethodName, + aSource->HumanReadablePath().get(), aDest->HumanReadablePath().get())); + } + return Ok(); +} + +/* static */ +Result<Ok, IOUtils::IOError> IOUtils::RemoveSync(nsIFile* aFile, + bool aIgnoreAbsent, + bool aRecursive, + bool aRetryReadonly) { + MOZ_ASSERT(!NS_IsMainThread()); + + // Prevent an unused variable warning. + (void)aRetryReadonly; + + nsresult rv = aFile->Remove(aRecursive); + if (aIgnoreAbsent && IsFileNotFound(rv)) { + return Ok(); + } + if (NS_FAILED(rv)) { + IOError err(rv); + if (IsFileNotFound(rv)) { + return Err(err.WithMessage( + "Could not remove the file at %s because it does not exist.\n" + "Specify the `ignoreAbsent: true` option to mitigate this error", + aFile->HumanReadablePath().get())); + } + if (rv == NS_ERROR_FILE_DIR_NOT_EMPTY) { + return Err(err.WithMessage( + "Could not remove the non-empty directory at %s.\n" + "Specify the `recursive: true` option to mitigate this error", + aFile->HumanReadablePath().get())); + } + +#ifdef XP_WIN + + if (rv == NS_ERROR_FILE_ACCESS_DENIED && aRetryReadonly) { + MOZ_TRY(SetWindowsAttributesSync(aFile, 0, FILE_ATTRIBUTE_READONLY)); + return RemoveSync(aFile, aIgnoreAbsent, aRecursive, + /* aRetryReadonly = */ false); + } + +#endif + + return Err(err.WithMessage("Could not remove the file at %s", + aFile->HumanReadablePath().get())); + } + return Ok(); +} + +/* static */ +Result<Ok, IOUtils::IOError> IOUtils::MakeDirectorySync(nsIFile* aFile, + bool aCreateAncestors, + bool aIgnoreExisting, + int32_t aMode) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<nsIFile> parent; + MOZ_TRY(aFile->GetParent(getter_AddRefs(parent))); + if (!parent) { + // If we don't have a parent directory, we were called with a + // root directory. If the directory doesn't already exist (e.g., asking + // for a drive on Windows that does not exist), we will not be able to + // create it. + // + // Calling `nsLocalFile::Create()` on Windows can fail with + // `NS_ERROR_ACCESS_DENIED` trying to create a root directory, but we + // would rather the call succeed, so return early if the directory exists. + // + // Otherwise, we fall through to `nsiFile::Create()` and let it fail there + // instead. + bool exists = false; + MOZ_TRY(aFile->Exists(&exists)); + if (exists) { + return Ok(); + } + } + + nsresult rv = + aFile->Create(nsIFile::DIRECTORY_TYPE, aMode, !aCreateAncestors); + if (NS_FAILED(rv)) { + if (rv == NS_ERROR_FILE_ALREADY_EXISTS) { + // NB: We may report a success only if the target is an existing + // directory. We don't want to silence errors that occur if the target is + // an existing file, since trying to create a directory where a regular + // file exists may be indicative of a logic error. + bool isDirectory; + MOZ_TRY(aFile->IsDirectory(&isDirectory)); + if (!isDirectory) { + return Err(IOError(NS_ERROR_FILE_NOT_DIRECTORY) + .WithMessage("Could not create directory because the " + "target file(%s) exists " + "and is not a directory", + aFile->HumanReadablePath().get())); + } + // The directory exists. + // The caller may suppress this error. + if (aIgnoreExisting) { + return Ok(); + } + // Otherwise, forward it. + return Err(IOError(rv).WithMessage( + "Could not create directory because it already exists at %s\n" + "Specify the `ignoreExisting: true` option to mitigate this " + "error", + aFile->HumanReadablePath().get())); + } + return Err(IOError(rv).WithMessage("Could not create directory at %s", + aFile->HumanReadablePath().get())); + } + return Ok(); +} + +Result<IOUtils::InternalFileInfo, IOUtils::IOError> IOUtils::StatSync( + nsIFile* aFile) { + MOZ_ASSERT(!NS_IsMainThread()); + + InternalFileInfo info; + MOZ_ALWAYS_SUCCEEDS(aFile->GetPath(info.mPath)); + + bool isRegular = false; + // IsFile will stat and cache info in the file object. If the file doesn't + // exist, or there is an access error, we'll discover it here. + // Any subsequent errors are unexpected and will just be forwarded. + nsresult rv = aFile->IsFile(&isRegular); + if (NS_FAILED(rv)) { + IOError err(rv); + if (IsFileNotFound(rv)) { + return Err( + err.WithMessage("Could not stat file(%s) because it does not exist", + aFile->HumanReadablePath().get())); + } + return Err(err); + } + + // Now we can populate the info object by querying the file. + info.mType = FileType::Regular; + if (!isRegular) { + bool isDir = false; + MOZ_TRY(aFile->IsDirectory(&isDir)); + info.mType = isDir ? FileType::Directory : FileType::Other; + } + + int64_t size = -1; + if (info.mType == FileType::Regular) { + MOZ_TRY(aFile->GetFileSize(&size)); + } + info.mSize = size; + + PRTime creationTime = 0; + if (nsresult rv = aFile->GetCreationTime(&creationTime); NS_SUCCEEDED(rv)) { + info.mCreationTime.emplace(static_cast<int64_t>(creationTime)); + } else if (NS_FAILED(rv) && rv != NS_ERROR_NOT_IMPLEMENTED) { + // This field is only supported on some platforms. + return Err(IOError(rv)); + } + + PRTime lastAccessed = 0; + MOZ_TRY(aFile->GetLastAccessedTime(&lastAccessed)); + info.mLastAccessed = static_cast<int64_t>(lastAccessed); + + PRTime lastModified = 0; + MOZ_TRY(aFile->GetLastModifiedTime(&lastModified)); + info.mLastModified = static_cast<int64_t>(lastModified); + + MOZ_TRY(aFile->GetPermissions(&info.mPermissions)); + + return info; +} + +/* static */ +Result<int64_t, IOUtils::IOError> IOUtils::SetTimeSync( + nsIFile* aFile, IOUtils::SetTimeFn aSetTimeFn, int64_t aNewTime) { + MOZ_ASSERT(!NS_IsMainThread()); + + // nsIFile::SetLastModifiedTime will *not* do what is expected when passed 0 + // as an argument. Rather than setting the time to 0, it will recalculate the + // system time and set it to that value instead. We explicit forbid this, + // because this side effect is surprising. + // + // If it ever becomes possible to set a file time to 0, this check should be + // removed, though this use case seems rare. + if (aNewTime == 0) { + return Err( + IOError(NS_ERROR_ILLEGAL_VALUE) + .WithMessage( + "Refusing to set the modification time of file(%s) to 0.\n" + "To use the current system time, call `setModificationTime` " + "with no arguments", + aFile->HumanReadablePath().get())); + } + + nsresult rv = (aFile->*aSetTimeFn)(aNewTime); + + if (NS_FAILED(rv)) { + IOError err(rv); + if (IsFileNotFound(rv)) { + return Err( + err.WithMessage("Could not set modification time of file(%s) " + "because it does not exist", + aFile->HumanReadablePath().get())); + } + return Err(err); + } + return aNewTime; +} + +/* static */ +Result<nsTArray<nsString>, IOUtils::IOError> IOUtils::GetChildrenSync( + nsIFile* aFile, bool aIgnoreAbsent) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsTArray<nsString> children; + nsCOMPtr<nsIDirectoryEnumerator> iter; + nsresult rv = aFile->GetDirectoryEntries(getter_AddRefs(iter)); + if (aIgnoreAbsent && IsFileNotFound(rv)) { + return children; + } + if (NS_FAILED(rv)) { + IOError err(rv); + if (IsFileNotFound(rv)) { + return Err(err.WithMessage( + "Could not get children of file(%s) because it does not exist", + aFile->HumanReadablePath().get())); + } + if (IsNotDirectory(rv)) { + return Err(err.WithMessage( + "Could not get children of file(%s) because it is not a directory", + aFile->HumanReadablePath().get())); + } + return Err(err); + } + + bool hasMoreElements = false; + MOZ_TRY(iter->HasMoreElements(&hasMoreElements)); + while (hasMoreElements) { + nsCOMPtr<nsIFile> child; + MOZ_TRY(iter->GetNextFile(getter_AddRefs(child))); + if (child) { + nsString path; + MOZ_TRY(child->GetPath(path)); + children.AppendElement(path); + } + MOZ_TRY(iter->HasMoreElements(&hasMoreElements)); + } + + return children; +} + +/* static */ +Result<Ok, IOUtils::IOError> IOUtils::SetPermissionsSync( + nsIFile* aFile, const uint32_t aPermissions) { + MOZ_ASSERT(!NS_IsMainThread()); + + MOZ_TRY(aFile->SetPermissions(aPermissions)); + return Ok{}; +} + +/* static */ +Result<bool, IOUtils::IOError> IOUtils::ExistsSync(nsIFile* aFile) { + MOZ_ASSERT(!NS_IsMainThread()); + + bool exists = false; + MOZ_TRY(aFile->Exists(&exists)); + + return exists; +} + +/* static */ +Result<nsString, IOUtils::IOError> IOUtils::CreateUniqueSync( + nsIFile* aFile, const uint32_t aFileType, const uint32_t aPermissions) { + MOZ_ASSERT(!NS_IsMainThread()); + + if (nsresult rv = aFile->CreateUnique(aFileType, aPermissions); + NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage("Could not create unique path")); + } + + nsString path; + MOZ_ALWAYS_SUCCEEDS(aFile->GetPath(path)); + + return path; +} + +/* static */ +Result<nsCString, IOUtils::IOError> IOUtils::ComputeHexDigestSync( + nsIFile* aFile, const HashAlgorithm aAlgorithm) { + static constexpr size_t BUFFER_SIZE = 8192; + + SECOidTag alg; + switch (aAlgorithm) { + case HashAlgorithm::Sha256: + alg = SEC_OID_SHA256; + break; + + case HashAlgorithm::Sha384: + alg = SEC_OID_SHA384; + break; + + case HashAlgorithm::Sha512: + alg = SEC_OID_SHA512; + break; + + default: + MOZ_RELEASE_ASSERT(false, "Unexpected HashAlgorithm"); + } + + Digest digest; + if (nsresult rv = digest.Begin(alg); NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage("Could not hash file at %s", + aFile->HumanReadablePath().get())); + } + + RefPtr<nsIInputStream> stream; + if (nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), aFile); + NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage("Could not open the file at %s", + aFile->HumanReadablePath().get())); + } + + char buffer[BUFFER_SIZE]; + uint32_t read = 0; + for (;;) { + if (nsresult rv = stream->Read(buffer, BUFFER_SIZE, &read); NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Encountered an unexpected error while reading file(%s)", + aFile->HumanReadablePath().get())); + } + if (read == 0) { + break; + } + + if (nsresult rv = + digest.Update(reinterpret_cast<unsigned char*>(buffer), read); + NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage("Could not hash file at %s", + aFile->HumanReadablePath().get())); + } + } + + AutoTArray<uint8_t, SHA512_LENGTH> rawDigest; + if (nsresult rv = digest.End(rawDigest); NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage("Could not hash file at %s", + aFile->HumanReadablePath().get())); + } + + nsCString hexDigest; + if (!hexDigest.SetCapacity(2 * rawDigest.Length(), fallible)) { + return Err(IOError(NS_ERROR_OUT_OF_MEMORY)); + } + + const char HEX[] = "0123456789abcdef"; + for (uint8_t b : rawDigest) { + hexDigest.Append(HEX[(b >> 4) & 0xF]); + hexDigest.Append(HEX[b & 0xF]); + } + + return hexDigest; +} + +#if defined(XP_WIN) + +Result<uint32_t, IOUtils::IOError> IOUtils::GetWindowsAttributesSync( + nsIFile* aFile) { + MOZ_ASSERT(!NS_IsMainThread()); + + uint32_t attrs = 0; + + nsCOMPtr<nsILocalFileWin> file = do_QueryInterface(aFile); + MOZ_ASSERT(file); + + if (nsresult rv = file->GetWindowsFileAttributes(&attrs); NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Could not get Windows file attributes for the file at `%s'", + aFile->HumanReadablePath().get())); + } + return attrs; +} + +Result<Ok, IOUtils::IOError> IOUtils::SetWindowsAttributesSync( + nsIFile* aFile, const uint32_t aSetAttrs, const uint32_t aClearAttrs) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<nsILocalFileWin> file = do_QueryInterface(aFile); + MOZ_ASSERT(file); + + if (nsresult rv = file->SetWindowsFileAttributes(aSetAttrs, aClearAttrs); + NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Could not set Windows file attributes for the file at `%s'", + aFile->HumanReadablePath().get())); + } + + return Ok{}; +} + +#elif defined(XP_MACOSX) + +/* static */ +Result<bool, IOUtils::IOError> IOUtils::HasMacXAttrSync( + nsIFile* aFile, const nsCString& aAttr) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<nsILocalFileMac> file = do_QueryInterface(aFile); + MOZ_ASSERT(file); + + bool hasAttr = false; + if (nsresult rv = file->HasXAttr(aAttr, &hasAttr); NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Could not read the extended attribute `%s' from the file `%s'", + aAttr.get(), aFile->HumanReadablePath().get())); + } + + return hasAttr; +} + +/* static */ +Result<nsTArray<uint8_t>, IOUtils::IOError> IOUtils::GetMacXAttrSync( + nsIFile* aFile, const nsCString& aAttr) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<nsILocalFileMac> file = do_QueryInterface(aFile); + MOZ_ASSERT(file); + + nsTArray<uint8_t> value; + if (nsresult rv = file->GetXAttr(aAttr, value); NS_FAILED(rv)) { + auto err = IOError(rv); + + if (rv == NS_ERROR_NOT_AVAILABLE) { + return Err(err.WithMessage( + "The file `%s' does not have an extended attribute `%s'", + aFile->HumanReadablePath().get(), aAttr.get())); + } + + return Err(err.WithMessage( + "Could not read the extended attribute `%s' from the file `%s'", + aAttr.get(), aFile->HumanReadablePath().get())); + } + + return value; +} + +/* static */ +Result<Ok, IOUtils::IOError> IOUtils::SetMacXAttrSync( + nsIFile* aFile, const nsCString& aAttr, const nsTArray<uint8_t>& aValue) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<nsILocalFileMac> file = do_QueryInterface(aFile); + MOZ_ASSERT(file); + + if (nsresult rv = file->SetXAttr(aAttr, aValue); NS_FAILED(rv)) { + return Err(IOError(rv).WithMessage( + "Could not set extended attribute `%s' on file `%s'", aAttr.get(), + aFile->HumanReadablePath().get())); + } + + return Ok{}; +} + +/* static */ +Result<Ok, IOUtils::IOError> IOUtils::DelMacXAttrSync(nsIFile* aFile, + const nsCString& aAttr) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<nsILocalFileMac> file = do_QueryInterface(aFile); + MOZ_ASSERT(file); + + if (nsresult rv = file->DelXAttr(aAttr); NS_FAILED(rv)) { + auto err = IOError(rv); + + if (rv == NS_ERROR_NOT_AVAILABLE) { + return Err(err.WithMessage( + "The file `%s' does not have an extended attribute `%s'", + aFile->HumanReadablePath().get(), aAttr.get())); + } + + return Err(IOError(rv).WithMessage( + "Could not delete extended attribute `%s' on file `%s'", aAttr.get(), + aFile->HumanReadablePath().get())); + } + + return Ok{}; +} + +#endif + +/* static */ +void IOUtils::GetProfileBeforeChange(GlobalObject& aGlobal, + JS::MutableHandle<JS::Value> aClient, + ErrorResult& aRv) { + return GetShutdownClient(aGlobal, aClient, aRv, + ShutdownPhase::ProfileBeforeChange); +} + +/* static */ +void IOUtils::GetSendTelemetry(GlobalObject& aGlobal, + JS::MutableHandle<JS::Value> aClient, + ErrorResult& aRv) { + return GetShutdownClient(aGlobal, aClient, aRv, ShutdownPhase::SendTelemetry); +} + +/** + * Assert that the given phase has a shutdown client exposed by IOUtils + * + * There is no shutdown client exposed for XpcomWillShutdown. + */ +static void AssertHasShutdownClient(const IOUtils::ShutdownPhase aPhase) { + MOZ_RELEASE_ASSERT(aPhase >= IOUtils::ShutdownPhase::ProfileBeforeChange && + aPhase < IOUtils::ShutdownPhase::XpcomWillShutdown); +} + +/* static */ +void IOUtils::GetShutdownClient(GlobalObject& aGlobal, + JS::MutableHandle<JS::Value> aClient, + ErrorResult& aRv, + const IOUtils::ShutdownPhase aPhase) { + MOZ_RELEASE_ASSERT(XRE_IsParentProcess()); + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + AssertHasShutdownClient(aPhase); + + if (auto state = GetState()) { + MOZ_RELEASE_ASSERT(state.ref()->mBlockerStatus != + ShutdownBlockerStatus::Uninitialized); + + if (state.ref()->mBlockerStatus == ShutdownBlockerStatus::Failed) { + aRv.ThrowAbortError("IOUtils: could not register shutdown blockers"); + return; + } + + MOZ_RELEASE_ASSERT(state.ref()->mBlockerStatus == + ShutdownBlockerStatus::Initialized); + auto result = state.ref()->mEventQueue->GetShutdownClient(aPhase); + if (result.isErr()) { + aRv.ThrowAbortError("IOUtils: could not get shutdown client"); + return; + } + + RefPtr<nsIAsyncShutdownClient> client = result.unwrap(); + MOZ_RELEASE_ASSERT(client); + if (nsresult rv = client->GetJsclient(aClient); NS_FAILED(rv)) { + aRv.ThrowAbortError("IOUtils: Could not get shutdown jsclient"); + } + return; + } + + aRv.ThrowAbortError( + "IOUtils: profileBeforeChange phase has already finished"); +} + +/* sstatic */ +Maybe<IOUtils::StateMutex::AutoLock> IOUtils::GetState() { + auto state = sState.Lock(); + if (state->mQueueStatus == EventQueueStatus::Shutdown) { + return Nothing{}; + } + + if (state->mQueueStatus == EventQueueStatus::Uninitialized) { + MOZ_RELEASE_ASSERT(!state->mEventQueue); + state->mEventQueue = new EventQueue(); + state->mQueueStatus = EventQueueStatus::Initialized; + + MOZ_RELEASE_ASSERT(state->mBlockerStatus == + ShutdownBlockerStatus::Uninitialized); + } + + if (NS_IsMainThread() && + state->mBlockerStatus == ShutdownBlockerStatus::Uninitialized) { + state->SetShutdownHooks(); + } + + return Some(std::move(state)); +} + +IOUtils::EventQueue::EventQueue() { + MOZ_ALWAYS_SUCCEEDS(NS_CreateBackgroundTaskQueue( + "IOUtils::EventQueue", getter_AddRefs(mBackgroundEventTarget))); + + MOZ_RELEASE_ASSERT(mBackgroundEventTarget); +} + +void IOUtils::State::SetShutdownHooks() { + if (mBlockerStatus != ShutdownBlockerStatus::Uninitialized) { + return; + } + + if (NS_WARN_IF(NS_FAILED(mEventQueue->SetShutdownHooks()))) { + mBlockerStatus = ShutdownBlockerStatus::Failed; + } else { + mBlockerStatus = ShutdownBlockerStatus::Initialized; + } + + if (mBlockerStatus != ShutdownBlockerStatus::Initialized) { + NS_WARNING("IOUtils: could not register shutdown blockers."); + } +} + +nsresult IOUtils::EventQueue::SetShutdownHooks() { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + + constexpr static auto STACK = u"IOUtils::EventQueue::SetShutdownHooks"_ns; + constexpr static auto FILE = NS_LITERAL_STRING_FROM_CSTRING(__FILE__); + + nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdownService(); + if (!svc) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr<nsIAsyncShutdownBlocker> profileBeforeChangeBlocker; + + // Create a shutdown blocker for the profile-before-change phase. + { + profileBeforeChangeBlocker = + new IOUtilsShutdownBlocker(ShutdownPhase::ProfileBeforeChange); + + nsCOMPtr<nsIAsyncShutdownClient> globalClient; + MOZ_TRY(svc->GetProfileBeforeChange(getter_AddRefs(globalClient))); + MOZ_RELEASE_ASSERT(globalClient); + + MOZ_TRY(globalClient->AddBlocker(profileBeforeChangeBlocker, FILE, __LINE__, + STACK)); + } + + // Create the shutdown barrier for profile-before-change so that consumers can + // register shutdown blockers. + // + // The blocker we just created will wait for all clients registered on this + // barrier to finish. + { + nsCOMPtr<nsIAsyncShutdownBarrier> barrier; + + // It is okay for this to fail. The created shutdown blocker won't await + // anything and shutdown will proceed. + MOZ_TRY(svc->MakeBarrier( + u"IOUtils: waiting for profileBeforeChange IO to complete"_ns, + getter_AddRefs(barrier))); + MOZ_RELEASE_ASSERT(barrier); + + mBarriers[ShutdownPhase::ProfileBeforeChange] = std::move(barrier); + } + + // Create a shutdown blocker for the profile-before-change-telemetry phase. + nsCOMPtr<nsIAsyncShutdownBlocker> sendTelemetryBlocker; + { + sendTelemetryBlocker = + new IOUtilsShutdownBlocker(ShutdownPhase::SendTelemetry); + + nsCOMPtr<nsIAsyncShutdownClient> globalClient; + MOZ_TRY(svc->GetSendTelemetry(getter_AddRefs(globalClient))); + MOZ_RELEASE_ASSERT(globalClient); + + MOZ_TRY( + globalClient->AddBlocker(sendTelemetryBlocker, FILE, __LINE__, STACK)); + } + + // Create the shutdown barrier for profile-before-change-telemetry so that + // consumers can register shutdown blockers. + // + // The blocker we just created will wait for all clients registered on this + // barrier to finish. + { + nsCOMPtr<nsIAsyncShutdownBarrier> barrier; + + MOZ_TRY(svc->MakeBarrier( + u"IOUtils: waiting for sendTelemetry IO to complete"_ns, + getter_AddRefs(barrier))); + MOZ_RELEASE_ASSERT(barrier); + + // Add a blocker on the previous shutdown phase. + nsCOMPtr<nsIAsyncShutdownClient> client; + MOZ_TRY(barrier->GetClient(getter_AddRefs(client))); + + MOZ_TRY( + client->AddBlocker(profileBeforeChangeBlocker, FILE, __LINE__, STACK)); + + mBarriers[ShutdownPhase::SendTelemetry] = std::move(barrier); + } + + // Create a shutdown blocker for the xpcom-will-shutdown phase. + { + nsCOMPtr<nsIAsyncShutdownClient> globalClient; + MOZ_TRY(svc->GetXpcomWillShutdown(getter_AddRefs(globalClient))); + MOZ_RELEASE_ASSERT(globalClient); + + nsCOMPtr<nsIAsyncShutdownBlocker> blocker = + new IOUtilsShutdownBlocker(ShutdownPhase::XpcomWillShutdown); + MOZ_TRY(globalClient->AddBlocker( + blocker, FILE, __LINE__, u"IOUtils::EventQueue::SetShutdownHooks"_ns)); + } + + // Create a shutdown barrier for the xpcom-will-shutdown phase. + // + // The blocker we just created will wait for all clients registered on this + // barrier to finish. + // + // The only client registered on this barrier should be a blocker for the + // previous phase. This is to ensure that all shutdown IO happens when + // shutdown phases do not happen (e.g., in xpcshell tests where + // profile-before-change does not occur). + { + nsCOMPtr<nsIAsyncShutdownBarrier> barrier; + + MOZ_TRY(svc->MakeBarrier( + u"IOUtils: waiting for xpcomWillShutdown IO to complete"_ns, + getter_AddRefs(barrier))); + MOZ_RELEASE_ASSERT(barrier); + + // Add a blocker on the previous shutdown phase. + nsCOMPtr<nsIAsyncShutdownClient> client; + MOZ_TRY(barrier->GetClient(getter_AddRefs(client))); + + client->AddBlocker(sendTelemetryBlocker, FILE, __LINE__, + u"IOUtils::EventQueue::SetShutdownHooks"_ns); + + mBarriers[ShutdownPhase::XpcomWillShutdown] = std::move(barrier); + } + + return NS_OK; +} + +template <typename OkT, typename Fn> +RefPtr<IOUtils::IOPromise<OkT>> IOUtils::EventQueue::Dispatch(Fn aFunc) { + MOZ_RELEASE_ASSERT(mBackgroundEventTarget); + + auto promise = + MakeRefPtr<typename IOUtils::IOPromise<OkT>::Private>(__func__); + mBackgroundEventTarget->Dispatch( + NS_NewRunnableFunction("IOUtils::EventQueue::Dispatch", + [promise, func = std::move(aFunc)] { + Result<OkT, IOError> result = func(); + if (result.isErr()) { + promise->Reject(result.unwrapErr(), __func__); + } else { + promise->Resolve(result.unwrap(), __func__); + } + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + return promise; +}; + +Result<already_AddRefed<nsIAsyncShutdownBarrier>, nsresult> +IOUtils::EventQueue::GetShutdownBarrier(const IOUtils::ShutdownPhase aPhase) { + if (!mBarriers[aPhase]) { + return Err(NS_ERROR_NOT_AVAILABLE); + } + + return do_AddRef(mBarriers[aPhase]); +} + +Result<already_AddRefed<nsIAsyncShutdownClient>, nsresult> +IOUtils::EventQueue::GetShutdownClient(const IOUtils::ShutdownPhase aPhase) { + AssertHasShutdownClient(aPhase); + + if (!mBarriers[aPhase]) { + return Err(NS_ERROR_NOT_AVAILABLE); + } + + nsCOMPtr<nsIAsyncShutdownClient> client; + MOZ_TRY(mBarriers[aPhase]->GetClient(getter_AddRefs(client))); + + return do_AddRef(client); +} + +/* static */ +Result<nsTArray<uint8_t>, IOUtils::IOError> IOUtils::MozLZ4::Compress( + Span<const uint8_t> aUncompressed) { + nsTArray<uint8_t> result; + size_t worstCaseSize = + Compression::LZ4::maxCompressedSize(aUncompressed.Length()) + HEADER_SIZE; + if (!result.SetCapacity(worstCaseSize, fallible)) { + return Err(IOError(NS_ERROR_OUT_OF_MEMORY) + .WithMessage("Could not allocate buffer to compress data")); + } + result.AppendElements(Span(MAGIC_NUMBER.data(), MAGIC_NUMBER.size())); + std::array<uint8_t, sizeof(uint32_t)> contentSizeBytes{}; + LittleEndian::writeUint32(contentSizeBytes.data(), aUncompressed.Length()); + result.AppendElements(Span(contentSizeBytes.data(), contentSizeBytes.size())); + + if (aUncompressed.Length() == 0) { + // Don't try to compress an empty buffer. + // Just return the correctly formed header. + result.SetLength(HEADER_SIZE); + return result; + } + + size_t compressed = Compression::LZ4::compress( + reinterpret_cast<const char*>(aUncompressed.Elements()), + aUncompressed.Length(), + reinterpret_cast<char*>(result.Elements()) + HEADER_SIZE); + if (!compressed) { + return Err( + IOError(NS_ERROR_UNEXPECTED).WithMessage("Could not compress data")); + } + result.SetLength(HEADER_SIZE + compressed); + return result; +} + +/* static */ +Result<IOUtils::JsBuffer, IOUtils::IOError> IOUtils::MozLZ4::Decompress( + Span<const uint8_t> aFileContents, IOUtils::BufferKind aBufferKind) { + if (aFileContents.LengthBytes() < HEADER_SIZE) { + return Err( + IOError(NS_ERROR_FILE_CORRUPTED) + .WithMessage( + "Could not decompress file because the buffer is too short")); + } + auto header = aFileContents.To(HEADER_SIZE); + if (!std::equal(std::begin(MAGIC_NUMBER), std::end(MAGIC_NUMBER), + std::begin(header))) { + nsCString magicStr; + uint32_t i = 0; + for (; i < header.Length() - 1; ++i) { + magicStr.AppendPrintf("%02X ", header.at(i)); + } + magicStr.AppendPrintf("%02X", header.at(i)); + + return Err(IOError(NS_ERROR_FILE_CORRUPTED) + .WithMessage("Could not decompress file because it has an " + "invalid LZ4 header (wrong magic number: '%s')", + magicStr.get())); + } + size_t numBytes = sizeof(uint32_t); + Span<const uint8_t> sizeBytes = header.Last(numBytes); + uint32_t expectedDecompressedSize = + LittleEndian::readUint32(sizeBytes.data()); + if (expectedDecompressedSize == 0) { + return JsBuffer::CreateEmpty(aBufferKind); + } + auto contents = aFileContents.From(HEADER_SIZE); + auto result = JsBuffer::Create(aBufferKind, expectedDecompressedSize); + if (result.isErr()) { + return result.propagateErr(); + } + + JsBuffer decompressed = result.unwrap(); + size_t actualSize = 0; + if (!Compression::LZ4::decompress( + reinterpret_cast<const char*>(contents.Elements()), contents.Length(), + reinterpret_cast<char*>(decompressed.Elements()), + expectedDecompressedSize, &actualSize)) { + return Err( + IOError(NS_ERROR_FILE_CORRUPTED) + .WithMessage( + "Could not decompress file contents, the file may be corrupt")); + } + decompressed.SetLength(actualSize); + return decompressed; +} + +NS_IMPL_ISUPPORTS(IOUtilsShutdownBlocker, nsIAsyncShutdownBlocker, + nsIAsyncShutdownCompletionCallback); + +NS_IMETHODIMP IOUtilsShutdownBlocker::GetName(nsAString& aName) { + aName = u"IOUtils Blocker ("_ns; + aName.Append(PHASE_NAMES[mPhase]); + aName.Append(')'); + + return NS_OK; +} + +NS_IMETHODIMP IOUtilsShutdownBlocker::BlockShutdown( + nsIAsyncShutdownClient* aBarrierClient) { + using EventQueueStatus = IOUtils::EventQueueStatus; + using ShutdownPhase = IOUtils::ShutdownPhase; + + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIAsyncShutdownBarrier> barrier; + + { + auto state = IOUtils::sState.Lock(); + if (state->mQueueStatus == EventQueueStatus::Shutdown) { + // If the previous blockers have already run, then the event queue is + // already torn down and we have nothing to do. + + MOZ_RELEASE_ASSERT(mPhase == ShutdownPhase::XpcomWillShutdown); + MOZ_RELEASE_ASSERT(!state->mEventQueue); + + Unused << NS_WARN_IF(NS_FAILED(aBarrierClient->RemoveBlocker(this))); + mParentClient = nullptr; + + return NS_OK; + } + + MOZ_RELEASE_ASSERT(state->mEventQueue); + + mParentClient = aBarrierClient; + + barrier = state->mEventQueue->GetShutdownBarrier(mPhase).unwrapOr(nullptr); + } + + // We cannot barrier->Wait() while holding the mutex because it will lead to + // deadlock. + if (!barrier || NS_WARN_IF(NS_FAILED(barrier->Wait(this)))) { + // If we don't have a barrier, we still need to flush the IOUtils event + // queue and disable task submission. + // + // Likewise, if waiting on the barrier failed, we are going to make our best + // attempt to clean up. + Unused << Done(); + } + + return NS_OK; +} + +NS_IMETHODIMP IOUtilsShutdownBlocker::Done() { + using EventQueueStatus = IOUtils::EventQueueStatus; + using ShutdownPhase = IOUtils::ShutdownPhase; + + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + + bool didFlush = false; + + { + auto state = IOUtils::sState.Lock(); + + if (state->mEventQueue) { + MOZ_RELEASE_ASSERT(state->mQueueStatus == EventQueueStatus::Initialized); + + // This method is called once we have served all shutdown clients. Now we + // flush the remaining IO queue. This ensures any straggling IO that was + // not part of the shutdown blocker finishes before we move to the next + // phase. + state->mEventQueue->Dispatch<Ok>([]() { return Ok{}; }) + ->Then(GetMainThreadSerialEventTarget(), __func__, + [self = RefPtr(this)]() { self->OnFlush(); }); + + // And if we're the last shutdown phase to allow IO, disable the event + // queue to disallow further IO requests. + if (mPhase >= LAST_IO_PHASE) { + state->mQueueStatus = EventQueueStatus::Shutdown; + } + + didFlush = true; + } + } + + // If we have already shut down the event loop, then call OnFlush to stop + // blocking our parent shutdown client. + if (!didFlush) { + MOZ_RELEASE_ASSERT(mPhase == ShutdownPhase::XpcomWillShutdown); + OnFlush(); + } + + return NS_OK; +} + +void IOUtilsShutdownBlocker::OnFlush() { + if (mParentClient) { + (void)NS_WARN_IF(NS_FAILED(mParentClient->RemoveBlocker(this))); + mParentClient = nullptr; + + // If we are past the last shutdown phase that allows IO, + // we can shutdown the event queue here because no additional IO requests + // will be allowed (see |Done()|). + if (mPhase >= LAST_IO_PHASE) { + auto state = IOUtils::sState.Lock(); + if (state->mEventQueue) { + state->mEventQueue = nullptr; + } + } + } +} + +NS_IMETHODIMP IOUtilsShutdownBlocker::GetState(nsIPropertyBag** aState) { + return NS_OK; +} + +Result<IOUtils::InternalWriteOpts, IOUtils::IOError> +IOUtils::InternalWriteOpts::FromBinding(const WriteOptions& aOptions) { + InternalWriteOpts opts; + opts.mFlush = aOptions.mFlush; + opts.mMode = aOptions.mMode; + + if (aOptions.mBackupFile.WasPassed()) { + opts.mBackupFile = new nsLocalFile(); + if (nsresult rv = PathUtils::InitFileWithPath(opts.mBackupFile, + aOptions.mBackupFile.Value()); + NS_FAILED(rv)) { + return Err(IOUtils::IOError(rv).WithMessage( + "Could not parse path of backupFile (%s)", + NS_ConvertUTF16toUTF8(aOptions.mBackupFile.Value()).get())); + } + } + + if (aOptions.mTmpPath.WasPassed()) { + opts.mTmpFile = new nsLocalFile(); + if (nsresult rv = PathUtils::InitFileWithPath(opts.mTmpFile, + aOptions.mTmpPath.Value()); + NS_FAILED(rv)) { + return Err(IOUtils::IOError(rv).WithMessage( + "Could not parse path of temp file (%s)", + NS_ConvertUTF16toUTF8(aOptions.mTmpPath.Value()).get())); + } + } + + opts.mCompress = aOptions.mCompress; + return opts; +} + +/* static */ +Result<IOUtils::JsBuffer, IOUtils::IOError> IOUtils::JsBuffer::Create( + IOUtils::BufferKind aBufferKind, size_t aCapacity) { + JsBuffer buffer(aBufferKind, aCapacity); + if (aCapacity != 0 && !buffer.mBuffer) { + return Err(IOError(NS_ERROR_OUT_OF_MEMORY) + .WithMessage("Could not allocate buffer")); + } + return buffer; +} + +/* static */ +IOUtils::JsBuffer IOUtils::JsBuffer::CreateEmpty( + IOUtils::BufferKind aBufferKind) { + JsBuffer buffer(aBufferKind, 0); + MOZ_RELEASE_ASSERT(buffer.mBuffer == nullptr); + return buffer; +} + +IOUtils::JsBuffer::JsBuffer(IOUtils::BufferKind aBufferKind, size_t aCapacity) + : mBufferKind(aBufferKind), mCapacity(aCapacity), mLength(0) { + if (mCapacity) { + if (aBufferKind == BufferKind::String) { + mBuffer = JS::UniqueChars( + js_pod_arena_malloc<char>(js::StringBufferArena, mCapacity)); + } else { + MOZ_RELEASE_ASSERT(aBufferKind == BufferKind::Uint8Array); + mBuffer = JS::UniqueChars( + js_pod_arena_malloc<char>(js::ArrayBufferContentsArena, mCapacity)); + } + } +} + +IOUtils::JsBuffer::JsBuffer(IOUtils::JsBuffer&& aOther) noexcept + : mBufferKind(aOther.mBufferKind), + mCapacity(aOther.mCapacity), + mLength(aOther.mLength), + mBuffer(std::move(aOther.mBuffer)) { + aOther.mCapacity = 0; + aOther.mLength = 0; +} + +IOUtils::JsBuffer& IOUtils::JsBuffer::operator=( + IOUtils::JsBuffer&& aOther) noexcept { + mBufferKind = aOther.mBufferKind; + mCapacity = aOther.mCapacity; + mLength = aOther.mLength; + mBuffer = std::move(aOther.mBuffer); + + // Invalidate aOther. + aOther.mCapacity = 0; + aOther.mLength = 0; + + return *this; +} + +/* static */ +JSString* IOUtils::JsBuffer::IntoString(JSContext* aCx, JsBuffer aBuffer) { + MOZ_RELEASE_ASSERT(aBuffer.mBufferKind == IOUtils::BufferKind::String); + + if (!aBuffer.mCapacity) { + return JS_GetEmptyString(aCx); + } + + if (IsAscii(aBuffer.BeginReading())) { + // If the string is just plain ASCII, then we can hand the buffer off to + // JavaScript as a Latin1 string (since ASCII is a subset of Latin1). + JS::UniqueLatin1Chars asLatin1( + reinterpret_cast<JS::Latin1Char*>(aBuffer.mBuffer.release())); + return JS_NewLatin1String(aCx, std::move(asLatin1), aBuffer.mLength); + } + + const char* ptr = aBuffer.mBuffer.get(); + size_t length = aBuffer.mLength; + + // Strip off a leading UTF-8 byte order marker (BOM) if found. + if (length >= 3 && Substring(ptr, 3) == "\xEF\xBB\xBF"_ns) { + ptr += 3; + length -= 3; + } + + // If the string is encodable as Latin1, we need to deflate the string to a + // Latin1 string to account for UTF-8 characters that are encoded as more than + // a single byte. + // + // Otherwise, the string contains characters outside Latin1 so we have to + // inflate to UTF-16. + return JS_NewStringCopyUTF8N(aCx, JS::UTF8Chars(ptr, length)); +} + +/* static */ +JSObject* IOUtils::JsBuffer::IntoUint8Array(JSContext* aCx, JsBuffer aBuffer) { + MOZ_RELEASE_ASSERT(aBuffer.mBufferKind == IOUtils::BufferKind::Uint8Array); + + if (!aBuffer.mCapacity) { + return JS_NewUint8Array(aCx, 0); + } + + MOZ_RELEASE_ASSERT(aBuffer.mBuffer); + JS::Rooted<JSObject*> arrayBuffer( + aCx, JS::NewArrayBufferWithContents(aCx, aBuffer.mLength, + std::move(aBuffer.mBuffer))); + + if (!arrayBuffer) { + // aBuffer will be destructed at end of scope, but its destructor does not + // take into account |mCapacity| or |mLength|, so it is OK for them to be + // non-zero here with a null |mBuffer|. + return nullptr; + } + + return JS_NewUint8ArrayWithBuffer(aCx, arrayBuffer, 0, aBuffer.mLength); +} + +[[nodiscard]] bool ToJSValue(JSContext* aCx, IOUtils::JsBuffer&& aBuffer, + JS::MutableHandle<JS::Value> aValue) { + if (aBuffer.mBufferKind == IOUtils::BufferKind::String) { + JSString* str = IOUtils::JsBuffer::IntoString(aCx, std::move(aBuffer)); + if (!str) { + return false; + } + + aValue.setString(str); + return true; + } + + JSObject* array = IOUtils::JsBuffer::IntoUint8Array(aCx, std::move(aBuffer)); + if (!array) { + return false; + } + + aValue.setObject(*array); + return true; +} + +// SyncReadFile + +NS_IMPL_CYCLE_COLLECTING_ADDREF(SyncReadFile) +NS_IMPL_CYCLE_COLLECTING_RELEASE(SyncReadFile) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SyncReadFile) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(SyncReadFile, mParent) + +SyncReadFile::SyncReadFile(nsISupports* aParent, + RefPtr<nsFileRandomAccessStream>&& aStream, + int64_t aSize) + : mParent(aParent), mStream(std::move(aStream)), mSize(aSize) { + MOZ_RELEASE_ASSERT(mSize >= 0); +} + +SyncReadFile::~SyncReadFile() = default; + +JSObject* SyncReadFile::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return SyncReadFile_Binding::Wrap(aCx, this, aGivenProto); +} + +void SyncReadFile::ReadBytesInto(const Uint8Array& aDestArray, + const int64_t aOffset, ErrorResult& aRv) { + if (!mStream) { + return aRv.ThrowOperationError("SyncReadFile is closed"); + } + + aDestArray.ProcessFixedData([&](const Span<uint8_t>& aData) { + auto rangeEnd = CheckedInt64(aOffset) + aData.Length(); + if (!rangeEnd.isValid()) { + return aRv.ThrowOperationError("Requested range overflows i64"); + } + + if (rangeEnd.value() > mSize) { + return aRv.ThrowOperationError( + "Requested range overflows SyncReadFile size"); + } + + size_t readLen{aData.Length()}; + if (readLen == 0) { + return; + } + + if (nsresult rv = mStream->Seek(PR_SEEK_SET, aOffset); NS_FAILED(rv)) { + return aRv.ThrowOperationError( + FormatErrorMessage(rv, "Could not seek to position %lld", aOffset)); + } + + Span<char> toRead = AsWritableChars(aData); + + size_t totalRead = 0; + while (totalRead != readLen) { + // Read no more than INT32_MAX on each call to mStream->Read, + // otherwise it returns an error. + uint32_t bytesToReadThisChunk = + std::min(readLen - totalRead, size_t(INT32_MAX)); + + uint32_t bytesRead = 0; + if (nsresult rv = mStream->Read(toRead.Elements(), bytesToReadThisChunk, + &bytesRead); + NS_FAILED(rv)) { + return aRv.ThrowOperationError(FormatErrorMessage( + rv, "Encountered an unexpected error while reading file stream")); + } + if (bytesRead == 0) { + return aRv.ThrowOperationError( + "Reading stopped before the entire array was filled"); + } + totalRead += bytesRead; + toRead = toRead.From(bytesRead); + } + }); +} + +void SyncReadFile::Close() { mStream = nullptr; } + +#ifdef XP_UNIX +namespace { + +static nsCString FromUnixString(const IOUtils::UnixString& aString) { + if (aString.IsUTF8String()) { + return aString.GetAsUTF8String(); + } + if (aString.IsUint8Array()) { + nsCString data; + Unused << aString.GetAsUint8Array().AppendDataTo(data); + return data; + } + MOZ_CRASH("unreachable"); +} + +} // namespace + +// static +uint32_t IOUtils::LaunchProcess(GlobalObject& aGlobal, + const Sequence<UnixString>& aArgv, + const LaunchOptions& aOptions, + ErrorResult& aRv) { + // The binding is worker-only, so should always be off-main-thread. + MOZ_ASSERT(!NS_IsMainThread()); + + // This generally won't work in child processes due to sandboxing. + AssertParentProcessWithCallerLocation(aGlobal); + + std::vector<std::string> argv; + base::LaunchOptions options; + + for (const auto& arg : aArgv) { + argv.push_back(FromUnixString(arg).get()); + } + + size_t envLen = aOptions.mEnvironment.Length(); + base::EnvironmentArray envp(new char*[envLen + 1]); + for (size_t i = 0; i < envLen; ++i) { + // EnvironmentArray is a UniquePtr instance which will `free` + // these strings. + envp[i] = strdup(FromUnixString(aOptions.mEnvironment[i]).get()); + } + envp[envLen] = nullptr; + options.full_env = std::move(envp); + + if (aOptions.mWorkdir.WasPassed()) { + options.workdir = FromUnixString(aOptions.mWorkdir.Value()).get(); + } + + if (aOptions.mFdMap.WasPassed()) { + for (const auto& fdItem : aOptions.mFdMap.Value()) { + options.fds_to_remap.push_back({fdItem.mSrc, fdItem.mDst}); + } + } + +# ifdef XP_MACOSX + options.disclaim = aOptions.mDisclaim; +# endif + + base::ProcessHandle pid; + static_assert(sizeof(pid) <= sizeof(uint32_t), + "WebIDL long should be large enough for a pid"); + Result<Ok, mozilla::ipc::LaunchError> err = + base::LaunchApp(argv, std::move(options), &pid); + if (err.isErr()) { + aRv.Throw(NS_ERROR_FAILURE); + return 0; + } + + MOZ_ASSERT(pid >= 0); + return static_cast<uint32_t>(pid); +} +#endif // XP_UNIX + +} // namespace mozilla::dom + +#undef REJECT_IF_INIT_PATH_FAILED diff --git a/dom/system/IOUtils.h b/dom/system/IOUtils.h new file mode 100644 index 0000000000..82ea30eaa8 --- /dev/null +++ b/dom/system/IOUtils.h @@ -0,0 +1,925 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_IOUtils__ +#define mozilla_dom_IOUtils__ + +#include "js/Utility.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Attributes.h" +#include "mozilla/Buffer.h" +#include "mozilla/DataMutex.h" +#include "mozilla/MozPromise.h" +#include "mozilla/Result.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/IOUtilsBinding.h" +#include "mozilla/dom/TypedArray.h" +#include "nsIAsyncShutdown.h" +#include "nsIFile.h" +#include "nsISerialEventTarget.h" +#include "nsPrintfCString.h" +#include "nsProxyRelease.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsTArray.h" +#include "prio.h" + +class nsFileRandomAccessStream; + +namespace mozilla { + +/** + * Utility class to be used with |UniquePtr| to automatically close NSPR file + * descriptors when they go out of scope. + * + * Example: + * + * UniquePtr<PRFileDesc, PR_CloseDelete> fd = PR_Open(path, flags, mode); + */ +class PR_CloseDelete { + public: + constexpr PR_CloseDelete() = default; + PR_CloseDelete(const PR_CloseDelete& aOther) = default; + PR_CloseDelete(PR_CloseDelete&& aOther) = default; + PR_CloseDelete& operator=(const PR_CloseDelete& aOther) = default; + PR_CloseDelete& operator=(PR_CloseDelete&& aOther) = default; + + void operator()(PRFileDesc* aPtr) const { PR_Close(aPtr); } +}; + +namespace dom { + +/** + * Implementation for the Web IDL interface at dom/chrome-webidl/IOUtils.webidl. + * Methods of this class must only be called from the parent process. + */ +class IOUtils final { + public: + class IOError; + + enum class ShutdownPhase : uint8_t { + ProfileBeforeChange, + SendTelemetry, + XpcomWillShutdown, + Count, + }; + + template <typename T> + using PhaseArray = + EnumeratedArray<IOUtils::ShutdownPhase, IOUtils::ShutdownPhase::Count, T>; + + static already_AddRefed<Promise> Read(GlobalObject& aGlobal, + const nsAString& aPath, + const ReadOptions& aOptions, + ErrorResult& aError); + + static already_AddRefed<Promise> ReadUTF8(GlobalObject& aGlobal, + const nsAString& aPath, + const ReadUTF8Options& aOptions, + ErrorResult& aError); + + static already_AddRefed<Promise> ReadJSON(GlobalObject& aGlobal, + const nsAString& aPath, + const ReadUTF8Options& aOptions, + ErrorResult& aError); + + static already_AddRefed<Promise> Write(GlobalObject& aGlobal, + const nsAString& aPath, + const Uint8Array& aData, + const WriteOptions& aOptions, + ErrorResult& aError); + + static already_AddRefed<Promise> WriteUTF8(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aString, + const WriteOptions& aOptions, + ErrorResult& aError); + + static already_AddRefed<Promise> WriteJSON(GlobalObject& aGlobal, + const nsAString& aPath, + JS::Handle<JS::Value> aValue, + const WriteOptions& aOptions, + ErrorResult& aError); + + static already_AddRefed<Promise> Move(GlobalObject& aGlobal, + const nsAString& aSourcePath, + const nsAString& aDestPath, + const MoveOptions& aOptions, + ErrorResult& aError); + + static already_AddRefed<Promise> Remove(GlobalObject& aGlobal, + const nsAString& aPath, + const RemoveOptions& aOptions, + ErrorResult& aError); + + static already_AddRefed<Promise> MakeDirectory( + GlobalObject& aGlobal, const nsAString& aPath, + const MakeDirectoryOptions& aOptions, ErrorResult& aError); + + static already_AddRefed<Promise> Stat(GlobalObject& aGlobal, + const nsAString& aPath, + ErrorResult& aError); + + static already_AddRefed<Promise> Copy(GlobalObject& aGlobal, + const nsAString& aSourcePath, + const nsAString& aDestPath, + const CopyOptions& aOptions, + ErrorResult& aError); + + static already_AddRefed<Promise> SetAccessTime( + GlobalObject& aGlobal, const nsAString& aPath, + const Optional<int64_t>& aAccess, ErrorResult& aError); + + static already_AddRefed<Promise> SetModificationTime( + GlobalObject& aGlobal, const nsAString& aPath, + const Optional<int64_t>& aModification, ErrorResult& aError); + + private: + using SetTimeFn = decltype(&nsIFile::SetLastAccessedTime); + + static_assert( + std::is_same_v<SetTimeFn, decltype(&nsIFile::SetLastModifiedTime)>); + + static already_AddRefed<Promise> SetTime(GlobalObject& aGlobal, + const nsAString& aPath, + const Optional<int64_t>& aNewTime, + SetTimeFn aSetTimeFn, + ErrorResult& aError); + + public: + static already_AddRefed<Promise> GetChildren( + GlobalObject& aGlobal, const nsAString& aPath, + const GetChildrenOptions& aOptions, ErrorResult& aError); + + static already_AddRefed<Promise> SetPermissions(GlobalObject& aGlobal, + const nsAString& aPath, + uint32_t aPermissions, + const bool aHonorUmask, + ErrorResult& aError); + + static already_AddRefed<Promise> Exists(GlobalObject& aGlobal, + const nsAString& aPath, + ErrorResult& aError); + + static already_AddRefed<Promise> CreateUniqueFile(GlobalObject& aGlobal, + const nsAString& aParent, + const nsAString& aPrefix, + const uint32_t aPermissions, + ErrorResult& aError); + static already_AddRefed<Promise> CreateUniqueDirectory( + GlobalObject& aGlobal, const nsAString& aParent, const nsAString& aPrefix, + const uint32_t aPermissions, ErrorResult& aError); + + private: + /** + * A helper method for CreateUniqueFile and CreateUniqueDirectory. + */ + static already_AddRefed<Promise> CreateUnique(GlobalObject& aGlobal, + const nsAString& aParent, + const nsAString& aPrefix, + const uint32_t aFileType, + const uint32_t aPermissions, + ErrorResult& aError); + + public: + static already_AddRefed<Promise> ComputeHexDigest( + GlobalObject& aGlobal, const nsAString& aPath, + const HashAlgorithm aAlgorithm, ErrorResult& aError); + +#if defined(XP_WIN) + static already_AddRefed<Promise> GetWindowsAttributes(GlobalObject& aGlobal, + const nsAString& aPath, + ErrorResult& aError); + + static already_AddRefed<Promise> SetWindowsAttributes( + GlobalObject& aGlobal, const nsAString& aPath, + const mozilla::dom::WindowsFileAttributes& aAttrs, ErrorResult& aError); +#elif defined(XP_MACOSX) + static already_AddRefed<Promise> HasMacXAttr(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aAttr, + ErrorResult& aError); + static already_AddRefed<Promise> GetMacXAttr(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aAttr, + ErrorResult& aError); + static already_AddRefed<Promise> SetMacXAttr(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aAttr, + const Uint8Array& aValue, + ErrorResult& aError); + static already_AddRefed<Promise> DelMacXAttr(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aAttr, + ErrorResult& aError); +#endif + +#ifdef XP_UNIX + using UnixString = OwningUTF8StringOrUint8Array; + static uint32_t LaunchProcess(GlobalObject& aGlobal, + const Sequence<UnixString>& aArgv, + const LaunchOptions& aOptions, + ErrorResult& aRv); +#endif + + static already_AddRefed<Promise> GetFile( + GlobalObject& aGlobal, const Sequence<nsString>& aComponents, + ErrorResult& aError); + + static already_AddRefed<Promise> GetDirectory( + GlobalObject& aGlobal, const Sequence<nsString>& aComponents, + ErrorResult& aError); + + static void GetProfileBeforeChange(GlobalObject& aGlobal, + JS::MutableHandle<JS::Value>, + ErrorResult& aRv); + + static void GetSendTelemetry(GlobalObject& aGlobal, + JS::MutableHandle<JS::Value>, ErrorResult& aRv); + + static RefPtr<SyncReadFile> OpenFileForSyncReading(GlobalObject& aGlobal, + const nsAString& aPath, + ErrorResult& aRv); + + class JsBuffer; + + /** + * The kind of buffer to allocate. + * + * This controls what kind of JS object (a JSString or a Uint8Array) is + * returned by |ToJSValue()|. + */ + enum class BufferKind { + String, + Uint8Array, + }; + + private: + ~IOUtils() = default; + + template <typename T> + using IOPromise = MozPromise<T, IOError, true>; + + friend class IOUtilsShutdownBlocker; + struct InternalFileInfo; + struct InternalWriteOpts; + class MozLZ4; + class EventQueue; + class State; + + template <typename Fn> + static already_AddRefed<Promise> WithPromiseAndState(GlobalObject& aGlobal, + ErrorResult& aError, + Fn aFn); + + /** + * Dispatch a task on the event queue and resolve or reject the associated + * promise based on the result. + * + * NB: If the calling thread is a worker, this function takes care of keepting + * it alive until the |IOPromise| can complete. + * + * @param aPromise The promise corresponding to the task running on the event + * queue. + * @param aFunc The task to run. + */ + template <typename OkT, typename Fn> + static void DispatchAndResolve(EventQueue* aQueue, Promise* aPromise, + Fn aFunc); + + /** + * Creates a new JS Promise. + * + * @return The new promise, or |nullptr| on failure. + */ + static already_AddRefed<Promise> CreateJSPromise(GlobalObject& aGlobal, + ErrorResult& aError); + + // Allow conversion of |InternalFileInfo| with |ToJSValue|. + friend bool ToJSValue(JSContext* aCx, + const InternalFileInfo& aInternalFileInfo, + JS::MutableHandle<JS::Value> aValue); + + /** + * Attempts to read the entire file at |aPath| into a buffer. + * + * @param aFile The location of the file. + * @param aOffset The offset to start reading from. + * @param aMaxBytes If |Some|, then only read up this this number of bytes, + * otherwise attempt to read the whole file. + * @param aDecompress If true, decompress the bytes read from disk before + * returning the result to the caller. + * @param aBufferKind The kind of buffer to allocate. + * + * @return A buffer containing the entire (decompressed) file contents, or an + * error. + */ + static Result<JsBuffer, IOError> ReadSync(nsIFile* aFile, + const uint64_t aOffset, + const Maybe<uint32_t> aMaxBytes, + const bool aDecompress, + BufferKind aBufferKind); + + /** + * Attempts to read the entire file at |aPath| as a UTF-8 string. + * + * @param aFile The location of the file. + * @param aDecompress If true, decompress the bytes read from disk before + * returning the result to the caller. + * + * @return The (decompressed) contents of the file re-encoded as a UTF-16 + * string. + */ + static Result<JsBuffer, IOError> ReadUTF8Sync(nsIFile* aFile, + const bool aDecompress); + + /** + * Attempt to write the entirety of |aByteArray| to the file at |aPath|. + * This may occur by writing to an intermediate destination and performing a + * move, depending on |aOptions|. + * + * @param aFile The location of the file. + * @param aByteArray The data to write to the file. + * @param aOptions Options to modify the way the write is completed. + * + * @return The number of bytes written to the file, or an error if the write + * failed or was incomplete. + */ + static Result<uint32_t, IOError> WriteSync( + nsIFile* aFile, const Span<const uint8_t>& aByteArray, + const InternalWriteOpts& aOptions); + + /** + * Attempts to move the file located at |aSourceFile| to |aDestFile|. + * + * @param aSourceFile The location of the file to move. + * @param aDestFile The destination for the file. + * @param aNoOverWrite If true, abort with an error if a file already exists + * at |aDestFile|. Otherwise, the file will be overwritten by the move. + * + * @return Ok if the file was moved successfully, or an error. + */ + static Result<Ok, IOError> MoveSync(nsIFile* aSourceFile, nsIFile* aDestFile, + bool aNoOverwrite); + + /** + * Attempts to copy the file at |aSourceFile| to |aDestFile|. + * + * @param aSourceFile The location of the file to copy. + * @param aDestFile The destination that the file will be copied to. + * + * @return Ok if the operation was successful, or an error. + */ + static Result<Ok, IOError> CopySync(nsIFile* aSourceFile, nsIFile* aDestFile, + bool aNoOverWrite, bool aRecursive); + + /** + * Provides the implementation for |CopySync| and |MoveSync|. + * + * @param aMethod A pointer to one of |nsIFile::MoveTo| or |CopyTo| + * instance methods. + * @param aMethodName The name of the method to the performed. Either "move" + * or "copy". + * @param aSource The source file to be copied or moved. + * @param aDest The destination file. + * @param aNoOverwrite If true, allow overwriting |aDest| during the copy or + * move. Otherwise, abort with an error if the file would + * be overwritten. + * + * @return Ok if the operation was successful, or an error. + */ + template <typename CopyOrMoveFn> + static Result<Ok, IOError> CopyOrMoveSync(CopyOrMoveFn aMethod, + const char* aMethodName, + nsIFile* aSource, nsIFile* aDest, + bool aNoOverwrite); + + /** + * Attempts to remove the file located at |aFile|. + * + * @param aFile The location of the file. + * @param aIgnoreAbsent If true, suppress errors due to an absent target + * file. + * @param aRecursive If true, attempt to recursively remove descendant + * files. This option is safe to use even if the target + * is not a directory. + * @param aRetryReadonly Retry a delete that failed with a NotAllowedError by + * first removing the readonly attribute. Only has an + * effect on Windows. + * + * @return Ok if the file was removed successfully, or an error. + */ + static Result<Ok, IOError> RemoveSync(nsIFile* aFile, bool aIgnoreAbsent, + bool aRecursive, bool aRetryReadonly); + + /** + * Attempts to create a new directory at |aFile|. + * + * @param aFile The location of the directory to create. + * @param aCreateAncestors If true, create missing ancestor directories as + * needed. Otherwise, report an error if the target + * has non-existing ancestor directories. + * @param aIgnoreExisting If true, suppress errors that occur if the target + * directory already exists. Otherwise, propagate the + * error if it occurs. + * @param aMode Optional file mode. Defaults to 0777 to allow the + * system umask to compute the best mode for the new + * directory. + * + * @return Ok if the directory was created successfully, or an error. + */ + static Result<Ok, IOError> MakeDirectorySync(nsIFile* aFile, + bool aCreateAncestors, + bool aIgnoreExisting, + int32_t aMode = 0777); + + /** + * Attempts to stat a file at |aFile|. + * + * @param aFile The location of the file. + * + * @return An |InternalFileInfo| struct if successful, or an error. + */ + static Result<IOUtils::InternalFileInfo, IOError> StatSync(nsIFile* aFile); + + /** + * Attempts to update the last access or modification time of the file at + * |aFile|. + * + * @param aFile The location of the file. + * @param SetTimeFn A member function pointer to either + * nsIFile::SetLastAccessedTime or + * nsIFile::SetLastModifiedTime. + * @param aNewTime Some value in milliseconds since Epoch. + * + * @return Timestamp of the file if the operation was successful, or an error. + */ + static Result<int64_t, IOError> SetTimeSync(nsIFile* aFile, + SetTimeFn aSetTimeFn, + int64_t aNewTime); + + /** + * Returns the immediate children of the directory at |aFile|, if any. + * + * @param aFile The location of the directory. + * + * @return An array of absolute paths identifying the children of |aFile|. + * If there are no children, an empty array. Otherwise, an error. + */ + static Result<nsTArray<nsString>, IOError> GetChildrenSync( + nsIFile* aFile, bool aIgnoreAbsent); + + /** + * Set the permissions of the given file. + * + * Windows does not make a distinction between user, group, and other + * permissions like UNICES do. If a permission flag is set for any of user, + * group, or other has a permission, then all users will have that + * permission. + * + * @param aFile The location of the file. + * @param aPermissions The permissions to set, as a UNIX file mode. + * + * @return |Ok| if the permissions were successfully set, or an error. + */ + static Result<Ok, IOError> SetPermissionsSync(nsIFile* aFile, + const uint32_t aPermissions); + + /** + * Return whether or not the file exists. + * + * @param aFile The location of the file. + * + * @return Whether or not the file exists. + */ + static Result<bool, IOError> ExistsSync(nsIFile* aFile); + + /** + * Create a file or directory with a unique path. + * + * @param aFile The location of the file or directory (including prefix) + * @param aFileType One of |nsIFile::NORMAL_FILE_TYPE| or + * |nsIFile::DIRECTORY_TYPE|. + * @param aperms The permissions to create the file or directory with. + * + * @return A unique path. + */ + static Result<nsString, IOError> CreateUniqueSync( + nsIFile* aFile, const uint32_t aFileType, const uint32_t aPermissions); + + /** + * Compute the hash of a file. + * + * @param aFile The file to hash. + * @param aAlgorithm The hashing algorithm to use. + * + * @return The hash of the file, as a hex digest. + */ + static Result<nsCString, IOError> ComputeHexDigestSync( + nsIFile* aFile, const HashAlgorithm aAlgorithm); + +#if defined(XP_WIN) + /** + * Return the Windows-specific attributes of the file. + * + * @param aFile The location of the file. + * + * @return The Windows-specific attributes of the file. + */ + static Result<uint32_t, IOError> GetWindowsAttributesSync(nsIFile* aFile); + + /** + * Set the Windows-specific attributes of the file. + * + * @param aFile The location of the file. + * @param aAttrs The attributes to set on the file. + * + * @return |Ok| if the attributes were successfully set, or an error. + */ + static Result<Ok, IOError> SetWindowsAttributesSync( + nsIFile* aFile, const uint32_t aSetAttrs, const uint32_t aClearAttrs); +#elif defined(XP_MACOSX) + static Result<bool, IOError> HasMacXAttrSync(nsIFile* aFile, + const nsCString& aAttr); + static Result<nsTArray<uint8_t>, IOError> GetMacXAttrSync( + nsIFile* aFile, const nsCString& aAttr); + static Result<Ok, IOError> SetMacXAttrSync(nsIFile* aFile, + const nsCString& aAttr, + const nsTArray<uint8_t>& aValue); + static Result<Ok, IOError> DelMacXAttrSync(nsIFile* aFile, + const nsCString& aAttr); +#endif + + static void GetShutdownClient(GlobalObject& aGlobal, + JS::MutableHandle<JS::Value> aClient, + ErrorResult& aRv, const ShutdownPhase aPhase); + + enum class EventQueueStatus { + Uninitialized, + Initialized, + Shutdown, + }; + + enum class ShutdownBlockerStatus { + Uninitialized, + Initialized, + Failed, + }; + + /** + * Internal IOUtils state. + */ + class State { + public: + StaticAutoPtr<EventQueue> mEventQueue; + EventQueueStatus mQueueStatus = EventQueueStatus::Uninitialized; + ShutdownBlockerStatus mBlockerStatus = ShutdownBlockerStatus::Uninitialized; + + /** + * Set up shutdown hooks to free our internals at shutdown. + * + * NB: Must be called on main thread. + */ + void SetShutdownHooks(); + }; + + using StateMutex = StaticDataMutex<State>; + + /** + * Lock the state mutex and return a handle. If shutdown has not yet + * finished, the internals will be constructed if necessary. + * + * @returns A handle to the internal state, which can be used to retrieve the + * event queue. + * If |Some| is returned, |mEventQueue| is guaranteed to be + * initialized. If shutdown has finished, |Nothing| is returned. + */ + static Maybe<StateMutex::AutoLock> GetState(); + + static StateMutex sState; +}; + +/** + * The IOUtils event queue. + */ +class IOUtils::EventQueue final { + friend void IOUtils::State::SetShutdownHooks(); + + public: + EventQueue(); + + EventQueue(const EventQueue&) = delete; + EventQueue(EventQueue&&) = delete; + EventQueue& operator=(const EventQueue&) = delete; + EventQueue& operator=(EventQueue&&) = delete; + + /** + * Dispatch a task on the event queue. + * + * NB: If using this directly from |IOUtils| instead of + * |IOUtils::DispatchAndResolve| *and* the calling thread is a worker, you + * *must* take care to keep the worker thread alive until the |IOPromise| + * resolves or rejects. See the implementation of + * |IOUtils::DispatchAndResolve| or |IOUtils::GetWindowsAttributes| for an + * example. + * + * @param aFunc The task to dispatch on the event queue. + * + * @return A promise that resolves to the task's return value or rejects with + * an error. + */ + template <typename OkT, typename Fn> + RefPtr<IOPromise<OkT>> Dispatch(Fn aFunc); + + Result<already_AddRefed<nsIAsyncShutdownBarrier>, nsresult> + GetShutdownBarrier(const ShutdownPhase aPhase); + Result<already_AddRefed<nsIAsyncShutdownClient>, nsresult> GetShutdownClient( + const ShutdownPhase aPhase); + + private: + nsresult SetShutdownHooks(); + + nsCOMPtr<nsISerialEventTarget> mBackgroundEventTarget; + IOUtils::PhaseArray<nsCOMPtr<nsIAsyncShutdownBarrier>> mBarriers; +}; + +/** + * An error class used with the |Result| type returned by most private |IOUtils| + * methods. + */ +class IOUtils::IOError { + public: + MOZ_IMPLICIT IOError(nsresult aCode) : mCode(aCode), mMessage(Nothing()) {} + + /** + * Replaces the message associated with this error. + */ + template <typename... Args> + IOError WithMessage(const char* const aMessage, Args... aArgs) { + mMessage.emplace(nsPrintfCString(aMessage, aArgs...)); + return *this; + } + IOError WithMessage(const char* const aMessage) { + mMessage.emplace(nsCString(aMessage)); + return *this; + } + IOError WithMessage(const nsCString& aMessage) { + mMessage.emplace(aMessage); + return *this; + } + + /** + * Returns the |nsresult| associated with this error. + */ + nsresult Code() const { return mCode; } + + /** + * Maybe returns a message associated with this error. + */ + const Maybe<nsCString>& Message() const { return mMessage; } + + private: + nsresult mCode; + Maybe<nsCString> mMessage; +}; + +/** + * This is an easier to work with representation of a |mozilla::dom::FileInfo| + * for private use in the IOUtils implementation. + * + * Because web IDL dictionaries are not easily copy/moveable, this class is + * used instead, until converted to the proper |mozilla::dom::FileInfo| before + * returning any results to JavaScript. + */ +struct IOUtils::InternalFileInfo { + nsString mPath; + FileType mType = FileType::Other; + uint64_t mSize = 0; + Maybe<PRTime> mCreationTime; // In ms since epoch. + PRTime mLastAccessed = 0; // In ms since epoch. + PRTime mLastModified = 0; // In ms since epoch. + uint32_t mPermissions = 0; +}; + +/** + * This is an easier to work with representation of a + * |mozilla::dom::WriteOptions| for private use in the |IOUtils| + * implementation. + * + * Because web IDL dictionaries are not easily copy/moveable, this class is + * used instead. + */ +struct IOUtils::InternalWriteOpts { + RefPtr<nsIFile> mBackupFile; + RefPtr<nsIFile> mTmpFile; + WriteMode mMode; + bool mFlush = false; + bool mCompress = false; + + static Result<InternalWriteOpts, IOUtils::IOError> FromBinding( + const WriteOptions& aOptions); +}; + +/** + * Re-implements the file compression and decompression utilities found + * in toolkit/components/lz4/lz4.js + * + * This implementation uses the non-standard data layout: + * + * - MAGIC_NUMBER (8 bytes) + * - content size (uint32_t, little endian) + * - content, as obtained from mozilla::Compression::LZ4::compress + * + * See bug 1209390 for more info. + */ +class IOUtils::MozLZ4 { + public: + static constexpr std::array<uint8_t, 8> MAGIC_NUMBER{ + {'m', 'o', 'z', 'L', 'z', '4', '0', '\0'}}; + + static const uint32_t HEADER_SIZE = 8 + sizeof(uint32_t); + + /** + * Compresses |aUncompressed| byte array, and returns a byte array with the + * correct format whose contents may be written to disk. + */ + static Result<nsTArray<uint8_t>, IOError> Compress( + Span<const uint8_t> aUncompressed); + + /** + * Checks |aFileContents| for the correct file header, and returns the + * decompressed content. + */ + static Result<IOUtils::JsBuffer, IOError> Decompress( + Span<const uint8_t> aFileContents, IOUtils::BufferKind); +}; + +class IOUtilsShutdownBlocker : public nsIAsyncShutdownBlocker, + public nsIAsyncShutdownCompletionCallback { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIASYNCSHUTDOWNBLOCKER + NS_DECL_NSIASYNCSHUTDOWNCOMPLETIONCALLBACK + + explicit IOUtilsShutdownBlocker(const IOUtils::ShutdownPhase aPhase) + : mPhase(aPhase) {} + + private: + virtual ~IOUtilsShutdownBlocker() = default; + + /** + * Called on the main thread after the event queue has been flushed. + */ + void OnFlush(); + + static constexpr IOUtils::PhaseArray<const char16_t*> PHASE_NAMES{ + u"profile-before-change", + u"profile-before-change-telemetry", + u"xpcom-will-shutdown", + }; + + // The last shutdown phase before we should shut down the event loop. + static constexpr auto LAST_IO_PHASE = IOUtils::ShutdownPhase::SendTelemetry; + + IOUtils::ShutdownPhase mPhase; + nsCOMPtr<nsIAsyncShutdownClient> mParentClient; +}; + +/** + * A buffer that is allocated inside one of JS heaps so that it can be converted + * to a JSString or Uint8Array object with at most one copy in the worst case. + */ +class IOUtils::JsBuffer final { + public: + /** + * Create a new buffer of the given kind with the requested capacity. + * + * @param aBufferKind The kind of buffer to create (either a string or an + * array). + * @param aCapacity The capacity of the buffer. + * + * @return Either a successfully created buffer or an error if it could not be + * allocated. + */ + static Result<JsBuffer, IOUtils::IOError> Create( + IOUtils::BufferKind aBufferKind, size_t aCapacity); + + /** + * Create a new, empty buffer. + * + * This operation cannot fail. + * + * @param aBufferKind The kind of buffer to create (either a string or an + * array). + * + * @return An empty JsBuffer. + */ + static JsBuffer CreateEmpty(IOUtils::BufferKind aBufferKind); + + JsBuffer(const JsBuffer&) = delete; + JsBuffer(JsBuffer&& aOther) noexcept; + JsBuffer& operator=(const JsBuffer&) = delete; + JsBuffer& operator=(JsBuffer&& aOther) noexcept; + + size_t Length() { return mLength; } + char* Elements() { return mBuffer.get(); } + void SetLength(size_t aNewLength) { + MOZ_RELEASE_ASSERT(aNewLength <= mCapacity); + mLength = aNewLength; + } + + /** + * Return a span for writing to the buffer. + * + * |SetLength| should be called after the buffer has been written to. + * + * @returns A span for writing to. The size of the span is the entire + * allocated capacity. + */ + Span<char> BeginWriting() { + MOZ_RELEASE_ASSERT(mBuffer.get()); + return Span(mBuffer.get(), mCapacity); + } + + /** + * Return a span for reading from. + * + * @returns A span for reading form. The size of the span is the set length + * of the buffer. + */ + Span<const char> BeginReading() const { + MOZ_RELEASE_ASSERT(mBuffer.get() || mLength == 0); + return Span(mBuffer.get(), mLength); + } + + /** + * Consume the JsBuffer and convert it into a JSString. + * + * NOTE: This method asserts the buffer was allocated as a string buffer. + * + * @param aBuffer The buffer to convert to a string. After this call, the + * buffer will be invaldated and |IntoString| cannot be called + * again. + * + * @returns A JSString with the contents of |aBuffer|. + */ + static JSString* IntoString(JSContext* aCx, JsBuffer aBuffer); + + /** + * Consume the JsBuffer and convert it into a Uint8Array. + * + * NOTE: This method asserts the buffer was allocated as an array buffer. + * + * @param aBuffer The buffer to convert to an array. After this call, the + * buffer will be invalidated and |IntoUint8Array| cannot be + * called again. + * + * @returns A JSBuffer + */ + static JSObject* IntoUint8Array(JSContext* aCx, JsBuffer aBuffer); + + friend bool ToJSValue(JSContext* aCx, JsBuffer&& aBuffer, + JS::MutableHandle<JS::Value> aValue); + + private: + IOUtils::BufferKind mBufferKind; + size_t mCapacity; + size_t mLength; + JS::UniqueChars mBuffer; + + JsBuffer(BufferKind aBufferKind, size_t aCapacity); +}; + +class SyncReadFile : public nsISupports, public nsWrapperCache { + public: + SyncReadFile(nsISupports* aParent, RefPtr<nsFileRandomAccessStream>&& aStream, + int64_t aSize); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(SyncReadFile) + + nsISupports* GetParentObject() const { return mParent; } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + int64_t Size() const { return mSize; } + void ReadBytesInto(const Uint8Array&, const int64_t, ErrorResult& aRv); + void Close(); + + private: + virtual ~SyncReadFile(); + + nsCOMPtr<nsISupports> mParent; + RefPtr<nsFileRandomAccessStream> mStream; + int64_t mSize = 0; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/system/NetworkGeolocationProvider.sys.mjs b/dom/system/NetworkGeolocationProvider.sys.mjs new file mode 100644 index 0000000000..1bee69a282 --- /dev/null +++ b/dom/system/NetworkGeolocationProvider.sys.mjs @@ -0,0 +1,504 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + LocationHelper: "resource://gre/modules/LocationHelper.sys.mjs", + clearTimeout: "resource://gre/modules/Timer.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +// GeolocationPositionError has no interface object, so we can't use that here. +const POSITION_UNAVAILABLE = 2; + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "gLoggingEnabled", + "geo.provider.network.logging.enabled", + false +); + +function LOG(aMsg) { + if (lazy.gLoggingEnabled) { + dump("*** WIFI GEO: " + aMsg + "\n"); + } +} + +function CachedRequest(loc, cellInfo, wifiList) { + this.location = loc; + + let wifis = new Set(); + if (wifiList) { + for (let i = 0; i < wifiList.length; i++) { + wifis.add(wifiList[i].macAddress); + } + } + + // Use only these values for equality + // (the JSON will contain additional values in future) + function makeCellKey(cell) { + return ( + "" + + cell.radio + + ":" + + cell.mobileCountryCode + + ":" + + cell.mobileNetworkCode + + ":" + + cell.locationAreaCode + + ":" + + cell.cellId + ); + } + + let cells = new Set(); + if (cellInfo) { + for (let i = 0; i < cellInfo.length; i++) { + cells.add(makeCellKey(cellInfo[i])); + } + } + + this.hasCells = () => cells.size > 0; + + this.hasWifis = () => wifis.size > 0; + + // if fields match + this.isCellEqual = function (cellInfo) { + if (!this.hasCells()) { + return false; + } + + let len1 = cells.size; + let len2 = cellInfo.length; + + if (len1 != len2) { + LOG("cells not equal len"); + return false; + } + + for (let i = 0; i < len2; i++) { + if (!cells.has(makeCellKey(cellInfo[i]))) { + return false; + } + } + return true; + }; + + // if 50% of the SSIDS match + this.isWifiApproxEqual = function (wifiList) { + if (!this.hasWifis()) { + return false; + } + + // if either list is a 50% subset of the other, they are equal + let common = 0; + for (let i = 0; i < wifiList.length; i++) { + if (wifis.has(wifiList[i].macAddress)) { + common++; + } + } + let kPercentMatch = 0.5; + return common >= Math.max(wifis.size, wifiList.length) * kPercentMatch; + }; + + this.isGeoip = function () { + return !this.hasCells() && !this.hasWifis(); + }; + + this.isCellAndWifi = function () { + return this.hasCells() && this.hasWifis(); + }; + + this.isCellOnly = function () { + return this.hasCells() && !this.hasWifis(); + }; + + this.isWifiOnly = function () { + return this.hasWifis() && !this.hasCells(); + }; +} + +var gCachedRequest = null; +var gDebugCacheReasoning = ""; // for logging the caching logic + +// This function serves two purposes: +// 1) do we have a cached request +// 2) is the cached request better than what newCell and newWifiList will obtain +// If the cached request exists, and we know it to have greater accuracy +// by the nature of its origin (wifi/cell/geoip), use its cached location. +// +// If there is more source info than the cached request had, return false +// In other cases, MLS is known to produce better/worse accuracy based on the +// inputs, so base the decision on that. +function isCachedRequestMoreAccurateThanServerRequest(newCell, newWifiList) { + gDebugCacheReasoning = ""; + let isNetworkRequestCacheEnabled = true; + try { + // Mochitest needs this pref to simulate request failure + isNetworkRequestCacheEnabled = Services.prefs.getBoolPref( + "geo.provider.network.debug.requestCache.enabled" + ); + if (!isNetworkRequestCacheEnabled) { + gCachedRequest = null; + } + } catch (e) {} + + if (!gCachedRequest || !isNetworkRequestCacheEnabled) { + gDebugCacheReasoning = "No cached data"; + return false; + } + + if (!newCell && !newWifiList) { + gDebugCacheReasoning = "New req. is GeoIP."; + return true; + } + + if ( + newCell && + newWifiList && + (gCachedRequest.isCellOnly() || gCachedRequest.isWifiOnly()) + ) { + gDebugCacheReasoning = "New req. is cell+wifi, cache only cell or wifi."; + return false; + } + + if (newCell && gCachedRequest.isWifiOnly()) { + // In order to know if a cell-only request should trump a wifi-only request + // need to know if wifi is low accuracy. >5km would be VERY low accuracy, + // it is worth trying the cell + var isHighAccuracyWifi = gCachedRequest.location.coords.accuracy < 5000; + gDebugCacheReasoning = + "Req. is cell, cache is wifi, isHigh:" + isHighAccuracyWifi; + return isHighAccuracyWifi; + } + + let hasEqualCells = false; + if (newCell) { + hasEqualCells = gCachedRequest.isCellEqual(newCell); + } + + let hasEqualWifis = false; + if (newWifiList) { + hasEqualWifis = gCachedRequest.isWifiApproxEqual(newWifiList); + } + + gDebugCacheReasoning = + "EqualCells:" + hasEqualCells + " EqualWifis:" + hasEqualWifis; + + if (gCachedRequest.isCellOnly()) { + gDebugCacheReasoning += ", Cell only."; + if (hasEqualCells) { + return true; + } + } else if (gCachedRequest.isWifiOnly() && hasEqualWifis) { + gDebugCacheReasoning += ", Wifi only."; + return true; + } else if (gCachedRequest.isCellAndWifi()) { + gDebugCacheReasoning += ", Cache has Cell+Wifi."; + if ( + (hasEqualCells && hasEqualWifis) || + (!newWifiList && hasEqualCells) || + (!newCell && hasEqualWifis) + ) { + return true; + } + } + + return false; +} + +function NetworkGeoCoordsObject(lat, lon, acc) { + this.latitude = lat; + this.longitude = lon; + this.accuracy = acc; + + // Neither GLS nor MLS return the following properties, so set them to NaN + // here. nsGeoPositionCoords will convert NaNs to null for optional properties + // of the JavaScript Coordinates object. + this.altitude = NaN; + this.altitudeAccuracy = NaN; + this.heading = NaN; + this.speed = NaN; +} + +NetworkGeoCoordsObject.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIDOMGeoPositionCoords"]), +}; + +function NetworkGeoPositionObject(lat, lng, acc) { + this.coords = new NetworkGeoCoordsObject(lat, lng, acc); + this.address = null; + this.timestamp = Date.now(); +} + +NetworkGeoPositionObject.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIDOMGeoPosition"]), +}; + +export function NetworkGeolocationProvider() { + /* + The _wifiMonitorTimeout controls how long we wait on receiving an update + from the Wifi subsystem. If this timer fires, we believe the Wifi scan has + had a problem and we no longer can use Wifi to position the user this time + around (we will continue to be hopeful that Wifi will recover). + + This timeout value is also used when Wifi scanning is disabled (see + isWifiScanningEnabled). In this case, we use this timer to collect cell/ip + data and xhr it to the location server. + */ + XPCOMUtils.defineLazyPreferenceGetter( + this, + "_wifiMonitorTimeout", + "geo.provider.network.timeToWaitBeforeSending", + 5000 + ); + + XPCOMUtils.defineLazyPreferenceGetter( + this, + "_wifiScanningEnabled", + "geo.provider.network.scan", + true + ); + + this.wifiService = null; + this.timer = null; + this.started = false; +} + +NetworkGeolocationProvider.prototype = { + classID: Components.ID("{77DA64D3-7458-4920-9491-86CC9914F904}"), + name: "NetworkGeolocationProvider", + QueryInterface: ChromeUtils.generateQI([ + "nsIGeolocationProvider", + "nsIWifiListener", + "nsITimerCallback", + "nsIObserver", + "nsINamed", + ]), + listener: null, + + get isWifiScanningEnabled() { + return Cc["@mozilla.org/wifi/monitor;1"] && this._wifiScanningEnabled; + }, + + resetTimer() { + if (this.timer) { + this.timer.cancel(); + this.timer = null; + } + // Wifi thread triggers NetworkGeolocationProvider to proceed. With no wifi, + // do manual timeout. + this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this.timer.initWithCallback( + this, + this._wifiMonitorTimeout, + this.timer.TYPE_REPEATING_SLACK + ); + }, + + startup() { + if (this.started) { + return; + } + + this.started = true; + + if (this.isWifiScanningEnabled) { + if (this.wifiService) { + this.wifiService.stopWatching(this); + } + this.wifiService = Cc["@mozilla.org/wifi/monitor;1"].getService( + Ci.nsIWifiMonitor + ); + this.wifiService.startWatching(this, false); + } + + this.resetTimer(); + LOG("startup called."); + }, + + watch(c) { + this.listener = c; + }, + + shutdown() { + LOG("shutdown called"); + if (!this.started) { + return; + } + + // Without clearing this, we could end up using the cache almost indefinitely + // TODO: add logic for cache lifespan, for now just be safe and clear it + gCachedRequest = null; + + if (this.timer) { + this.timer.cancel(); + this.timer = null; + } + + if (this.wifiService) { + this.wifiService.stopWatching(this); + this.wifiService = null; + } + + this.listener = null; + this.started = false; + }, + + setHighAccuracy(enable) { + // Mochitest wants to check this value + if (Services.prefs.getBoolPref("geo.provider.testing")) { + Services.obs.notifyObservers( + null, + "testing-geolocation-high-accuracy", + enable + ); + } + }, + + onChange(accessPoints) { + // we got some wifi data, rearm the timer. + this.resetTimer(); + + let wifiData = null; + if (accessPoints) { + wifiData = lazy.LocationHelper.formatWifiAccessPoints(accessPoints); + } + this.sendLocationRequest(wifiData); + }, + + onError(code) { + LOG("wifi error: " + code); + this.sendLocationRequest(null); + }, + + onStatus(err, statusMessage) { + if (!this.listener) { + return; + } + LOG("onStatus called." + statusMessage); + + if (statusMessage && this.listener.notifyStatus) { + this.listener.notifyStatus(statusMessage); + } + + if (err && this.listener.notifyError) { + this.listener.notifyError(POSITION_UNAVAILABLE, statusMessage); + } + }, + + notify(timer) { + this.onStatus(false, "wifi-timeout"); + this.sendLocationRequest(null); + }, + + /** + * After wifi (and possible cell tower) data has been gathered, this method is + * invoked to perform the request to network geolocation provider. + * The result of each request is sent to all registered listener (@see watch) + * by invoking its respective `update`, `notifyError` or `notifyStatus` + * callbacks. + * `update` is called upon a successful request with its response data; this will be a `NetworkGeoPositionObject` instance. + * `notifyError` is called whenever the request gets an error from the local + * network subsystem, the server or simply times out. + * `notifyStatus` is called for each status change of the request that may be + * of interest to the consumer of this class. Currently the following status + * changes are reported: 'xhr-start', 'xhr-timeout', 'xhr-error' and + * 'xhr-empty'. + * + * @param {Array} wifiData Optional set of publicly available wifi networks + * in the following structure: + * <code> + * [ + * { macAddress: <mac1>, signalStrength: <signal1> }, + * { macAddress: <mac2>, signalStrength: <signal2> } + * ] + * </code> + */ + async sendLocationRequest(wifiData) { + let data = { cellTowers: undefined, wifiAccessPoints: undefined }; + if (wifiData && wifiData.length >= 2) { + data.wifiAccessPoints = wifiData; + } + + let useCached = isCachedRequestMoreAccurateThanServerRequest( + data.cellTowers, + data.wifiAccessPoints + ); + + LOG("Use request cache:" + useCached + " reason:" + gDebugCacheReasoning); + + if (useCached) { + gCachedRequest.location.timestamp = Date.now(); + if (this.listener) { + this.listener.update(gCachedRequest.location); + } + return; + } + + // From here on, do a network geolocation request // + let url = Services.urlFormatter.formatURLPref("geo.provider.network.url"); + LOG("Sending request"); + + let result; + try { + result = await this.makeRequest(url, wifiData); + LOG( + `geo provider reported: ${result.location.lng}:${result.location.lat}` + ); + let newLocation = new NetworkGeoPositionObject( + result.location.lat, + result.location.lng, + result.accuracy + ); + + if (this.listener) { + this.listener.update(newLocation); + } + + gCachedRequest = new CachedRequest( + newLocation, + data.cellTowers, + data.wifiAccessPoints + ); + } catch (err) { + LOG("Location request hit error: " + err.name); + console.error(err); + if (err.name == "AbortError") { + this.onStatus(true, "xhr-timeout"); + } else { + this.onStatus(true, "xhr-error"); + } + } + }, + + async makeRequest(url, wifiData) { + this.onStatus(false, "xhr-start"); + + let fetchController = new AbortController(); + let fetchOpts = { + method: "POST", + headers: { "Content-Type": "application/json; charset=UTF-8" }, + credentials: "omit", + signal: fetchController.signal, + }; + + if (wifiData) { + fetchOpts.body = JSON.stringify({ wifiAccessPoints: wifiData }); + } + + let timeoutId = lazy.setTimeout( + () => fetchController.abort(), + Services.prefs.getIntPref("geo.provider.network.timeout") + ); + + let req = await fetch(url, fetchOpts); + lazy.clearTimeout(timeoutId); + let result = req.json(); + return result; + }, +}; diff --git a/dom/system/PathUtils.cpp b/dom/system/PathUtils.cpp new file mode 100644 index 0000000000..24d7b648db --- /dev/null +++ b/dom/system/PathUtils.cpp @@ -0,0 +1,635 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "PathUtils.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/DataMutex.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/Maybe.h" +#include "mozilla/MozPromise.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/Span.h" +#include "mozilla/Try.h" +#include "mozilla/dom/DOMParser.h" +#include "mozilla/dom/PathUtilsBinding.h" +#include "mozilla/dom/Promise.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsCOMPtr.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsIFile.h" +#include "nsIGlobalObject.h" +#include "nsLocalFile.h" +#include "nsNetUtil.h" +#include "nsString.h" +#include "nsURLHelper.h" +#include "xpcpublic.h" + +namespace mozilla::dom { + +static constexpr auto ERROR_EMPTY_PATH = + "PathUtils does not support empty paths"_ns; +static constexpr auto ERROR_INITIALIZE_PATH = "Could not initialize path"_ns; +static constexpr auto ERROR_GET_PARENT = "Could not get parent path"_ns; +static constexpr auto ERROR_JOIN = "Could not append to path"_ns; + +static constexpr auto COLON = ": "_ns; + +static void ThrowError(ErrorResult& aErr, const nsresult aResult, + const nsCString& aMessage) { + nsAutoCStringN<32> errName; + GetErrorName(aResult, errName); + + nsAutoCStringN<256> formattedMsg; + formattedMsg.Append(aMessage); + formattedMsg.Append(COLON); + formattedMsg.Append(errName); + + switch (aResult) { + case NS_ERROR_FILE_UNRECOGNIZED_PATH: + aErr.ThrowOperationError(formattedMsg); + break; + + case NS_ERROR_FILE_ACCESS_DENIED: + aErr.ThrowInvalidAccessError(formattedMsg); + break; + + case NS_ERROR_FAILURE: + default: + aErr.ThrowUnknownError(formattedMsg); + break; + } +} + +static bool DoWindowsPathCheck() { +#ifdef XP_WIN +# ifdef DEBUG + return true; +# else // DEBUG + return xpc::IsInAutomation(); +# endif // DEBUG +#else // XP_WIN + return false; +#endif // XP_WIN +} + +/* static */ +nsresult PathUtils::InitFileWithPath(nsIFile* aFile, const nsAString& aPath) { + if (DoWindowsPathCheck()) { + MOZ_RELEASE_ASSERT(!aPath.Contains(u'/'), + "Windows paths cannot include forward slashes"); + } + + MOZ_ASSERT(aFile); + return aFile->InitWithPath(aPath); +} + +StaticDataMutex<Maybe<PathUtils::DirectoryCache>> PathUtils::sDirCache{ + "sDirCache"}; + +/** + * Return the leaf name, including leading path separators in the case of + * Windows UNC drive paths. + * + * @param aFile The file whose leaf name is to be returned. + * @param aResult The string to hold the resulting leaf name. + * @param aParent The pre-computed parent of |aFile|. If not provided, it will + * be computed. + */ +static nsresult GetLeafNamePreservingRoot(nsIFile* aFile, nsString& aResult, + nsIFile* aParent = nullptr) { + MOZ_ASSERT(aFile); + + nsCOMPtr<nsIFile> parent = aParent; + if (!parent) { + MOZ_TRY(aFile->GetParent(getter_AddRefs(parent))); + } + + if (parent) { + return aFile->GetLeafName(aResult); + } + + // We have reached the root path. On Windows, the leafname for a UNC path + // will not have the leading backslashes, so we need to use the entire path + // here: + // + // * for a UNIX root path (/) this will be /; + // * for a Windows drive path (e.g., C:), this will be the drive path (C:); + // and + // * for a Windows UNC server path (e.g., \\\\server), this will be the full + // server path (\\\\server). + return aFile->GetPath(aResult); +} + +void PathUtils::Filename(const GlobalObject&, const nsAString& aPath, + nsString& aResult, ErrorResult& aErr) { + if (aPath.IsEmpty()) { + aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH); + return; + } + + nsCOMPtr<nsIFile> path = new nsLocalFile(); + if (nsresult rv = InitFileWithPath(path, aPath); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return; + } + + if (nsresult rv = GetLeafNamePreservingRoot(path, aResult); NS_FAILED(rv)) { + ThrowError(aErr, rv, "Could not get leaf name of path"_ns); + return; + } +} + +void PathUtils::Parent(const GlobalObject&, const nsAString& aPath, + const int32_t aDepth, nsString& aResult, + ErrorResult& aErr) { + if (aPath.IsEmpty()) { + aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH); + return; + } + + nsCOMPtr<nsIFile> path = new nsLocalFile(); + if (nsresult rv = InitFileWithPath(path, aPath); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return; + } + + if (aDepth <= 0) { + aErr.ThrowNotSupportedError("A depth of at least 1 is required"); + return; + } + + nsCOMPtr<nsIFile> parent; + for (int32_t i = 0; path && i < aDepth; i++) { + if (nsresult rv = path->GetParent(getter_AddRefs(parent)); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_GET_PARENT); + return; + } + path = parent; + } + + if (parent) { + MOZ_ALWAYS_SUCCEEDS(parent->GetPath(aResult)); + } else { + aResult = VoidString(); + } +} + +void PathUtils::Join(const GlobalObject&, const Sequence<nsString>& aComponents, + nsString& aResult, ErrorResult& aErr) { + nsCOMPtr<nsIFile> path = Join(Span(aComponents), aErr); + if (aErr.Failed()) { + return; + } + + MOZ_ALWAYS_SUCCEEDS(path->GetPath(aResult)); +} + +already_AddRefed<nsIFile> PathUtils::Join( + const Span<const nsString>& aComponents, ErrorResult& aErr) { + if (aComponents.IsEmpty() || aComponents[0].IsEmpty()) { + aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH); + return nullptr; + } + + nsCOMPtr<nsIFile> path = new nsLocalFile(); + if (nsresult rv = InitFileWithPath(path, aComponents[0]); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return nullptr; + } + + const auto components = Span<const nsString>(aComponents).Subspan(1); + for (const auto& component : components) { + if (nsresult rv = path->Append(component); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_JOIN); + return nullptr; + } + } + + return path.forget(); +} + +void PathUtils::JoinRelative(const GlobalObject&, const nsAString& aBasePath, + const nsAString& aRelativePath, nsString& aResult, + ErrorResult& aErr) { + if (aRelativePath.IsEmpty()) { + aResult = aBasePath; + return; + } + + nsCOMPtr<nsIFile> path = new nsLocalFile(); + if (nsresult rv = InitFileWithPath(path, aBasePath); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return; + } + + if (nsresult rv = path->AppendRelativePath(aRelativePath); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_JOIN); + return; + } + + MOZ_ALWAYS_SUCCEEDS(path->GetPath(aResult)); +} + +void PathUtils::ToExtendedWindowsPath(const GlobalObject&, + const nsAString& aPath, nsString& aResult, + ErrorResult& aErr) { +#ifndef XP_WIN + aErr.ThrowNotAllowedError("Operation is windows specific"_ns); + return; +#else + if (aPath.IsEmpty()) { + aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH); + return; + } + + const nsAString& str1 = Substring(aPath, 1, 1); + const nsAString& str2 = Substring(aPath, 2, aPath.Length() - 2); + + bool isUNC = aPath.Length() >= 2 && + (aPath.First() == '\\' || aPath.First() == '/') && + (str1.EqualsLiteral("\\") || str1.EqualsLiteral("/")); + + constexpr auto pathPrefix = u"\\\\?\\"_ns; + const nsAString& uncPath = pathPrefix + u"UNC\\"_ns + str2; + const nsAString& normalPath = pathPrefix + aPath; + + nsCOMPtr<nsIFile> path = new nsLocalFile(); + if (nsresult rv = InitFileWithPath(path, isUNC ? uncPath : normalPath); + NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return; + } + + MOZ_ALWAYS_SUCCEEDS(path->GetPath(aResult)); +#endif +} + +void PathUtils::Normalize(const GlobalObject&, const nsAString& aPath, + nsString& aResult, ErrorResult& aErr) { + if (aPath.IsEmpty()) { + aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH); + return; + } + + nsCOMPtr<nsIFile> path = new nsLocalFile(); + if (nsresult rv = InitFileWithPath(path, aPath); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return; + } + + if (nsresult rv = path->Normalize(); NS_FAILED(rv)) { + ThrowError(aErr, rv, "Could not normalize path"_ns); + return; + } + + MOZ_ALWAYS_SUCCEEDS(path->GetPath(aResult)); +} + +void PathUtils::Split(const GlobalObject&, const nsAString& aPath, + nsTArray<nsString>& aResult, ErrorResult& aErr) { + if (aPath.IsEmpty()) { + aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH); + return; + } + + nsCOMPtr<nsIFile> path = new nsLocalFile(); + if (nsresult rv = InitFileWithPath(path, aPath); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return; + } + + while (path) { + auto* component = aResult.EmplaceBack(fallible); + if (!component) { + aErr.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + nsCOMPtr<nsIFile> parent; + if (nsresult rv = path->GetParent(getter_AddRefs(parent)); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_GET_PARENT); + return; + } + + // GetLeafPreservingRoot cannot fail if we pass it a parent path. + MOZ_ALWAYS_SUCCEEDS(GetLeafNamePreservingRoot(path, *component, parent)); + + path = parent; + } + + aResult.Reverse(); +} + +void PathUtils::SplitRelative(const GlobalObject& aGlobal, + const nsAString& aPath, + const SplitRelativeOptions& aOptions, + nsTArray<nsString>& aResult, ErrorResult& aErr) { + if (aPath.IsEmpty()) { + aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH); + return; + } + + if (DoWindowsPathCheck()) { + MOZ_RELEASE_ASSERT(!aPath.Contains(u'/'), + "Windows paths cannot include forward slashes"); + } + + if (IsAbsolute(aGlobal, aPath)) { + aErr.ThrowNotAllowedError( + "PathUtils.splitRelative requires a relative path"_ns); + return; + } + +#ifdef XP_WIN + constexpr auto SEPARATOR = u'\\'; +#else + constexpr auto SEPARATOR = u'/'; +#endif + + constexpr auto PARENT = u".."_ns; + constexpr auto CURRENT = u"."_ns; + + for (const nsAString& pathComponent : + nsCharSeparatedTokenizerTemplate<NS_TokenizerIgnoreNothing>{aPath, + SEPARATOR} + .ToRange()) { + if (!aOptions.mAllowEmpty && pathComponent.IsEmpty()) { + aErr.ThrowNotAllowedError( + "PathUtils.splitRelative: Empty directory components (\"\") not " + "allowed by options"); + return; + } + + if (!aOptions.mAllowParentDir && pathComponent == PARENT) { + aErr.ThrowNotAllowedError( + "PathUtils.splitRelative: Parent directory components (\"..\") not " + "allowed by options"); + return; + } + + if (!aOptions.mAllowCurrentDir && pathComponent == CURRENT) { + aErr.ThrowNotAllowedError( + "PathUtils.splitRelative: Current directory components (\".\") not " + "allowed by options"); + return; + } + + aResult.AppendElement(pathComponent); + } +} + +void PathUtils::ToFileURI(const GlobalObject&, const nsAString& aPath, + nsCString& aResult, ErrorResult& aErr) { + if (aPath.IsEmpty()) { + aErr.ThrowNotAllowedError(ERROR_EMPTY_PATH); + return; + } + + nsCOMPtr<nsIFile> path = new nsLocalFile(); + if (nsresult rv = InitFileWithPath(path, aPath); NS_FAILED(rv)) { + ThrowError(aErr, rv, ERROR_INITIALIZE_PATH); + return; + } + + if (nsresult rv = net_GetURLSpecFromActualFile(path, aResult); + NS_FAILED(rv)) { + ThrowError(aErr, rv, "Could not retrieve URI spec"_ns); + return; + } +} + +bool PathUtils::IsAbsolute(const GlobalObject&, const nsAString& aPath) { + nsCOMPtr<nsIFile> path = new nsLocalFile(); + nsresult rv = InitFileWithPath(path, aPath); + return NS_SUCCEEDED(rv); +} + +void PathUtils::GetProfileDirSync(const GlobalObject&, nsString& aResult, + ErrorResult& aErr) { + MOZ_ASSERT(NS_IsMainThread()); + + auto guard = sDirCache.Lock(); + DirectoryCache::Ensure(guard.ref()) + .GetDirectorySync(aResult, aErr, DirectoryCache::Directory::Profile); +} +void PathUtils::GetLocalProfileDirSync(const GlobalObject&, nsString& aResult, + ErrorResult& aErr) { + MOZ_ASSERT(NS_IsMainThread()); + + auto guard = sDirCache.Lock(); + DirectoryCache::Ensure(guard.ref()) + .GetDirectorySync(aResult, aErr, DirectoryCache::Directory::LocalProfile); +} +void PathUtils::GetTempDirSync(const GlobalObject&, nsString& aResult, + ErrorResult& aErr) { + MOZ_ASSERT(NS_IsMainThread()); + + auto guard = sDirCache.Lock(); + DirectoryCache::Ensure(guard.ref()) + .GetDirectorySync(aResult, aErr, DirectoryCache::Directory::Temp); +} + +void PathUtils::GetXulLibraryPathSync(const GlobalObject&, nsString& aResult, + ErrorResult& aErr) { + MOZ_ASSERT(NS_IsMainThread()); + + auto guard = sDirCache.Lock(); + DirectoryCache::Ensure(guard.ref()) + .GetDirectorySync(aResult, aErr, DirectoryCache::Directory::XulLibrary); +} + +already_AddRefed<Promise> PathUtils::GetProfileDirAsync( + const GlobalObject& aGlobal, ErrorResult& aErr) { + MOZ_ASSERT(!NS_IsMainThread()); + + auto guard = sDirCache.Lock(); + return DirectoryCache::Ensure(guard.ref()) + .GetDirectoryAsync(aGlobal, aErr, DirectoryCache::Directory::Profile); +} + +already_AddRefed<Promise> PathUtils::GetLocalProfileDirAsync( + const GlobalObject& aGlobal, ErrorResult& aErr) { + MOZ_ASSERT(!NS_IsMainThread()); + + auto guard = sDirCache.Lock(); + return DirectoryCache::Ensure(guard.ref()) + .GetDirectoryAsync(aGlobal, aErr, + DirectoryCache::Directory::LocalProfile); +} + +already_AddRefed<Promise> PathUtils::GetTempDirAsync( + const GlobalObject& aGlobal, ErrorResult& aErr) { + MOZ_ASSERT(!NS_IsMainThread()); + + auto guard = sDirCache.Lock(); + return DirectoryCache::Ensure(guard.ref()) + .GetDirectoryAsync(aGlobal, aErr, DirectoryCache::Directory::Temp); +} + +already_AddRefed<Promise> PathUtils::GetXulLibraryPathAsync( + const GlobalObject& aGlobal, ErrorResult& aErr) { + MOZ_ASSERT(!NS_IsMainThread()); + + auto guard = sDirCache.Lock(); + return DirectoryCache::Ensure(guard.ref()) + .GetDirectoryAsync(aGlobal, aErr, DirectoryCache::Directory::XulLibrary); +} + +PathUtils::DirectoryCache::DirectoryCache() { + for (auto& dir : mDirectories) { + dir.SetIsVoid(true); + } +} + +PathUtils::DirectoryCache& PathUtils::DirectoryCache::Ensure( + Maybe<PathUtils::DirectoryCache>& aCache) { + if (aCache.isNothing()) { + aCache.emplace(); + + auto clearAtShutdown = []() { + RunOnShutdown([]() { + auto cache = PathUtils::sDirCache.Lock(); + cache->reset(); + }); + }; + + if (NS_IsMainThread()) { + clearAtShutdown(); + } else { + NS_DispatchToMainThread( + NS_NewRunnableFunction(__func__, std::move(clearAtShutdown))); + } + } + + return aCache.ref(); +} + +void PathUtils::DirectoryCache::GetDirectorySync( + nsString& aResult, ErrorResult& aErr, const Directory aRequestedDir) { + MOZ_RELEASE_ASSERT(aRequestedDir < Directory::Count); + + if (nsresult rv = PopulateDirectoriesImpl(aRequestedDir); NS_FAILED(rv)) { + nsAutoCStringN<32> errorName; + GetErrorName(rv, errorName); + + nsAutoCStringN<256> msg; + msg.Append("Could not retrieve directory "_ns); + msg.Append(kDirectoryNames[aRequestedDir]); + msg.Append(COLON); + msg.Append(errorName); + + aErr.ThrowUnknownError(msg); + return; + } + + aResult = mDirectories[aRequestedDir]; +} + +already_AddRefed<Promise> PathUtils::DirectoryCache::GetDirectoryAsync( + const GlobalObject& aGlobal, ErrorResult& aErr, + const Directory aRequestedDir) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<Promise> promise = Promise::Create(global, aErr); + if (aErr.Failed()) { + return nullptr; + } + + if (RefPtr<PopulateDirectoriesPromise> p = + PopulateDirectories(aRequestedDir)) { + p->Then( + GetCurrentSerialEventTarget(), __func__, + [promise, aRequestedDir](const Ok&) { + auto cache = PathUtils::sDirCache.Lock(); + cache.ref()->ResolveWithDirectory(promise, aRequestedDir); + }, + [promise](const nsresult& aRv) { promise->MaybeReject(aRv); }); + } else { + ResolveWithDirectory(promise, aRequestedDir); + } + + return promise.forget(); +} + +void PathUtils::DirectoryCache::ResolveWithDirectory( + Promise* aPromise, const Directory aRequestedDir) { + MOZ_RELEASE_ASSERT(aRequestedDir < Directory::Count); + MOZ_RELEASE_ASSERT(!mDirectories[aRequestedDir].IsVoid()); + aPromise->MaybeResolve(mDirectories[aRequestedDir]); +} + +already_AddRefed<PathUtils::DirectoryCache::PopulateDirectoriesPromise> +PathUtils::DirectoryCache::PopulateDirectories( + const PathUtils::DirectoryCache::Directory aRequestedDir) { + MOZ_RELEASE_ASSERT(aRequestedDir < Directory::Count); + + // If we have already resolved the requested directory, we can return + // immediately. + // Otherwise, if we have already fired off a request to populate the entry, + // so we can return the corresponding promise immediately. caller will queue + // a Thenable onto that promise to resolve/reject the request. + if (!mDirectories[aRequestedDir].IsVoid()) { + return nullptr; + } + if (!mPromises[aRequestedDir].IsEmpty()) { + return mPromises[aRequestedDir].Ensure(__func__); + } + + RefPtr<PopulateDirectoriesPromise> promise = + mPromises[aRequestedDir].Ensure(__func__); + + if (NS_IsMainThread()) { + nsresult rv = PopulateDirectoriesImpl(aRequestedDir); + ResolvePopulateDirectoriesPromise(rv, aRequestedDir); + } else { + nsCOMPtr<nsIRunnable> runnable = + NS_NewRunnableFunction(__func__, [aRequestedDir]() { + auto cache = PathUtils::sDirCache.Lock(); + nsresult rv = cache.ref()->PopulateDirectoriesImpl(aRequestedDir); + cache.ref()->ResolvePopulateDirectoriesPromise(rv, aRequestedDir); + }); + NS_DispatchToMainThread(runnable.forget()); + } + + return promise.forget(); +} + +void PathUtils::DirectoryCache::ResolvePopulateDirectoriesPromise( + nsresult aRv, const PathUtils::DirectoryCache::Directory aRequestedDir) { + MOZ_RELEASE_ASSERT(aRequestedDir < Directory::Count); + + if (NS_SUCCEEDED(aRv)) { + mPromises[aRequestedDir].Resolve(Ok{}, __func__); + } else { + mPromises[aRequestedDir].Reject(aRv, __func__); + } +} + +nsresult PathUtils::DirectoryCache::PopulateDirectoriesImpl( + const PathUtils::DirectoryCache::Directory aRequestedDir) { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + MOZ_RELEASE_ASSERT(aRequestedDir < Directory::Count); + + if (!mDirectories[aRequestedDir].IsVoid()) { + // In between when this promise was dispatched to the main thread and now, + // the directory cache has had this entry populated (via the + // on-main-thread sync method). + return NS_OK; + } + + nsCOMPtr<nsIFile> path; + + MOZ_TRY(NS_GetSpecialDirectory(kDirectoryNames[aRequestedDir], + getter_AddRefs(path))); + MOZ_TRY(path->GetPath(mDirectories[aRequestedDir])); + + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/system/PathUtils.h b/dom/system/PathUtils.h new file mode 100644 index 0000000000..ff01ddfc1e --- /dev/null +++ b/dom/system/PathUtils.h @@ -0,0 +1,260 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_PathUtils__ +#define mozilla_dom_PathUtils__ + +#include "mozilla/DataMutex.h" +#include "mozilla/EnumeratedArray.h" +#include "mozilla/Maybe.h" +#include "mozilla/MozPromise.h" +#include "mozilla/Mutex.h" +#include "mozilla/Result.h" +#include "mozilla/dom/PathUtilsBinding.h" +#include "mozilla/dom/Promise.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsDirectoryServiceDefs.h" +#include "nsString.h" +#include "nsTArray.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class PathUtils final { + public: + /** + * Initialize the given nsIFile with the given path. + * + * This is equivalent to calling nsIFile::InitWithPath() with the caveat that + * on Windows debug or during Windows CI tests, we will crash if the path + * contains a forward slash. + * + * @param aFile The file to initialize. + * @param aPath The path to initialize the file with. + * + * @return The result of calling nsIFile::InitWithPath. + */ + static nsresult InitFileWithPath(nsIFile* aFile, const nsAString& aPath); + + static void Filename(const GlobalObject&, const nsAString& aPath, + nsString& aResult, ErrorResult& aErr); + + static void Parent(const GlobalObject&, const nsAString& aPath, + const int32_t aDepth, nsString& aResult, + ErrorResult& aErr); + + static void Join(const GlobalObject&, const Sequence<nsString>& aComponents, + nsString& aResult, ErrorResult& aErr); + + /** + * Join a sequence of path components and return an nsIFile with the resulting + * path. + * + * @param aComponents A sequence of path components. The first component must + * be an absolute path. + * @param aErr The error result, if any. + * + * @return An nsIFile with the resulting path, if there were no errors. + * Otherwise, nullptr is returned. + */ + static already_AddRefed<nsIFile> Join(const Span<const nsString>& aComponents, + ErrorResult& aErr); + + static void JoinRelative(const GlobalObject&, const nsAString& aBasePath, + const nsAString& aRelativePath, nsString& aResult, + ErrorResult& aErr); + + static void ToExtendedWindowsPath(const GlobalObject&, const nsAString& aPath, + nsString& aResult, ErrorResult& aErr); + + static void Normalize(const GlobalObject&, const nsAString& aPath, + nsString& aResult, ErrorResult& aErr); + + static void Split(const GlobalObject&, const nsAString& aPath, + nsTArray<nsString>& aResult, ErrorResult& aErr); + + static void SplitRelative(const GlobalObject& aGlobal, const nsAString& aPath, + const SplitRelativeOptions& aOptions, + nsTArray<nsString>& aResult, ErrorResult& aErr); + + static void ToFileURI(const GlobalObject&, const nsAString& aPath, + nsCString& aResult, ErrorResult& aErr); + + static bool IsAbsolute(const GlobalObject&, const nsAString& aPath); + + static void GetProfileDirSync(const GlobalObject&, nsString& aResult, + ErrorResult& aErr); + static void GetLocalProfileDirSync(const GlobalObject&, nsString& aResult, + ErrorResult& aErr); + static void GetTempDirSync(const GlobalObject&, nsString& aResult, + ErrorResult& aErr); + static void GetXulLibraryPathSync(const GlobalObject&, nsString& aResult, + ErrorResult& aErr); + + static already_AddRefed<Promise> GetProfileDirAsync( + const GlobalObject& aGlobal, ErrorResult& aErr); + static already_AddRefed<Promise> GetLocalProfileDirAsync( + const GlobalObject& aGlobal, ErrorResult& aErr); + static already_AddRefed<Promise> GetTempDirAsync(const GlobalObject& aGlobal, + ErrorResult& aErr); + static already_AddRefed<Promise> GetXulLibraryPathAsync( + const GlobalObject& aGlobal, ErrorResult& aErr); + + private: + class DirectoryCache; + friend class DirectoryCache; + + static StaticDataMutex<Maybe<DirectoryCache>> sDirCache; +}; + +/** + * A cache of commonly used directories + */ +class PathUtils::DirectoryCache final { + public: + /** + * A directory that can be requested via |GetDirectorySync| or + * |GetDirectoryAsync|. + */ + enum class Directory { + /** + * The user's profile directory. + */ + Profile, + /** + * The user's local profile directory. + */ + LocalProfile, + /** + * The OS temporary directory. + */ + Temp, + /** + * The libxul path. + */ + XulLibrary, + /** + * The number of Directory entries. + */ + Count, + }; + + DirectoryCache(); + DirectoryCache(const DirectoryCache&) = delete; + DirectoryCache(DirectoryCache&&) = delete; + DirectoryCache& operator=(const DirectoryCache&) = delete; + DirectoryCache& operator=(DirectoryCache&&) = delete; + + /** + * Ensure the cache is instantiated and schedule its destructor to run at + * shutdown. + * + * If the cache is already instantiated, this is a no-op. + * + * @param aCache The cache to ensure is instantiated. + */ + static DirectoryCache& Ensure(Maybe<DirectoryCache>& aCache); + + void GetDirectorySync(nsString& aResult, ErrorResult& aErr, + const Directory aRequestedDir); + + /** + * Request the path of a specific directory. + * + * If the directory has not been requested before, this may require a trip to + * the main thread to retrieve its path. + * + * @param aGlobalObject The JavaScript global. + * @param aErr The error result. + * @param aRequestedDir The directory for which the path is to be retrieved. + * + * @return A promise that resolves to the path of the requested directory. + */ + already_AddRefed<Promise> GetDirectoryAsync(const GlobalObject& aGlobalObject, + ErrorResult& aErr, + const Directory aRequestedDir); + + private: + using PopulateDirectoriesPromise = MozPromise<Ok, nsresult, false>; + + /** + * Populate the directory cache entry for the requested directory. + * + * @param aRequestedDir The directory cache entry that was requested via + * |GetDirectory|. + * + * @return If the requested directory has not been populated, this returns a + * promise that resolves when the population is complete. + * + * If the requested directory has already been populated, it returns + * nullptr instead. + */ + already_AddRefed<PopulateDirectoriesPromise> PopulateDirectories( + const Directory aRequestedDir); + + /** + * Initialize the requested directory cache entry. + * + * If |Directory::Temp| is requested, all cache entries will be populated. + * Otherwise, only the profile and local profile cache entries will be + * populated. The profile and local profile cache entries have no additional + * overhead for populating them, but the temp directory requires creating a + * directory on the main thread if it has not already happened. + * + * Must be called on the main thread. + * + * @param aRequestedDir The requested directory. + * + * @return The result of initializing directories. + */ + nsresult PopulateDirectoriesImpl(const Directory aRequestedDir); + + /** + * Resolve the internal PopulateDirectoriesPromise corresponding to + * |aRequestedDir| with the given result. + * + * This will allow all pending queries for the requested directory to resolve + * or be rejected. + * + * @param aRv The return value from PopulateDirectoriesImpl. + * @param aRequestedDir The requested directory cache entry. This is used to + * determine which internal MozPromiseHolder we are + * resolving. + */ + void ResolvePopulateDirectoriesPromise(nsresult aRv, + const Directory aRequestedDir); + + /** + * Resolve the given JS promise with the path of the requested directory + * + * Can only be called once the cache entry for the requested directory is + * populated. + * + * @param aPromise The JS promise to resolve. + * @param aRequestedDir The requested directory cache entry. + */ + void ResolveWithDirectory(Promise* aPromise, const Directory aRequestedDir); + + template <typename T> + using DirectoryArray = EnumeratedArray<Directory, Directory::Count, T>; + + DirectoryArray<nsString> mDirectories; + DirectoryArray<MozPromiseHolder<PopulateDirectoriesPromise>> mPromises; + + static constexpr DirectoryArray<const char*> kDirectoryNames{ + NS_APP_USER_PROFILE_50_DIR, + NS_APP_USER_PROFILE_LOCAL_50_DIR, + NS_OS_TEMP_DIR, + NS_XPCOM_LIBRARY_FILE, + }; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/system/android/AndroidLocationProvider.cpp b/dom/system/android/AndroidLocationProvider.cpp new file mode 100644 index 0000000000..8861c30298 --- /dev/null +++ b/dom/system/android/AndroidLocationProvider.cpp @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "Geolocation.h" +#include "GeolocationPosition.h" +#include "AndroidLocationProvider.h" +#include "mozilla/java/GeckoAppShellWrappers.h" + +using namespace mozilla; + +extern nsIGeolocationUpdate* gLocationCallback; + +NS_IMPL_ISUPPORTS(AndroidLocationProvider, nsIGeolocationProvider) + +AndroidLocationProvider::AndroidLocationProvider() {} + +AndroidLocationProvider::~AndroidLocationProvider() { + NS_IF_RELEASE(gLocationCallback); +} + +NS_IMETHODIMP +AndroidLocationProvider::Startup() { + if (java::GeckoAppShell::EnableLocationUpdates(true)) { + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +AndroidLocationProvider::Watch(nsIGeolocationUpdate* aCallback) { + NS_IF_RELEASE(gLocationCallback); + gLocationCallback = aCallback; + NS_IF_ADDREF(gLocationCallback); + return NS_OK; +} + +NS_IMETHODIMP +AndroidLocationProvider::Shutdown() { + if (java::GeckoAppShell::EnableLocationUpdates(false)) { + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +AndroidLocationProvider::SetHighAccuracy(bool enable) { + java::GeckoAppShell::EnableLocationHighAccuracy(enable); + return NS_OK; +} diff --git a/dom/system/android/AndroidLocationProvider.h b/dom/system/android/AndroidLocationProvider.h new file mode 100644 index 0000000000..e0d38f6c8f --- /dev/null +++ b/dom/system/android/AndroidLocationProvider.h @@ -0,0 +1,23 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef AndroidLocationProvider_h +#define AndroidLocationProvider_h + +#include "nsIGeolocationProvider.h" + +class AndroidLocationProvider final : public nsIGeolocationProvider { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONPROVIDER + + AndroidLocationProvider(); + + private: + ~AndroidLocationProvider(); +}; + +#endif /* AndroidLocationProvider_h */ diff --git a/dom/system/android/moz.build b/dom/system/android/moz.build new file mode 100644 index 0000000000..04dffba024 --- /dev/null +++ b/dom/system/android/moz.build @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +SOURCES += [ + "AndroidLocationProvider.cpp", + "nsHapticFeedback.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" +LOCAL_INCLUDES += [ + "/dom/geolocation", +] diff --git a/dom/system/android/nsHapticFeedback.cpp b/dom/system/android/nsHapticFeedback.cpp new file mode 100644 index 0000000000..87c77d8334 --- /dev/null +++ b/dom/system/android/nsHapticFeedback.cpp @@ -0,0 +1,18 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsHapticFeedback.h" +#include "mozilla/java/GeckoAppShellWrappers.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(nsHapticFeedback, nsIHapticFeedback) + +NS_IMETHODIMP +nsHapticFeedback::PerformSimpleAction(int32_t aType) { + java::GeckoAppShell::PerformHapticFeedback(aType == LongPress); + return NS_OK; +} diff --git a/dom/system/android/nsHapticFeedback.h b/dom/system/android/nsHapticFeedback.h new file mode 100644 index 0000000000..e55062058b --- /dev/null +++ b/dom/system/android/nsHapticFeedback.h @@ -0,0 +1,16 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIHapticFeedback.h" + +class nsHapticFeedback final : public nsIHapticFeedback { + private: + ~nsHapticFeedback() {} + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHAPTICFEEDBACK +}; diff --git a/dom/system/components.conf b/dom/system/components.conf new file mode 100644 index 0000000000..82bcb2cb6f --- /dev/null +++ b/dom/system/components.conf @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Classes = [ + { + 'cid': '{77DA64D3-7458-4920-9491-86CC9914F904}', + 'contract_ids': [ + '@mozilla.org/geolocation/provider;1', + '@mozilla.org/geolocation/mls-provider;1', + ], + 'esModule': 'resource://gre/modules/NetworkGeolocationProvider.sys.mjs', + 'constructor': 'NetworkGeolocationProvider', + }, +] diff --git a/dom/system/linux/GeoclueLocationProvider.cpp b/dom/system/linux/GeoclueLocationProvider.cpp new file mode 100644 index 0000000000..c9f5ef5dac --- /dev/null +++ b/dom/system/linux/GeoclueLocationProvider.cpp @@ -0,0 +1,1060 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Author: Maciej S. Szmigiero <mail@maciej.szmigiero.name> + */ + +#include "GeoclueLocationProvider.h" + +#include <gio/gio.h> +#include <glib.h> +#include "mozilla/FloatingPoint.h" +#include "mozilla/GRefPtr.h" +#include "mozilla/GUniquePtr.h" +#include "mozilla/Logging.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_geo.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/WeakPtr.h" +#include "mozilla/XREAppData.h" +#include "mozilla/dom/GeolocationPosition.h" +#include "mozilla/dom/GeolocationPositionErrorBinding.h" +#include "MLSFallback.h" +#include "nsAppRunner.h" +#include "nsCOMPtr.h" +#include "nsIDOMGeoPosition.h" +#include "nsINamed.h" +#include "nsITimer.h" +#include "nsStringFwd.h" +#include "prtime.h" + +namespace mozilla::dom { + +static LazyLogModule gGCLocationLog("GeoclueLocation"); + +#define GCL_LOG(level, ...) \ + MOZ_LOG(gGCLocationLog, mozilla::LogLevel::level, (__VA_ARGS__)) + +static const char* const kGeoclueBusName = "org.freedesktop.GeoClue2"; +static const char* const kGCManagerPath = "/org/freedesktop/GeoClue2/Manager"; +static const char* const kGCManagerInterface = + "org.freedesktop.GeoClue2.Manager"; +static const char* const kGCClientInterface = "org.freedesktop.GeoClue2.Client"; +static const char* const kGCLocationInterface = + "org.freedesktop.GeoClue2.Location"; +static const char* const kDBPropertySetMethod = + "org.freedesktop.DBus.Properties.Set"; + +/* + * Minimum altitude reported as valid (in meters), + * https://en.wikipedia.org/wiki/List_of_places_on_land_with_elevations_below_sea_level + * says that lowest land in the world is at -430 m, so let's use -500 m here. + */ +static const double kGCMinAlt = -500; + +/* + * Matches "enum GClueAccuracyLevel" values, see: + * https://www.freedesktop.org/software/geoclue/docs/geoclue-gclue-enums.html#GClueAccuracyLevel + */ +enum class GCAccuracyLevel { + None = 0, + Country = 1, + City = 4, + Neighborhood = 5, + Street = 6, + Exact = 8, +}; + +/* + * Whether to reuse D-Bus proxies between uses of this provider. + * Usually a good thing, can be disabled for debug purposes. + */ +static const bool kGCReuseDBusProxy = true; + +class GCLocProviderPriv final : public nsIGeolocationProvider, + public SupportsWeakPtr { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONPROVIDER + + GCLocProviderPriv(); + + void UpdateLastPosition(); + + private: + class LocationTimerCallback final : public nsITimerCallback, public nsINamed { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSITIMERCALLBACK + + explicit LocationTimerCallback(GCLocProviderPriv* aParent) + : mParent(aParent) {} + + NS_IMETHOD GetName(nsACString& aName) override { + aName.AssignLiteral("GCLocProvider::LocationTimerCallback"); + return NS_OK; + } + + private: + ~LocationTimerCallback() = default; + WeakPtr<GCLocProviderPriv> mParent; + }; + + enum class Accuracy { Unset, Low, High }; + // States: + // Uninit: The default / initial state, with no client proxy yet. + // Initing: Takes care of establishing the client connection (GetClient / + // ConnectClient / SetDesktopID). + // SettingAccuracy: Does SetAccuracy operation, knows it should just go idle + // after finishing it. + // SettingAccuracyForStart: Does SetAccuracy operation, knows it then needs + // to do a Start operation after finishing it. + // Idle: Fully initialized, but not running state (quiescent). + // Starting: Starts the client by calling the Start D-Bus method. + // Started: Normal running state. + // Stopping: Stops the client by calling the Stop D-Bus method, knows it + // should just go idle after finishing it. + // StoppingForRestart: Stops the client by calling the Stop D-Bus method as + // a part of a Stop -> Start sequence (with possibly + // an accuracy update between these method calls). + // + // Valid state transitions are: + // (any state) -> Uninit: Transition when a D-Bus call failed or + // provided invalid data. + // + // Watch() startup path: + // Uninit -> Initing: Transition after getting the very first Watch() + // request + // or any such request while not having the client proxy. + // Initing -> SettingAccuracyForStart: Transition after getting a successful + // SetDesktopID response. + // SettingAccuracyForStart -> Starting: Transition after getting a + // successful + // SetAccuracy response. + // Idle -> Starting: Transition after getting a Watch() request while in + // fully + // initialized, but not running state. + // SettingAccuracy -> SettingAccuracyForStart: Transition after getting a + // Watch() + // request in the middle of + // setting accuracy during idle + // status. + // Stopping -> StoppingForRestart: Transition after getting a Watch() + // request + // in the middle of doing a Stop D-Bus call + // for idle status. + // StoppingForRestart -> Starting: Transition after getting a successful + // Stop response as a part of a Stop -> + // Start sequence while the previously set + // accuracy is still correct. + // StoppingForRestart -> SettingAccuracyForStart: Transition after getting + // a successful Stop response + // as a part of a Stop -> + // Start sequence but the set + // accuracy needs updating. + // Starting -> Started: Transition after getting a successful Start + // response. + // + // Shutdown() path: + // (any state) -> Uninit: Transition when not reusing the client proxy for + // any reason. + // Started -> Stopping: Transition from normal running state when reusing + // the client proxy. + // SettingAccuracyForStart -> SettingAccuracy: Transition when doing + // a shutdown in the middle of + // setting accuracy for a start + // when reusing the client + // proxy. + // SettingAccuracy -> Idle: Transition after getting a successful + // SetAccuracy + // response. + // StoppingForRestart -> Stopping: Transition when doing shutdown + // in the middle of a Stop -> Start sequence + // when reusing the client proxy. + // Stopping -> Idle: Transition after getting a successful Stop response. + // + // SetHighAccuracy() path: + // Started -> StoppingForRestart: Transition when accuracy needs updating + // on a running client. + // (the rest of the flow in StoppingForRestart state is the same as when + // being in this state in the Watch() startup path) + enum class ClientState { + Uninit, + Initing, + SettingAccuracy, + SettingAccuracyForStart, + Idle, + Starting, + Started, + Stopping, + StoppingForRestart + }; + + ~GCLocProviderPriv(); + + static bool AlwaysHighAccuracy(); + + void SetState(ClientState aNewState, const char* aNewStateStr); + + void Update(nsIDOMGeoPosition* aPosition); + MOZ_CAN_RUN_SCRIPT void NotifyError(int aError); + MOZ_CAN_RUN_SCRIPT void DBusProxyError(const GError* aGError, + bool aResetManager = false); + + MOZ_CAN_RUN_SCRIPT static void GetClientResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData); + void ConnectClient(const gchar* aClientPath); + MOZ_CAN_RUN_SCRIPT static void ConnectClientResponse(GObject* aObject, + GAsyncResult* aResult, + gpointer aUserData); + void SetDesktopID(); + MOZ_CAN_RUN_SCRIPT static void SetDesktopIDResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData); + void SetAccuracy(); + MOZ_CAN_RUN_SCRIPT static void SetAccuracyResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData); + void StartClient(); + MOZ_CAN_RUN_SCRIPT static void StartClientResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData); + void StopClient(bool aForRestart); + MOZ_CAN_RUN_SCRIPT static void StopClientResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData); + void StopClientNoWait(); + void MaybeRestartForAccuracy(); + + MOZ_CAN_RUN_SCRIPT static void GCManagerOwnerNotify(GObject* aObject, + GParamSpec* aPSpec, + gpointer aUserData); + + static void GCClientSignal(GDBusProxy* aProxy, gchar* aSenderName, + gchar* aSignalName, GVariant* aParameters, + gpointer aUserData); + void ConnectLocation(const gchar* aLocationPath); + static bool GetLocationProperty(GDBusProxy* aProxyLocation, + const gchar* aName, double* aOut); + static void ConnectLocationResponse(GObject* aObject, GAsyncResult* aResult, + gpointer aUserData); + + void SetLocationTimer(); + void StopLocationTimer(); + + bool InDBusCall(); + bool InDBusStoppingCall(); + bool InDBusStoppedCall(); + + void DeleteManager(); + void DoShutdown(bool aDeleteClient, bool aDeleteManager); + void DoShutdownClearCallback(bool aDestroying); + + nsresult FallbackToMLS(); + void StopMLSFallback(); + + void WatchStart(); + + Accuracy mAccuracyWanted = Accuracy::Unset; + Accuracy mAccuracySet = Accuracy::Unset; + RefPtr<GDBusProxy> mProxyManager; + RefPtr<GDBusProxy> mProxyClient; + RefPtr<GCancellable> mCancellable; + nsCOMPtr<nsIGeolocationUpdate> mCallback; + ClientState mClientState = ClientState::Uninit; + RefPtr<nsIDOMGeoPosition> mLastPosition; + RefPtr<nsITimer> mLocationTimer; + RefPtr<MLSFallback> mMLSFallback; +}; + +// +// GCLocProviderPriv +// + +#define GCLP_SETSTATE(this, state) this->SetState(ClientState::state, #state) + +GCLocProviderPriv::GCLocProviderPriv() { + if (AlwaysHighAccuracy()) { + mAccuracyWanted = Accuracy::High; + } else { + mAccuracyWanted = Accuracy::Low; + } +} + +GCLocProviderPriv::~GCLocProviderPriv() { DoShutdownClearCallback(true); } + +bool GCLocProviderPriv::AlwaysHighAccuracy() { + return StaticPrefs::geo_provider_geoclue_always_high_accuracy(); +} + +void GCLocProviderPriv::SetState(ClientState aNewState, + const char* aNewStateStr) { + if (mClientState == aNewState) { + return; + } + + GCL_LOG(Debug, "changing state to %s", aNewStateStr); + mClientState = aNewState; +} + +void GCLocProviderPriv::Update(nsIDOMGeoPosition* aPosition) { + if (!mCallback) { + return; + } + + mCallback->Update(aPosition); +} + +void GCLocProviderPriv::UpdateLastPosition() { + MOZ_DIAGNOSTIC_ASSERT(mLastPosition, "No last position to update"); + StopLocationTimer(); + Update(mLastPosition); +} + +nsresult GCLocProviderPriv::FallbackToMLS() { + GCL_LOG(Debug, "trying to fall back to MLS"); + StopMLSFallback(); + + RefPtr fallback = new MLSFallback(0); + MOZ_TRY(fallback->Startup(mCallback)); + + GCL_LOG(Debug, "Started up MLS fallback"); + mMLSFallback = std::move(fallback); + return NS_OK; +} + +void GCLocProviderPriv::StopMLSFallback() { + if (!mMLSFallback) { + return; + } + GCL_LOG(Debug, "Clearing MLS fallback"); + if (mMLSFallback) { + mMLSFallback->Shutdown(); + mMLSFallback = nullptr; + } +} + +void GCLocProviderPriv::NotifyError(int aError) { + if (!mCallback) { + return; + } + + // We errored out, try to fall back to MLS. + if (NS_SUCCEEDED(FallbackToMLS())) { + return; + } + + nsCOMPtr callback = mCallback; + callback->NotifyError(aError); +} + +void GCLocProviderPriv::DBusProxyError(const GError* aGError, + bool aResetManager) { + // that G_DBUS_ERROR below is actually a function call, not a constant + GQuark gdbusDomain = G_DBUS_ERROR; + int error = GeolocationPositionError_Binding::POSITION_UNAVAILABLE; + if (aGError) { + if (g_error_matches(aGError, gdbusDomain, G_DBUS_ERROR_TIMEOUT) || + g_error_matches(aGError, gdbusDomain, G_DBUS_ERROR_TIMED_OUT)) { + error = GeolocationPositionError_Binding::TIMEOUT; + } else if (g_error_matches(aGError, gdbusDomain, + G_DBUS_ERROR_LIMITS_EXCEEDED) || + g_error_matches(aGError, gdbusDomain, + G_DBUS_ERROR_ACCESS_DENIED) || + g_error_matches(aGError, gdbusDomain, + G_DBUS_ERROR_AUTH_FAILED)) { + error = GeolocationPositionError_Binding::PERMISSION_DENIED; + } + } + + DoShutdown(true, aResetManager); + NotifyError(error); +} + +void GCLocProviderPriv::GetClientResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData) { + GUniquePtr<GError> error; + RefPtr<GVariant> variant = dont_AddRef( + g_dbus_proxy_call_finish(aProxy, aResult, getter_Transfers(error))); + if (!variant) { + // if cancelled |self| might no longer be there + if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + GCL_LOG(Error, "Failed to get client: %s\n", error->message); + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + self->DBusProxyError(error.get(), true); + } + return; + } + + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + MOZ_DIAGNOSTIC_ASSERT(self->mClientState == ClientState::Initing, + "Client in a wrong state"); + + auto signalError = MakeScopeExit([&]() MOZ_CAN_RUN_SCRIPT_BOUNDARY { + self->DBusProxyError(nullptr, true); + }); + + if (!g_variant_is_of_type(variant, G_VARIANT_TYPE_TUPLE)) { + GCL_LOG(Error, "Unexpected get client call return type: %s\n", + g_variant_get_type_string(variant)); + return; + } + + if (g_variant_n_children(variant) < 1) { + GCL_LOG(Error, + "Not enough params in get client call return: %" G_GSIZE_FORMAT + "\n", + g_variant_n_children(variant)); + return; + } + + variant = dont_AddRef(g_variant_get_child_value(variant, 0)); + if (!g_variant_is_of_type(variant, G_VARIANT_TYPE_OBJECT_PATH)) { + GCL_LOG(Error, "Unexpected get client call return type inside tuple: %s\n", + g_variant_get_type_string(variant)); + return; + } + + const gchar* clientPath = g_variant_get_string(variant, nullptr); + GCL_LOG(Debug, "Client path: %s\n", clientPath); + + signalError.release(); + self->ConnectClient(clientPath); +} + +void GCLocProviderPriv::ConnectClient(const gchar* aClientPath) { + MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Initing, + "Client in a wrong state"); + MOZ_ASSERT(mCancellable, "Watch() wasn't successfully called"); + g_dbus_proxy_new_for_bus( + G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, nullptr, kGeoclueBusName, + aClientPath, kGCClientInterface, mCancellable, + reinterpret_cast<GAsyncReadyCallback>(ConnectClientResponse), this); +} + +void GCLocProviderPriv::ConnectClientResponse(GObject* aObject, + GAsyncResult* aResult, + gpointer aUserData) { + GUniquePtr<GError> error; + RefPtr<GDBusProxy> proxyClient = + dont_AddRef(g_dbus_proxy_new_finish(aResult, getter_Transfers(error))); + if (!proxyClient) { + // if cancelled |self| might no longer be there + if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + GCL_LOG(Error, "Failed to connect to client: %s\n", error->message); + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + self->DBusProxyError(error.get()); + } + return; + } + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + self->mProxyClient = std::move(proxyClient); + + MOZ_DIAGNOSTIC_ASSERT(self->mClientState == ClientState::Initing, + "Client in a wrong state"); + + GCL_LOG(Info, "Client interface connected\n"); + + g_signal_connect(self->mProxyClient, "g-signal", G_CALLBACK(GCClientSignal), + self); + self->SetDesktopID(); +} + +void GCLocProviderPriv::SetDesktopID() { + MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Initing, + "Client in a wrong state"); + MOZ_DIAGNOSTIC_ASSERT(mProxyClient && mCancellable, + "Watch() wasn't successfully called"); + + nsAutoCString appName; + gAppData->GetDBusAppName(appName); + g_dbus_proxy_call(mProxyClient, kDBPropertySetMethod, + g_variant_new("(ssv)", kGCClientInterface, "DesktopId", + g_variant_new_string(appName.get())), + G_DBUS_CALL_FLAGS_NONE, -1, mCancellable, + reinterpret_cast<GAsyncReadyCallback>(SetDesktopIDResponse), + this); +} + +void GCLocProviderPriv::SetDesktopIDResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData) { + GUniquePtr<GError> error; + + RefPtr<GVariant> variant = dont_AddRef( + g_dbus_proxy_call_finish(aProxy, aResult, getter_Transfers(error))); + if (!variant) { + // if cancelled |self| might no longer be there + if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + GCL_LOG(Error, "Failed to set DesktopId: %s\n", error->message); + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + self->DBusProxyError(error.get()); + } + return; + } + + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + MOZ_DIAGNOSTIC_ASSERT(self->mClientState == ClientState::Initing, + "Client in a wrong state"); + + GCLP_SETSTATE(self, Idle); + self->SetAccuracy(); +} + +void GCLocProviderPriv::SetAccuracy() { + MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Idle, + "Client in a wrong state"); + MOZ_DIAGNOSTIC_ASSERT(mProxyClient && mCancellable, + "Watch() wasn't successfully called"); + MOZ_ASSERT(mAccuracyWanted != Accuracy::Unset, "Invalid accuracy"); + + guint32 accuracy; + if (mAccuracyWanted == Accuracy::High) { + accuracy = (guint32)GCAccuracyLevel::Exact; + } else { + accuracy = (guint32)GCAccuracyLevel::City; + } + + mAccuracySet = mAccuracyWanted; + GCLP_SETSTATE(this, SettingAccuracyForStart); + g_dbus_proxy_call( + mProxyClient, kDBPropertySetMethod, + g_variant_new("(ssv)", kGCClientInterface, "RequestedAccuracyLevel", + g_variant_new_uint32(accuracy)), + G_DBUS_CALL_FLAGS_NONE, -1, mCancellable, + reinterpret_cast<GAsyncReadyCallback>(SetAccuracyResponse), this); +} + +void GCLocProviderPriv::SetAccuracyResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData) { + GUniquePtr<GError> error; + RefPtr<GVariant> variant = dont_AddRef( + g_dbus_proxy_call_finish(aProxy, aResult, getter_Transfers(error))); + if (!variant) { + // if cancelled |self| might no longer be there + if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + GCL_LOG(Error, "Failed to set requested accuracy level: %s\n", + error->message); + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + self->DBusProxyError(error.get()); + } + return; + } + + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + MOZ_DIAGNOSTIC_ASSERT( + self->mClientState == ClientState::SettingAccuracyForStart || + self->mClientState == ClientState::SettingAccuracy, + "Client in a wrong state"); + bool wantStart = self->mClientState == ClientState::SettingAccuracyForStart; + GCLP_SETSTATE(self, Idle); + + if (wantStart) { + self->StartClient(); + } +} + +void GCLocProviderPriv::StartClient() { + MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Idle, + "Client in a wrong state"); + MOZ_DIAGNOSTIC_ASSERT(mProxyClient && mCancellable, + "Watch() wasn't successfully called"); + GCLP_SETSTATE(this, Starting); + g_dbus_proxy_call( + mProxyClient, "Start", nullptr, G_DBUS_CALL_FLAGS_NONE, -1, mCancellable, + reinterpret_cast<GAsyncReadyCallback>(StartClientResponse), this); +} + +void GCLocProviderPriv::StartClientResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData) { + GUniquePtr<GError> error; + + RefPtr<GVariant> variant = dont_AddRef( + g_dbus_proxy_call_finish(aProxy, aResult, getter_Transfers(error))); + if (!variant) { + // if cancelled |self| might no longer be there + if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + GCL_LOG(Error, "Failed to start client: %s\n", error->message); + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + /* + * A workaround for + * https://gitlab.freedesktop.org/geoclue/geoclue/-/issues/143 We need to + * get a new client instance once the agent finally connects to the + * Geoclue service, otherwise every Start request on the old client + * interface will be denied. We need to reconnect to the Manager interface + * to achieve this since otherwise GetClient call will simply return the + * old client instance. + */ + bool resetManager = g_error_matches(error.get(), G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED); + self->DBusProxyError(error.get(), resetManager); + } + return; + } + + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + MOZ_DIAGNOSTIC_ASSERT(self->mClientState == ClientState::Starting, + "Client in a wrong state"); + GCLP_SETSTATE(self, Started); + self->MaybeRestartForAccuracy(); +} + +void GCLocProviderPriv::StopClient(bool aForRestart) { + MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Started, + "Client in a wrong state"); + MOZ_DIAGNOSTIC_ASSERT(mProxyClient && mCancellable, + "Watch() wasn't successfully called"); + + if (aForRestart) { + GCLP_SETSTATE(this, StoppingForRestart); + } else { + GCLP_SETSTATE(this, Stopping); + } + + g_dbus_proxy_call( + mProxyClient, "Stop", nullptr, G_DBUS_CALL_FLAGS_NONE, -1, mCancellable, + reinterpret_cast<GAsyncReadyCallback>(StopClientResponse), this); +} + +void GCLocProviderPriv::StopClientResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData) { + GUniquePtr<GError> error; + RefPtr<GVariant> variant = dont_AddRef( + g_dbus_proxy_call_finish(aProxy, aResult, getter_Transfers(error))); + if (!variant) { + // if cancelled |self| might no longer be there + if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + GCL_LOG(Error, "Failed to stop client: %s\n", error->message); + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + self->DBusProxyError(error.get()); + } + return; + } + + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + MOZ_DIAGNOSTIC_ASSERT(self->InDBusStoppingCall(), "Client in a wrong state"); + bool wantRestart = self->mClientState == ClientState::StoppingForRestart; + GCLP_SETSTATE(self, Idle); + + if (!wantRestart) { + return; + } + + if (self->mAccuracyWanted != self->mAccuracySet) { + self->SetAccuracy(); + } else { + self->StartClient(); + } +} + +void GCLocProviderPriv::StopClientNoWait() { + MOZ_DIAGNOSTIC_ASSERT(mProxyClient, "Watch() wasn't successfully called"); + g_dbus_proxy_call(mProxyClient, "Stop", nullptr, G_DBUS_CALL_FLAGS_NONE, -1, + nullptr, nullptr, nullptr); +} + +void GCLocProviderPriv::MaybeRestartForAccuracy() { + if (mAccuracyWanted == mAccuracySet) { + return; + } + + if (mClientState != ClientState::Started) { + return; + } + + // Setting a new accuracy requires restarting the client + StopClient(true); +} + +void GCLocProviderPriv::GCManagerOwnerNotify(GObject* aObject, + GParamSpec* aPSpec, + gpointer aUserData) { + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + GUniquePtr<gchar> managerOwner( + g_dbus_proxy_get_name_owner(self->mProxyManager)); + if (!managerOwner) { + GCL_LOG(Info, "The Manager interface has lost its owner\n"); + self->DBusProxyError(nullptr, true); + } +} + +void GCLocProviderPriv::GCClientSignal(GDBusProxy* aProxy, gchar* aSenderName, + gchar* aSignalName, + GVariant* aParameters, + gpointer aUserData) { + if (g_strcmp0(aSignalName, "LocationUpdated")) { + return; + } + + if (!g_variant_is_of_type(aParameters, G_VARIANT_TYPE_TUPLE)) { + GCL_LOG(Error, "Unexpected location updated signal params type: %s\n", + g_variant_get_type_string(aParameters)); + return; + } + + if (g_variant_n_children(aParameters) < 2) { + GCL_LOG(Error, + "Not enough params in location updated signal: %" G_GSIZE_FORMAT + "\n", + g_variant_n_children(aParameters)); + return; + } + + RefPtr<GVariant> variant = + dont_AddRef(g_variant_get_child_value(aParameters, 1)); + if (!g_variant_is_of_type(variant, G_VARIANT_TYPE_OBJECT_PATH)) { + GCL_LOG(Error, + "Unexpected location updated signal new location path type: %s\n", + g_variant_get_type_string(variant)); + return; + } + + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + const gchar* locationPath = g_variant_get_string(variant, nullptr); + GCL_LOG(Verbose, "New location path: %s\n", locationPath); + self->ConnectLocation(locationPath); +} + +void GCLocProviderPriv::ConnectLocation(const gchar* aLocationPath) { + MOZ_ASSERT(mCancellable, "Startup() wasn't successfully called"); + g_dbus_proxy_new_for_bus( + G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, nullptr, kGeoclueBusName, + aLocationPath, kGCLocationInterface, mCancellable, + reinterpret_cast<GAsyncReadyCallback>(ConnectLocationResponse), this); +} + +bool GCLocProviderPriv::GetLocationProperty(GDBusProxy* aProxyLocation, + const gchar* aName, double* aOut) { + RefPtr<GVariant> property = + dont_AddRef(g_dbus_proxy_get_cached_property(aProxyLocation, aName)); + if (!g_variant_is_of_type(property, G_VARIANT_TYPE_DOUBLE)) { + GCL_LOG(Error, "Unexpected location property %s type: %s\n", aName, + g_variant_get_type_string(property)); + return false; + } + + *aOut = g_variant_get_double(property); + return true; +} + +void GCLocProviderPriv::ConnectLocationResponse(GObject* aObject, + GAsyncResult* aResult, + gpointer aUserData) { + GUniquePtr<GError> error; + RefPtr<GDBusProxy> proxyLocation = + dont_AddRef(g_dbus_proxy_new_finish(aResult, getter_Transfers(error))); + if (!proxyLocation) { + if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + GCL_LOG(Warning, "Failed to connect to location: %s\n", error->message); + } + return; + } + + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + /* + * nsGeoPositionCoords will convert NaNs to null for optional properties of + * the JavaScript Coordinates object. + */ + double lat = UnspecifiedNaN<double>(); + double lon = UnspecifiedNaN<double>(); + double alt = UnspecifiedNaN<double>(); + double hError = UnspecifiedNaN<double>(); + const double vError = UnspecifiedNaN<double>(); + double heading = UnspecifiedNaN<double>(); + double speed = UnspecifiedNaN<double>(); + struct { + const gchar* name; + double* out; + } props[] = { + {"Latitude", &lat}, {"Longitude", &lon}, {"Altitude", &alt}, + {"Accuracy", &hError}, {"Heading", &heading}, {"Speed", &speed}, + }; + + for (auto& prop : props) { + if (!GetLocationProperty(proxyLocation, prop.name, prop.out)) { + return; + } + } + + if (alt < kGCMinAlt) { + alt = UnspecifiedNaN<double>(); + } + if (speed < 0) { + speed = UnspecifiedNaN<double>(); + } + if (heading < 0 || std::isnan(speed) || speed == 0) { + heading = UnspecifiedNaN<double>(); + } + + GCL_LOG(Info, "New location: %f %f +-%fm @ %gm; hdg %f spd %fm/s\n", lat, lon, + hError, alt, heading, speed); + + self->mLastPosition = + new nsGeoPosition(lat, lon, alt, hError, vError, heading, speed, + PR_Now() / PR_USEC_PER_MSEC); + self->UpdateLastPosition(); +} + +void GCLocProviderPriv::SetLocationTimer() { + MOZ_DIAGNOSTIC_ASSERT(mLastPosition, "no last position to report"); + + StopLocationTimer(); + + RefPtr<LocationTimerCallback> timerCallback = new LocationTimerCallback(this); + NS_NewTimerWithCallback(getter_AddRefs(mLocationTimer), timerCallback, 1000, + nsITimer::TYPE_ONE_SHOT); +} + +void GCLocProviderPriv::StopLocationTimer() { + if (!mLocationTimer) { + return; + } + + mLocationTimer->Cancel(); + mLocationTimer = nullptr; +} + +// Did we made some D-Bus call and are still waiting for its response? +bool GCLocProviderPriv::InDBusCall() { + return mClientState == ClientState::Initing || + mClientState == ClientState::SettingAccuracy || + mClientState == ClientState::SettingAccuracyForStart || + mClientState == ClientState::Starting || + mClientState == ClientState::Stopping || + mClientState == ClientState::StoppingForRestart; +} + +bool GCLocProviderPriv::InDBusStoppingCall() { + return mClientState == ClientState::Stopping || + mClientState == ClientState::StoppingForRestart; +} + +/* + * Did we made some D-Bus call while stopped and + * are still waiting for its response? + */ +bool GCLocProviderPriv::InDBusStoppedCall() { + return mClientState == ClientState::SettingAccuracy || + mClientState == ClientState::SettingAccuracyForStart; +} + +void GCLocProviderPriv::DeleteManager() { + if (!mProxyManager) { + return; + } + + g_signal_handlers_disconnect_matched(mProxyManager, G_SIGNAL_MATCH_DATA, 0, 0, + nullptr, nullptr, this); + mProxyManager = nullptr; +} + +void GCLocProviderPriv::DoShutdown(bool aDeleteClient, bool aDeleteManager) { + MOZ_DIAGNOSTIC_ASSERT( + !aDeleteManager || aDeleteClient, + "deleting manager proxy requires deleting client one, too"); + + // Invalidate the cached last position + StopLocationTimer(); + mLastPosition = nullptr; + + /* + * Do we need to delete the D-Bus proxy (or proxies)? + * Either because that's what our caller wanted, or because we are set to + * never reuse them, or because we are in a middle of some D-Bus call while + * having the service running (and so not being able to issue an immediate + * Stop call). + */ + if (aDeleteClient || !kGCReuseDBusProxy || + (InDBusCall() && !InDBusStoppingCall() && !InDBusStoppedCall())) { + if (mClientState == ClientState::Started) { + StopClientNoWait(); + GCLP_SETSTATE(this, Idle); + } + if (mProxyClient) { + g_signal_handlers_disconnect_matched(mProxyClient, G_SIGNAL_MATCH_DATA, 0, + 0, nullptr, nullptr, this); + } + if (mCancellable) { + g_cancellable_cancel(mCancellable); + mCancellable = nullptr; + } + mProxyClient = nullptr; + + if (aDeleteManager || !kGCReuseDBusProxy) { + DeleteManager(); + } + + GCLP_SETSTATE(this, Uninit); + } else if (mClientState == ClientState::Started) { + StopClient(false); + } else if (mClientState == ClientState::SettingAccuracyForStart) { + GCLP_SETSTATE(this, SettingAccuracy); + } else if (mClientState == ClientState::StoppingForRestart) { + GCLP_SETSTATE(this, Stopping); + } +} + +void GCLocProviderPriv::DoShutdownClearCallback(bool aDestroying) { + mCallback = nullptr; + StopMLSFallback(); + DoShutdown(aDestroying, aDestroying); +} + +NS_IMPL_ISUPPORTS(GCLocProviderPriv, nsIGeolocationProvider) + +// nsIGeolocationProvider +// + +/* + * The Startup() method should only succeed if Geoclue is available on D-Bus + * so it can be used for determining whether to continue with this geolocation + * provider in Geolocation.cpp + */ +NS_IMETHODIMP +GCLocProviderPriv::Startup() { + if (mProxyManager) { + return NS_OK; + } + + MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Uninit, + "Client in a initialized state but no manager"); + + GUniquePtr<GError> error; + mProxyManager = dont_AddRef(g_dbus_proxy_new_for_bus_sync( + G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, nullptr, kGeoclueBusName, + kGCManagerPath, kGCManagerInterface, nullptr, getter_Transfers(error))); + if (!mProxyManager) { + GCL_LOG(Info, "Cannot connect to the Manager interface: %s\n", + error->message); + return NS_ERROR_FAILURE; + } + + g_signal_connect(mProxyManager, "notify::g-name-owner", + G_CALLBACK(GCManagerOwnerNotify), this); + + GUniquePtr<gchar> managerOwner(g_dbus_proxy_get_name_owner(mProxyManager)); + if (!managerOwner) { + GCL_LOG(Info, "The Manager interface has no owner\n"); + DeleteManager(); + return NS_ERROR_FAILURE; + } + + GCL_LOG(Info, "Manager interface connected successfully\n"); + + return NS_OK; +} + +void GCLocProviderPriv::WatchStart() { + if (mClientState == ClientState::Idle) { + StartClient(); + } else if (mClientState == ClientState::Started) { + if (mLastPosition && !mLocationTimer) { + GCL_LOG(Verbose, + "Will report the existing location if new one doesn't come up\n"); + SetLocationTimer(); + } + } else if (mClientState == ClientState::SettingAccuracy) { + GCLP_SETSTATE(this, SettingAccuracyForStart); + } else if (mClientState == ClientState::Stopping) { + GCLP_SETSTATE(this, StoppingForRestart); + } +} + +NS_IMETHODIMP +GCLocProviderPriv::Watch(nsIGeolocationUpdate* aCallback) { + mCallback = aCallback; + + if (!mCancellable) { + mCancellable = dont_AddRef(g_cancellable_new()); + } + + if (mClientState != ClientState::Uninit) { + WatchStart(); + return NS_OK; + } + + if (!mProxyManager) { + GCL_LOG(Debug, "watch request falling back to MLS"); + return FallbackToMLS(); + } + + StopMLSFallback(); + + GCLP_SETSTATE(this, Initing); + g_dbus_proxy_call(mProxyManager, "GetClient", nullptr, G_DBUS_CALL_FLAGS_NONE, + -1, mCancellable, + reinterpret_cast<GAsyncReadyCallback>(GetClientResponse), + this); + + return NS_OK; +} + +NS_IMETHODIMP +GCLocProviderPriv::Shutdown() { + DoShutdownClearCallback(false); + return NS_OK; +} + +NS_IMETHODIMP +GCLocProviderPriv::SetHighAccuracy(bool aHigh) { + GCL_LOG(Verbose, "Want %s accuracy\n", aHigh ? "high" : "low"); + if (!aHigh && AlwaysHighAccuracy()) { + GCL_LOG(Verbose, "Forcing high accuracy due to pref\n"); + aHigh = true; + } + + mAccuracyWanted = aHigh ? Accuracy::High : Accuracy::Low; + MaybeRestartForAccuracy(); + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(GCLocProviderPriv::LocationTimerCallback, nsITimerCallback, + nsINamed) + +NS_IMETHODIMP +GCLocProviderPriv::LocationTimerCallback::Notify(nsITimer* aTimer) { + if (mParent) { + RefPtr<GCLocProviderPriv> parent(mParent); + parent->UpdateLastPosition(); + } + + return NS_OK; +} + +GeoclueLocationProvider::GeoclueLocationProvider() { + mPriv = new GCLocProviderPriv; +} + +// nsISupports +// + +NS_IMPL_ISUPPORTS(GeoclueLocationProvider, nsIGeolocationProvider) + +// nsIGeolocationProvider +// + +NS_IMETHODIMP +GeoclueLocationProvider::Startup() { return mPriv->Startup(); } + +NS_IMETHODIMP +GeoclueLocationProvider::Watch(nsIGeolocationUpdate* aCallback) { + return mPriv->Watch(aCallback); +} + +NS_IMETHODIMP +GeoclueLocationProvider::Shutdown() { return mPriv->Shutdown(); } + +NS_IMETHODIMP +GeoclueLocationProvider::SetHighAccuracy(bool aHigh) { + return mPriv->SetHighAccuracy(aHigh); +} + +} // namespace mozilla::dom diff --git a/dom/system/linux/GeoclueLocationProvider.h b/dom/system/linux/GeoclueLocationProvider.h new file mode 100644 index 0000000000..908cd25e37 --- /dev/null +++ b/dom/system/linux/GeoclueLocationProvider.h @@ -0,0 +1,32 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#ifndef GeoclueLocationProvider_h +#define GeoclueLocationProvider_h + +#include "mozilla/RefPtr.h" +#include "nsIGeolocationProvider.h" + +namespace mozilla::dom { + +class GCLocProviderPriv; + +class GeoclueLocationProvider final : public nsIGeolocationProvider { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONPROVIDER + + GeoclueLocationProvider(); + + private: + ~GeoclueLocationProvider() = default; + + RefPtr<GCLocProviderPriv> mPriv; +}; + +} // namespace mozilla::dom + +#endif /* GeoclueLocationProvider_h */ diff --git a/dom/system/linux/GpsdLocationProvider.cpp b/dom/system/linux/GpsdLocationProvider.cpp new file mode 100644 index 0000000000..34cd23c453 --- /dev/null +++ b/dom/system/linux/GpsdLocationProvider.cpp @@ -0,0 +1,446 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "GpsdLocationProvider.h" +#include <errno.h> +#include <gps.h> +#include "MLSFallback.h" +#include "mozilla/Atomics.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/LazyIdleThread.h" +#include "mozilla/dom/GeolocationPositionErrorBinding.h" +#include "GeolocationPosition.h" +#include "nsProxyRelease.h" +#include "nsThreadUtils.h" +#include "prtime.h" + +namespace mozilla { +namespace dom { + +// +// MLSGeolocationUpdate +// + +/** + * |MLSGeolocationUpdate| provides a fallback if gpsd is not supported. + */ +class GpsdLocationProvider::MLSGeolocationUpdate final + : public nsIGeolocationUpdate { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONUPDATE + + explicit MLSGeolocationUpdate(nsIGeolocationUpdate* aCallback); + + protected: + ~MLSGeolocationUpdate() = default; + + private: + nsCOMPtr<nsIGeolocationUpdate> mCallback; +}; + +GpsdLocationProvider::MLSGeolocationUpdate::MLSGeolocationUpdate( + nsIGeolocationUpdate* aCallback) + : mCallback(aCallback) { + MOZ_ASSERT(mCallback); +} + +// nsISupports +// + +NS_IMPL_ISUPPORTS(GpsdLocationProvider::MLSGeolocationUpdate, + nsIGeolocationUpdate); + +// nsIGeolocationUpdate +// + +NS_IMETHODIMP +GpsdLocationProvider::MLSGeolocationUpdate::Update( + nsIDOMGeoPosition* aPosition) { + nsCOMPtr<nsIDOMGeoPositionCoords> coords; + aPosition->GetCoords(getter_AddRefs(coords)); + if (!coords) { + return NS_ERROR_FAILURE; + } + + return mCallback->Update(aPosition); +} + +NS_IMETHODIMP +GpsdLocationProvider::MLSGeolocationUpdate::NotifyError(uint16_t aError) { + return mCallback->NotifyError(aError); +} + +// +// UpdateRunnable +// + +class GpsdLocationProvider::UpdateRunnable final : public Runnable { + public: + UpdateRunnable( + const nsMainThreadPtrHandle<GpsdLocationProvider>& aLocationProvider, + nsIDOMGeoPosition* aPosition) + : Runnable("GpsdU"), + mLocationProvider(aLocationProvider), + mPosition(aPosition) { + MOZ_ASSERT(mLocationProvider); + MOZ_ASSERT(mPosition); + } + + // nsIRunnable + // + + NS_IMETHOD Run() override { + mLocationProvider->Update(mPosition); + return NS_OK; + } + + private: + nsMainThreadPtrHandle<GpsdLocationProvider> mLocationProvider; + RefPtr<nsIDOMGeoPosition> mPosition; +}; + +// +// NotifyErrorRunnable +// + +class GpsdLocationProvider::NotifyErrorRunnable final : public Runnable { + public: + NotifyErrorRunnable( + const nsMainThreadPtrHandle<GpsdLocationProvider>& aLocationProvider, + int aError) + : Runnable("GpsdNE"), + mLocationProvider(aLocationProvider), + mError(aError) { + MOZ_ASSERT(mLocationProvider); + } + + // nsIRunnable + // + + NS_IMETHOD Run() override { + mLocationProvider->NotifyError(mError); + return NS_OK; + } + + private: + nsMainThreadPtrHandle<GpsdLocationProvider> mLocationProvider; + int mError; +}; + +// +// PollRunnable +// + +/** + * |PollRunnable| does the main work of processing GPS data received + * from gpsd. libgps blocks while polling, so this runnable has to be + * executed on it's own thread. To cancel the poll runnable, invoke + * |StopRunning| and |PollRunnable| will stop within a reasonable time + * frame. + */ +class GpsdLocationProvider::PollRunnable final : public Runnable { + public: + PollRunnable( + const nsMainThreadPtrHandle<GpsdLocationProvider>& aLocationProvider) + : Runnable("GpsdP"), + mLocationProvider(aLocationProvider), + mRunning(true) { + MOZ_ASSERT(mLocationProvider); + } + + static bool IsSupported() { + return GPSD_API_MAJOR_VERSION >= 5 && GPSD_API_MAJOR_VERSION <= 12; + } + + bool IsRunning() const { return mRunning; } + + void StopRunning() { mRunning = false; } + + // nsIRunnable + // + + NS_IMETHOD Run() override { + int err; + + switch (GPSD_API_MAJOR_VERSION) { + case 5 ... 12: + err = PollLoop5(); + break; + default: + err = GeolocationPositionError_Binding::POSITION_UNAVAILABLE; + break; + } + + if (err) { + NS_DispatchToMainThread( + MakeAndAddRef<NotifyErrorRunnable>(mLocationProvider, err)); + } + + mLocationProvider = nullptr; + + return NS_OK; + } + + protected: + int PollLoop5() { +#if GPSD_API_MAJOR_VERSION >= 5 && GPSD_API_MAJOR_VERSION <= 12 + static const int GPSD_WAIT_TIMEOUT_US = + 1000000; /* us to wait for GPS data */ + + struct gps_data_t gpsData; + + auto res = gps_open(nullptr, nullptr, &gpsData); + + if (res < 0) { + return ErrnoToError(errno); + } + + gps_stream(&gpsData, WATCH_ENABLE | WATCH_JSON, NULL); + + int err = 0; + + // nsGeoPositionCoords will convert NaNs to null for optional properties of + // the JavaScript Coordinates object. + double lat = 0; + double lon = 0; + double alt = UnspecifiedNaN<double>(); + double hError = 0; + double vError = UnspecifiedNaN<double>(); + double heading = UnspecifiedNaN<double>(); + double speed = UnspecifiedNaN<double>(); + + while (IsRunning()) { + errno = 0; + auto hasGpsData = gps_waiting(&gpsData, GPSD_WAIT_TIMEOUT_US); + + if (errno) { + err = ErrnoToError(errno); + break; + } + if (!hasGpsData) { + continue; /* woke up from timeout */ + } + +# if GPSD_API_MAJOR_VERSION >= 7 + res = gps_read(&gpsData, nullptr, 0); +# else + + res = gps_read(&gpsData); +# endif + + if (res < 0) { + err = ErrnoToError(errno); + break; + } else if (!res) { + continue; /* no data available */ + } + +# if GPSD_API_MAJOR_VERSION < 10 + if (gpsData.status == STATUS_NO_FIX) { + continue; + } +# endif + + switch (gpsData.fix.mode) { + case MODE_3D: + double galt; + +# if GPSD_API_MAJOR_VERSION >= 9 + galt = gpsData.fix.altMSL; +# else + galt = gpsData.fix.altitude; +# endif + if (!std::isnan(galt)) { + alt = galt; + } + [[fallthrough]]; + case MODE_2D: + if (!std::isnan(gpsData.fix.latitude)) { + lat = gpsData.fix.latitude; + } + if (!std::isnan(gpsData.fix.longitude)) { + lon = gpsData.fix.longitude; + } + if (!std::isnan(gpsData.fix.epx) && !std::isnan(gpsData.fix.epy)) { + hError = std::max(gpsData.fix.epx, gpsData.fix.epy); + } else if (!std::isnan(gpsData.fix.epx)) { + hError = gpsData.fix.epx; + } else if (!std::isnan(gpsData.fix.epy)) { + hError = gpsData.fix.epy; + } + if (!std::isnan(gpsData.fix.epv)) { + vError = gpsData.fix.epv; + } + if (!std::isnan(gpsData.fix.track)) { + heading = gpsData.fix.track; + } + if (!std::isnan(gpsData.fix.speed)) { + speed = gpsData.fix.speed; + } + break; + default: + continue; // There's no useful data in this fix; continue. + } + + NS_DispatchToMainThread(MakeAndAddRef<UpdateRunnable>( + mLocationProvider, + new nsGeoPosition(lat, lon, alt, hError, vError, heading, speed, + PR_Now() / PR_USEC_PER_MSEC))); + } + + gps_stream(&gpsData, WATCH_DISABLE, NULL); + gps_close(&gpsData); + + return err; +#else + return GeolocationPositionError_Binding::POSITION_UNAVAILABLE; +#endif // GPSD_MAJOR_API_VERSION + } + + static int ErrnoToError(int aErrno) { + switch (aErrno) { + case EACCES: + [[fallthrough]]; + case EPERM: + [[fallthrough]]; + case EROFS: + return GeolocationPositionError_Binding::PERMISSION_DENIED; + case ETIME: + [[fallthrough]]; + case ETIMEDOUT: + return GeolocationPositionError_Binding::TIMEOUT; + default: + return GeolocationPositionError_Binding::POSITION_UNAVAILABLE; + } + } + + private: + nsMainThreadPtrHandle<GpsdLocationProvider> mLocationProvider; + Atomic<bool> mRunning; +}; + +// +// GpsdLocationProvider +// + +const uint32_t GpsdLocationProvider::GPSD_POLL_THREAD_TIMEOUT_MS = 5000; + +GpsdLocationProvider::GpsdLocationProvider() {} + +GpsdLocationProvider::~GpsdLocationProvider() {} + +void GpsdLocationProvider::Update(nsIDOMGeoPosition* aPosition) { + if (!mCallback || !mPollRunnable) { + return; // not initialized or already shut down + } + + if (mMLSProvider) { + /* We got a location from gpsd, so let's cancel our MLS fallback. */ + mMLSProvider->Shutdown(); + mMLSProvider = nullptr; + } + + mCallback->Update(aPosition); +} + +void GpsdLocationProvider::NotifyError(int aError) { + if (!mCallback) { + return; // not initialized or already shut down + } + + if (!mMLSProvider) { + /* With gpsd failed, we restart MLS. It will be canceled once we + * get another location from gpsd. + */ + mMLSProvider = MakeAndAddRef<MLSFallback>(); + mMLSProvider->Startup(new MLSGeolocationUpdate(mCallback)); + } + + mCallback->NotifyError(aError); +} + +// nsISupports +// + +NS_IMPL_ISUPPORTS(GpsdLocationProvider, nsIGeolocationProvider) + +// nsIGeolocationProvider +// + +NS_IMETHODIMP +GpsdLocationProvider::Startup() { + if (!PollRunnable::IsSupported()) { + return NS_OK; // We'll fall back to MLS. + } + + if (mPollRunnable) { + return NS_OK; // already running + } + + RefPtr<PollRunnable> pollRunnable = + MakeAndAddRef<PollRunnable>(nsMainThreadPtrHandle<GpsdLocationProvider>( + new nsMainThreadPtrHolder<GpsdLocationProvider>("GpsdLP", this))); + + // Use existing poll thread... + RefPtr<LazyIdleThread> pollThread = mPollThread; + + // ... or create a new one. + if (!pollThread) { + pollThread = MakeAndAddRef<LazyIdleThread>(GPSD_POLL_THREAD_TIMEOUT_MS, + "Gpsd poll thread", + LazyIdleThread::ManualShutdown); + } + + auto rv = pollThread->Dispatch(pollRunnable, NS_DISPATCH_NORMAL); + + if (NS_FAILED(rv)) { + return rv; + } + + mPollRunnable = pollRunnable.forget(); + mPollThread = pollThread.forget(); + + return NS_OK; +} + +NS_IMETHODIMP +GpsdLocationProvider::Watch(nsIGeolocationUpdate* aCallback) { + mCallback = aCallback; + + /* The MLS fallback will kick in after a few seconds if gpsd + * doesn't provide location information within time. Once we + * see the first message from gpsd, the fallback will be + * disabled in |Update|. + */ + mMLSProvider = MakeAndAddRef<MLSFallback>(); + mMLSProvider->Startup(new MLSGeolocationUpdate(aCallback)); + + return NS_OK; +} + +NS_IMETHODIMP +GpsdLocationProvider::Shutdown() { + if (mMLSProvider) { + mMLSProvider->Shutdown(); + mMLSProvider = nullptr; + } + + if (!mPollRunnable) { + return NS_OK; // not running + } + + mPollRunnable->StopRunning(); + mPollRunnable = nullptr; + + return NS_OK; +} + +NS_IMETHODIMP +GpsdLocationProvider::SetHighAccuracy(bool aHigh) { return NS_OK; } + +} // namespace dom +} // namespace mozilla diff --git a/dom/system/linux/GpsdLocationProvider.h b/dom/system/linux/GpsdLocationProvider.h new file mode 100644 index 0000000000..544f0e4c69 --- /dev/null +++ b/dom/system/linux/GpsdLocationProvider.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef GpsdLocationProvider_h +#define GpsdLocationProvider_h + +#include "nsCOMPtr.h" +#include "Geolocation.h" +#include "nsIGeolocationProvider.h" + +class MLSFallback; + +namespace mozilla { + +class LazyIdleThread; + +namespace dom { + +class GpsdLocationProvider final : public nsIGeolocationProvider { + class MLSGeolocationUpdate; + class NotifyErrorRunnable; + class PollRunnable; + class UpdateRunnable; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONPROVIDER + + GpsdLocationProvider(); + + private: + ~GpsdLocationProvider(); + + void Update(nsIDOMGeoPosition* aPosition); + void NotifyError(int aError); + + static const uint32_t GPSD_POLL_THREAD_TIMEOUT_MS; + + nsCOMPtr<nsIGeolocationUpdate> mCallback; + RefPtr<LazyIdleThread> mPollThread; + RefPtr<PollRunnable> mPollRunnable; + RefPtr<MLSFallback> mMLSProvider; +}; + +} // namespace dom +} // namespace mozilla + +#endif /* GpsLocationProvider_h */ diff --git a/dom/system/linux/PortalLocationProvider.cpp b/dom/system/linux/PortalLocationProvider.cpp new file mode 100644 index 0000000000..6ebb1854dc --- /dev/null +++ b/dom/system/linux/PortalLocationProvider.cpp @@ -0,0 +1,351 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "PortalLocationProvider.h" +#include "MLSFallback.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/Logging.h" +#include "mozilla/dom/GeolocationPositionErrorBinding.h" +#include "GeolocationPosition.h" +#include "prtime.h" +#include "mozilla/GUniquePtr.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/XREAppData.h" + +#include <gio/gio.h> +#include <glib-object.h> + +extern const mozilla::XREAppData* gAppData; + +namespace mozilla::dom { + +#ifdef MOZ_LOGGING +static LazyLogModule sPortalLog("Portal"); +# define LOG_PORTAL(...) MOZ_LOG(sPortalLog, LogLevel::Debug, (__VA_ARGS__)) +#else +# define LOG_PORTAL(...) +#endif /* MOZ_LOGGING */ + +const char kDesktopBusName[] = "org.freedesktop.portal.Desktop"; +const char kSessionInterfaceName[] = "org.freedesktop.portal.Session"; + +/** + * |MLSGeolocationUpdate| provides a fallback if Portal is not supported. + */ +class PortalLocationProvider::MLSGeolocationUpdate final + : public nsIGeolocationUpdate { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONUPDATE + + explicit MLSGeolocationUpdate(nsIGeolocationUpdate* aCallback); + + protected: + ~MLSGeolocationUpdate() = default; + + private: + const nsCOMPtr<nsIGeolocationUpdate> mCallback; +}; + +PortalLocationProvider::MLSGeolocationUpdate::MLSGeolocationUpdate( + nsIGeolocationUpdate* aCallback) + : mCallback(aCallback) { + MOZ_ASSERT(mCallback); +} + +NS_IMPL_ISUPPORTS(PortalLocationProvider::MLSGeolocationUpdate, + nsIGeolocationUpdate); + +// nsIGeolocationUpdate +// + +NS_IMETHODIMP +PortalLocationProvider::MLSGeolocationUpdate::Update( + nsIDOMGeoPosition* aPosition) { + nsCOMPtr<nsIDOMGeoPositionCoords> coords; + aPosition->GetCoords(getter_AddRefs(coords)); + if (!coords) { + return NS_ERROR_FAILURE; + } + LOG_PORTAL("MLS is updating position\n"); + return mCallback->Update(aPosition); +} + +NS_IMETHODIMP +PortalLocationProvider::MLSGeolocationUpdate::NotifyError(uint16_t aError) { + nsCOMPtr<nsIGeolocationUpdate> callback(mCallback); + return callback->NotifyError(aError); +} + +// +// PortalLocationProvider +// + +PortalLocationProvider::PortalLocationProvider() = default; + +PortalLocationProvider::~PortalLocationProvider() { + if (mDBUSLocationProxy || mRefreshTimer || mMLSProvider) { + NS_WARNING( + "PortalLocationProvider: Shutdown() had not been called before " + "destructor."); + Shutdown(); + } +} + +void PortalLocationProvider::Update(nsIDOMGeoPosition* aPosition) { + if (!mCallback) { + return; // not initialized or already shut down + } + + if (mMLSProvider) { + LOG_PORTAL( + "Update from location portal received: Cancelling fallback MLS " + "provider\n"); + mMLSProvider->Shutdown(); + mMLSProvider = nullptr; + } + + LOG_PORTAL("Send updated location to the callback %p", mCallback.get()); + mCallback->Update(aPosition); + + aPosition->GetCoords(getter_AddRefs(mLastGeoPositionCoords)); + // Schedule sending repetitive updates because we don't get more until + // position is changed from portal. That would lead to timeout on the + // Firefox side. + SetRefreshTimer(5000); +} + +void PortalLocationProvider::NotifyError(int aError) { + LOG_PORTAL("*****NotifyError %d\n", aError); + if (!mCallback) { + return; // not initialized or already shut down + } + + if (!mMLSProvider) { + /* With Portal failed, we restart MLS. It will be canceled once we + * get another location from Portal. Start it immediately. + */ + mMLSProvider = MakeAndAddRef<MLSFallback>(0); + mMLSProvider->Startup(new MLSGeolocationUpdate(mCallback)); + } + + nsCOMPtr<nsIGeolocationUpdate> callback(mCallback); + callback->NotifyError(aError); +} + +NS_IMPL_ISUPPORTS(PortalLocationProvider, nsIGeolocationProvider) + +static void location_updated_signal_cb(GDBusProxy* proxy, gchar* sender_name, + gchar* signal_name, GVariant* parameters, + gpointer user_data) { + LOG_PORTAL("Signal: %s received from: %s\n", sender_name, signal_name); + + if (g_strcmp0(signal_name, "LocationUpdated")) { + LOG_PORTAL("Unexpected signal %s received", signal_name); + return; + } + + auto* locationProvider = static_cast<PortalLocationProvider*>(user_data); + RefPtr<GVariant> response_data; + gchar* session_handle; + g_variant_get(parameters, "(o@a{sv})", &session_handle, + response_data.StartAssignment()); + if (!response_data) { + LOG_PORTAL("Missing response data from portal\n"); + locationProvider->NotifyError( + GeolocationPositionError_Binding::POSITION_UNAVAILABLE); + return; + } + LOG_PORTAL("Session handle: %s Response data: %s\n", session_handle, + GUniquePtr<gchar>(g_variant_print(response_data, TRUE)).get()); + g_free(session_handle); + + double lat = 0; + double lon = 0; + if (!g_variant_lookup(response_data, "Latitude", "d", &lat) || + !g_variant_lookup(response_data, "Longitude", "d", &lon)) { + locationProvider->NotifyError( + GeolocationPositionError_Binding::POSITION_UNAVAILABLE); + return; + } + + double alt = UnspecifiedNaN<double>(); + g_variant_lookup(response_data, "Altitude", "d", &alt); + double vError = 0; + double hError = UnspecifiedNaN<double>(); + g_variant_lookup(response_data, "Accuracy", "d", &hError); + double heading = UnspecifiedNaN<double>(); + g_variant_lookup(response_data, "Heading", "d", &heading); + double speed = UnspecifiedNaN<double>(); + g_variant_lookup(response_data, "Speed", "d", &speed); + + locationProvider->Update(new nsGeoPosition(lat, lon, alt, hError, vError, + heading, speed, + PR_Now() / PR_USEC_PER_MSEC)); +} + +NS_IMETHODIMP +PortalLocationProvider::Startup() { + LOG_PORTAL("Starting location portal"); + if (mDBUSLocationProxy) { + LOG_PORTAL("Proxy already started.\n"); + return NS_OK; + } + + // Create dbus proxy for the Location portal + GUniquePtr<GError> error; + mDBUSLocationProxy = dont_AddRef(g_dbus_proxy_new_for_bus_sync( + G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, + nullptr, /* GDBusInterfaceInfo */ + kDesktopBusName, "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Location", nullptr, /* GCancellable */ + getter_Transfers(error))); + if (!mDBUSLocationProxy) { + g_printerr("Error creating location dbus proxy: %s\n", error->message); + return NS_OK; // fallback to MLS + } + + // Listen to signals which will be send to us with the location data + mDBUSSignalHandler = + g_signal_connect(mDBUSLocationProxy, "g-signal", + G_CALLBACK(location_updated_signal_cb), this); + + // Call CreateSession of the location portal + GVariantBuilder builder; + + nsAutoCString appName; + gAppData->GetDBusAppName(appName); + g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add(&builder, "{sv}", "session_handle_token", + g_variant_new_string(appName.get())); + + RefPtr<GVariant> result = dont_AddRef(g_dbus_proxy_call_sync( + mDBUSLocationProxy, "CreateSession", g_variant_new("(a{sv})", &builder), + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, getter_Transfers(error))); + + g_variant_builder_clear(&builder); + + if (!result) { + g_printerr("Error calling CreateSession method: %s\n", error->message); + return NS_OK; // fallback to MLS + } + + // Start to listen to the location changes + g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); + + // TODO Use wayland:handle as described in + // https://flatpak.github.io/xdg-desktop-portal/#parent_window + const gchar* parent_window = ""; + gchar* portalSession; + g_variant_get_child(result, 0, "o", &portalSession); + mPortalSession.reset(portalSession); + + result = g_dbus_proxy_call_sync( + mDBUSLocationProxy, "Start", + g_variant_new("(osa{sv})", mPortalSession.get(), parent_window, &builder), + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, getter_Transfers(error)); + + g_variant_builder_clear(&builder); + + if (!result) { + g_printerr("Error calling Start method: %s\n", error->message); + return NS_OK; // fallback to MLS + } + return NS_OK; +} + +NS_IMETHODIMP +PortalLocationProvider::Watch(nsIGeolocationUpdate* aCallback) { + mCallback = aCallback; + + if (mLastGeoPositionCoords) { + // We cannot immediately call the Update there becase the window is not + // yet ready for that. + LOG_PORTAL( + "Update location in 1ms because we have the valid coords cached."); + SetRefreshTimer(1); + return NS_OK; + } + + /* The MLS fallback will kick in after 12 seconds if portal + * doesn't provide location information within time. Once we + * see the first message from portal, the fallback will be + * disabled in |Update|. + */ + mMLSProvider = MakeAndAddRef<MLSFallback>(12000); + mMLSProvider->Startup(new MLSGeolocationUpdate(aCallback)); + + return NS_OK; +} + +NS_IMETHODIMP PortalLocationProvider::GetName(nsACString& aName) { + aName.AssignLiteral("PortalLocationProvider"); + return NS_OK; +} + +void PortalLocationProvider::SetRefreshTimer(int aDelay) { + LOG_PORTAL("SetRefreshTimer for %p to %d ms\n", this, aDelay); + if (!mRefreshTimer) { + NS_NewTimerWithCallback(getter_AddRefs(mRefreshTimer), this, aDelay, + nsITimer::TYPE_ONE_SHOT); + } else { + mRefreshTimer->Cancel(); + mRefreshTimer->InitWithCallback(this, aDelay, nsITimer::TYPE_ONE_SHOT); + } +} + +NS_IMETHODIMP +PortalLocationProvider::Notify(nsITimer* timer) { + // We need to reschedule the timer because we won't get any update + // from portal until the location is changed. That would cause + // watchPosition to fail with TIMEOUT error. + SetRefreshTimer(5000); + if (mLastGeoPositionCoords) { + LOG_PORTAL("Update location callback with latest coords."); + mCallback->Update( + new nsGeoPosition(mLastGeoPositionCoords, PR_Now() / PR_USEC_PER_MSEC)); + } + return NS_OK; +} + +NS_IMETHODIMP +PortalLocationProvider::Shutdown() { + LOG_PORTAL("Shutdown location provider"); + if (mRefreshTimer) { + mRefreshTimer->Cancel(); + mRefreshTimer = nullptr; + } + mLastGeoPositionCoords = nullptr; + if (mDBUSLocationProxy) { + g_signal_handler_disconnect(mDBUSLocationProxy, mDBUSSignalHandler); + LOG_PORTAL("calling Close method to the session interface...\n"); + RefPtr<GDBusMessage> message = dont_AddRef(g_dbus_message_new_method_call( + kDesktopBusName, mPortalSession.get(), kSessionInterfaceName, "Close")); + mPortalSession = nullptr; + if (message) { + GUniquePtr<GError> error; + GDBusConnection* connection = + g_dbus_proxy_get_connection(mDBUSLocationProxy); + g_dbus_connection_send_message( + connection, message, G_DBUS_SEND_MESSAGE_FLAGS_NONE, + /*out_serial=*/nullptr, getter_Transfers(error)); + if (error) { + g_printerr("Failed to close the session: %s\n", error->message); + } + } + mDBUSLocationProxy = nullptr; + } + if (mMLSProvider) { + mMLSProvider->Shutdown(); + mMLSProvider = nullptr; + } + return NS_OK; +} + +NS_IMETHODIMP +PortalLocationProvider::SetHighAccuracy(bool aHigh) { return NS_OK; } + +} // namespace mozilla::dom diff --git a/dom/system/linux/PortalLocationProvider.h b/dom/system/linux/PortalLocationProvider.h new file mode 100644 index 0000000000..e7ead0ab5c --- /dev/null +++ b/dom/system/linux/PortalLocationProvider.h @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef PortalLocationProvider_h +#define PortalLocationProvider_h + +#include "nsCOMPtr.h" +#include "mozilla/GRefPtr.h" +#include "mozilla/GUniquePtr.h" +#include "Geolocation.h" +#include "nsIGeolocationProvider.h" +#include <gio/gio.h> + +class MLSFallback; + +namespace mozilla::dom { + +class PortalLocationProvider final : public nsIGeolocationProvider, + public nsITimerCallback, + public nsINamed { + class MLSGeolocationUpdate; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONPROVIDER + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + + PortalLocationProvider(); + + void Update(nsIDOMGeoPosition* aPosition); + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void NotifyError(int aError); + + private: + ~PortalLocationProvider(); + void SetRefreshTimer(int aDelay); + + RefPtr<GDBusProxy> mDBUSLocationProxy; + gulong mDBUSSignalHandler = 0; + + GUniquePtr<gchar> mPortalSession; + nsCOMPtr<nsIGeolocationUpdate> mCallback; + RefPtr<MLSFallback> mMLSProvider; + nsCOMPtr<nsIDOMGeoPositionCoords> mLastGeoPositionCoords; + nsCOMPtr<nsITimer> mRefreshTimer; +}; + +} // namespace mozilla::dom + +#endif /* GpsLocationProvider_h */ diff --git a/dom/system/linux/moz.build b/dom/system/linux/moz.build new file mode 100644 index 0000000000..9e7d6ef31d --- /dev/null +++ b/dom/system/linux/moz.build @@ -0,0 +1,23 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +if CONFIG["MOZ_GPSD"]: + SOURCES += ["GpsdLocationProvider.cpp"] + + CXXFLAGS += CONFIG["MOZ_GPSD_CFLAGS"] + + OS_LIBS += CONFIG["MOZ_GPSD_LIBS"] + + LOCAL_INCLUDES += ["/dom/geolocation"] + + +if CONFIG["MOZ_ENABLE_DBUS"]: + SOURCES += ["GeoclueLocationProvider.cpp"] + SOURCES += ["PortalLocationProvider.cpp"] + LOCAL_INCLUDES += ["/dom/geolocation"] + CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] + +FINAL_LIBRARY = "xul" diff --git a/dom/system/mac/CoreLocationLocationProvider.h b/dom/system/mac/CoreLocationLocationProvider.h new file mode 100644 index 0000000000..27b990cf9a --- /dev/null +++ b/dom/system/mac/CoreLocationLocationProvider.h @@ -0,0 +1,61 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsCOMPtr.h" +#include "nsIGeolocationProvider.h" +#include "mozilla/Attributes.h" + +/* + * The CoreLocationObjects class contains the CoreLocation objects + * we'll need. + * + * Declaring them directly in CoreLocationLocationProvider + * would require Objective-C++ syntax, which would contaminate all + * files that include this header and require them to be Objective-C++ + * as well. + * + * The solution then is to forward-declare CoreLocationObjects here and + * hold a pointer to it in CoreLocationLocationProvider, and only actually + * define it in CoreLocationLocationProvider.mm, thus making it safe + * for Geolocation.cpp, which is C++-only, to include this header. + */ +class CoreLocationObjects; +class MLSFallback; + +class CoreLocationLocationProvider : public nsIGeolocationProvider { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONPROVIDER + + CoreLocationLocationProvider(); + // MOZ_CAN_RUN_SCRIPT_BOUNDARY because we can't mark Objective-C methods as + // MOZ_CAN_RUN_SCRIPT as far as I can tell, and this method is called from + // Objective-C. + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void NotifyError(uint16_t aErrorCode); + void Update(nsIDOMGeoPosition* aSomewhere); + void CreateMLSFallbackProvider(); + void CancelMLSFallbackProvider(); + + private: + virtual ~CoreLocationLocationProvider() = default; + + CoreLocationObjects* mCLObjects; + nsCOMPtr<nsIGeolocationUpdate> mCallback; + RefPtr<MLSFallback> mMLSFallbackProvider; + + class MLSUpdate : public nsIGeolocationUpdate { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONUPDATE + + explicit MLSUpdate(CoreLocationLocationProvider& parentProvider); + + private: + CoreLocationLocationProvider& mParentLocationProvider; + virtual ~MLSUpdate() = default; + }; +}; diff --git a/dom/system/mac/CoreLocationLocationProvider.mm b/dom/system/mac/CoreLocationLocationProvider.mm new file mode 100644 index 0000000000..2e9fee320b --- /dev/null +++ b/dom/system/mac/CoreLocationLocationProvider.mm @@ -0,0 +1,253 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsCOMPtr.h" +#include "GeolocationPosition.h" +#include "nsIConsoleService.h" +#include "nsServiceManagerUtils.h" +#include "CoreLocationLocationProvider.h" +#include "prtime.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/Telemetry.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/GeolocationPositionErrorBinding.h" +#include "MLSFallback.h" + +#include <CoreLocation/CLError.h> +#include <CoreLocation/CLLocation.h> +#include <CoreLocation/CLLocationManager.h> +#include <CoreLocation/CLLocationManagerDelegate.h> + +#include <objc/objc.h> +#include <objc/objc-runtime.h> + +#include "nsObjCExceptions.h" + +using namespace mozilla; + +static const CLLocationAccuracy kHIGH_ACCURACY = kCLLocationAccuracyBest; +static const CLLocationAccuracy kDEFAULT_ACCURACY = + kCLLocationAccuracyNearestTenMeters; + +@interface LocationDelegate : NSObject <CLLocationManagerDelegate> { + CoreLocationLocationProvider* mProvider; +} + +- (id)init:(CoreLocationLocationProvider*)aProvider; +- (void)locationManager:(CLLocationManager*)aManager + didFailWithError:(NSError*)aError; +- (void)locationManager:(CLLocationManager*)aManager + didUpdateLocations:(NSArray*)locations; + +@end + +@implementation LocationDelegate +- (id)init:(CoreLocationLocationProvider*)aProvider { + if ((self = [super init])) { + mProvider = aProvider; + } + + return self; +} + +- (void)locationManager:(CLLocationManager*)aManager + didFailWithError:(NSError*)aError { + nsCOMPtr<nsIConsoleService> console = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + + NS_ENSURE_TRUE_VOID(console); + + NSString* message = [@"Failed to acquire position: " + stringByAppendingString:[aError localizedDescription]]; + + console->LogStringMessage(NS_ConvertUTF8toUTF16([message UTF8String]).get()); + + // The CL provider does not fallback to GeoIP, so use + // NetworkGeolocationProvider for this. The concept here is: on error, hand + // off geolocation to MLS, which will then report back a location or error. + mProvider->CreateMLSFallbackProvider(); +} + +- (void)locationManager:(CLLocationManager*)aManager + didUpdateLocations:(NSArray*)aLocations { + if (aLocations.count < 1) { + return; + } + + mProvider->CancelMLSFallbackProvider(); + + CLLocation* location = [aLocations objectAtIndex:0]; + + double altitude; + double altitudeAccuracy; + + // A negative verticalAccuracy indicates that the altitude value is invalid. + if (location.verticalAccuracy >= 0) { + altitude = location.altitude; + altitudeAccuracy = location.verticalAccuracy; + } else { + altitude = UnspecifiedNaN<double>(); + altitudeAccuracy = UnspecifiedNaN<double>(); + } + + double speed = + location.speed >= 0 ? location.speed : UnspecifiedNaN<double>(); + + double heading = + location.course >= 0 ? location.course : UnspecifiedNaN<double>(); + + // nsGeoPositionCoords will convert NaNs to null for optional properties of + // the JavaScript Coordinates object. + nsCOMPtr<nsIDOMGeoPosition> geoPosition = new nsGeoPosition( + location.coordinate.latitude, location.coordinate.longitude, altitude, + location.horizontalAccuracy, altitudeAccuracy, heading, speed, + PR_Now() / PR_USEC_PER_MSEC); + + mProvider->Update(geoPosition); + Telemetry::Accumulate(Telemetry::GEOLOCATION_OSX_SOURCE_IS_MLS, false); +} +@end + +NS_IMPL_ISUPPORTS(CoreLocationLocationProvider::MLSUpdate, + nsIGeolocationUpdate); + +CoreLocationLocationProvider::MLSUpdate::MLSUpdate( + CoreLocationLocationProvider& parentProvider) + : mParentLocationProvider(parentProvider) {} + +NS_IMETHODIMP +CoreLocationLocationProvider::MLSUpdate::Update(nsIDOMGeoPosition* position) { + nsCOMPtr<nsIDOMGeoPositionCoords> coords; + position->GetCoords(getter_AddRefs(coords)); + if (!coords) { + return NS_ERROR_FAILURE; + } + mParentLocationProvider.Update(position); + Telemetry::Accumulate(Telemetry::GEOLOCATION_OSX_SOURCE_IS_MLS, true); + return NS_OK; +} + +NS_IMETHODIMP +CoreLocationLocationProvider::MLSUpdate::NotifyError(uint16_t error) { + mParentLocationProvider.NotifyError(error); + return NS_OK; +} + +class CoreLocationObjects { + public: + nsresult Init(CoreLocationLocationProvider* aProvider) { + mLocationManager = [[CLLocationManager alloc] init]; + NS_ENSURE_TRUE(mLocationManager, NS_ERROR_NOT_AVAILABLE); + + mLocationDelegate = [[LocationDelegate alloc] init:aProvider]; + NS_ENSURE_TRUE(mLocationDelegate, NS_ERROR_NOT_AVAILABLE); + + mLocationManager.desiredAccuracy = kDEFAULT_ACCURACY; + mLocationManager.delegate = mLocationDelegate; + + return NS_OK; + } + + ~CoreLocationObjects() { + if (mLocationManager) { + [mLocationManager release]; + } + + if (mLocationDelegate) { + [mLocationDelegate release]; + } + } + + LocationDelegate* mLocationDelegate; + CLLocationManager* mLocationManager; +}; + +NS_IMPL_ISUPPORTS(CoreLocationLocationProvider, nsIGeolocationProvider) + +CoreLocationLocationProvider::CoreLocationLocationProvider() + : mCLObjects(nullptr), mMLSFallbackProvider(nullptr) {} + +NS_IMETHODIMP +CoreLocationLocationProvider::Startup() { + if (!mCLObjects) { + auto clObjs = MakeUnique<CoreLocationObjects>(); + + nsresult rv = clObjs->Init(this); + NS_ENSURE_SUCCESS(rv, rv); + + mCLObjects = clObjs.release(); + } + + // Must be stopped before starting or response (success or failure) is not + // guaranteed + [mCLObjects->mLocationManager stopUpdatingLocation]; + [mCLObjects->mLocationManager startUpdatingLocation]; + return NS_OK; +} + +NS_IMETHODIMP +CoreLocationLocationProvider::Watch(nsIGeolocationUpdate* aCallback) { + if (mCallback) { + return NS_OK; + } + + mCallback = aCallback; + return NS_OK; +} + +NS_IMETHODIMP +CoreLocationLocationProvider::Shutdown() { + NS_ENSURE_STATE(mCLObjects); + + [mCLObjects->mLocationManager stopUpdatingLocation]; + + delete mCLObjects; + mCLObjects = nullptr; + + if (mMLSFallbackProvider) { + mMLSFallbackProvider->Shutdown(); + mMLSFallbackProvider = nullptr; + } + + return NS_OK; +} + +NS_IMETHODIMP +CoreLocationLocationProvider::SetHighAccuracy(bool aEnable) { + NS_ENSURE_STATE(mCLObjects); + + mCLObjects->mLocationManager.desiredAccuracy = + (aEnable ? kHIGH_ACCURACY : kDEFAULT_ACCURACY); + + return NS_OK; +} + +void CoreLocationLocationProvider::Update(nsIDOMGeoPosition* aSomewhere) { + if (aSomewhere && mCallback) { + mCallback->Update(aSomewhere); + } +} +void CoreLocationLocationProvider::NotifyError(uint16_t aErrorCode) { + nsCOMPtr<nsIGeolocationUpdate> callback(mCallback); + callback->NotifyError(aErrorCode); +} +void CoreLocationLocationProvider::CreateMLSFallbackProvider() { + if (mMLSFallbackProvider) { + return; + } + + mMLSFallbackProvider = new MLSFallback(0); + mMLSFallbackProvider->Startup(new MLSUpdate(*this)); +} + +void CoreLocationLocationProvider::CancelMLSFallbackProvider() { + if (!mMLSFallbackProvider) { + return; + } + + mMLSFallbackProvider->Shutdown(); + mMLSFallbackProvider = nullptr; +} diff --git a/dom/system/mac/moz.build b/dom/system/mac/moz.build new file mode 100644 index 0000000000..6a10090793 --- /dev/null +++ b/dom/system/mac/moz.build @@ -0,0 +1,21 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +SOURCES += [ + "CoreLocationLocationProvider.mm", + "nsOSPermissionRequest.mm", +] + +EXPORTS += [ + "nsOSPermissionRequest.h", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" +LOCAL_INCLUDES += [ + "/dom/geolocation", +] diff --git a/dom/system/mac/nsOSPermissionRequest.h b/dom/system/mac/nsOSPermissionRequest.h new file mode 100644 index 0000000000..62e4360fee --- /dev/null +++ b/dom/system/mac/nsOSPermissionRequest.h @@ -0,0 +1,31 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOSPermissionRequest_h__ +#define nsOSPermissionRequest_h__ + +#include "nsOSPermissionRequestBase.h" + +class nsOSPermissionRequest : public nsOSPermissionRequestBase { + public: + nsOSPermissionRequest(){}; + + NS_IMETHOD GetAudioCapturePermissionState(uint16_t* aAudio) override; + + NS_IMETHOD GetVideoCapturePermissionState(uint16_t* aVideo) override; + + NS_IMETHOD GetScreenCapturePermissionState(uint16_t* aScreen) override; + + NS_IMETHOD RequestVideoCapturePermission( + JSContext* aCx, mozilla::dom::Promise** aPromiseOut) override; + + NS_IMETHOD RequestAudioCapturePermission( + JSContext* aCx, mozilla::dom::Promise** aPromiseOut) override; + + NS_IMETHOD MaybeRequestScreenCapturePermission() override; +}; + +#endif diff --git a/dom/system/mac/nsOSPermissionRequest.mm b/dom/system/mac/nsOSPermissionRequest.mm new file mode 100644 index 0000000000..82e9edf1ac --- /dev/null +++ b/dom/system/mac/nsOSPermissionRequest.mm @@ -0,0 +1,65 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsOSPermissionRequest.h" + +#include "mozilla/dom/Promise.h" +#include "nsCocoaUtils.h" + +using namespace mozilla; + +using mozilla::dom::Promise; + +NS_IMETHODIMP +nsOSPermissionRequest::GetAudioCapturePermissionState(uint16_t* aAudio) { + MOZ_ASSERT(aAudio); + return nsCocoaUtils::GetAudioCapturePermissionState(*aAudio); +} + +NS_IMETHODIMP +nsOSPermissionRequest::GetVideoCapturePermissionState(uint16_t* aVideo) { + MOZ_ASSERT(aVideo); + return nsCocoaUtils::GetVideoCapturePermissionState(*aVideo); +} + +NS_IMETHODIMP +nsOSPermissionRequest::GetScreenCapturePermissionState(uint16_t* aScreen) { + MOZ_ASSERT(aScreen); + return nsCocoaUtils::GetScreenCapturePermissionState(*aScreen); +} + +NS_IMETHODIMP +nsOSPermissionRequest::RequestVideoCapturePermission(JSContext* aCx, + Promise** aPromiseOut) { + RefPtr<Promise> promiseHandle; + nsresult rv = GetPromise(aCx, promiseHandle); + if (NS_FAILED(rv)) { + return rv; + } + + rv = nsCocoaUtils::RequestVideoCapturePermission(promiseHandle); + promiseHandle.forget(aPromiseOut); + return rv; +} + +NS_IMETHODIMP +nsOSPermissionRequest::RequestAudioCapturePermission(JSContext* aCx, + Promise** aPromiseOut) { + RefPtr<Promise> promiseHandle; + nsresult rv = GetPromise(aCx, promiseHandle); + if (NS_FAILED(rv)) { + return rv; + } + + rv = nsCocoaUtils::RequestAudioCapturePermission(promiseHandle); + promiseHandle.forget(aPromiseOut); + return rv; +} + +NS_IMETHODIMP +nsOSPermissionRequest::MaybeRequestScreenCapturePermission() { + return nsCocoaUtils::MaybeRequestScreenCapturePermission(); +} diff --git a/dom/system/moz.build b/dom/system/moz.build new file mode 100644 index 0000000000..e702d44183 --- /dev/null +++ b/dom/system/moz.build @@ -0,0 +1,113 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# This picks up *hapticfeedback* which is graveyard +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +with Files("*ocationProvider*"): + BUG_COMPONENT = ("Core", "DOM: Geolocation") + +with Files("windows/*LocationProvider*"): + BUG_COMPONENT = ("Core", "DOM: Geolocation") + +with Files("IOUtils*"): + BUG_COMPONENT = ("Toolkit", "IOUtils and PathUtils") + +with Files("PathUtils*"): + BUG_COMPONENT = ("Toolkit", "IOUtils and PathUtils") + +with Files("mac/*LocationProvider*"): + BUG_COMPONENT = ("Core", "DOM: Geolocation") + +with Files("mac/*OSPermissionRequest*"): + BUG_COMPONENT = ("Firefox", "Site Permissions") + +with Files("linux/*LocationProvider*"): + BUG_COMPONENT = ("Core", "DOM: Geolocation") + +with Files("android/*LocationProvider*"): + BUG_COMPONENT = ("Core", "DOM: Geolocation") + +with Files("tests/chrome.toml"): + BUG_COMPONENT = ("Toolkit", "IOUtils and PathUtils") + +with Files("tests/*constants*"): + BUG_COMPONENT = ("Toolkit", "IOUtils and PathUtils") + +with Files("tests/ioutils/**"): + BUG_COMPONENT = ("Toolkit", "IOUtils and PathUtils") + +with Files("tests/mochitest.toml"): + BUG_COMPONENT = ("Core", "DOM: Device Interfaces") + +with Files("test/*pathutils*"): + BUG_COMPONENT = ("Toolkit", "IOUtils and PathUtils") + +with Files("tests/*1197901*"): + BUG_COMPONENT = ("Core", "DOM: Device Interfaces") + + +toolkit = CONFIG["MOZ_WIDGET_TOOLKIT"] + +if toolkit == "windows": + DIRS += ["windows"] +elif toolkit == "cocoa": + DIRS += ["mac"] +elif toolkit == "android": + DIRS += ["android"] +elif toolkit == "gtk": + DIRS += ["linux"] + +if toolkit != "cocoa": + EXPORTS += [ + "nsOSPermissionRequest.h", + ] + +XPIDL_SOURCES += [ + "nsIOSPermissionRequest.idl", +] + +XPIDL_MODULE = "dom_system" + +EXPORTS += [ + "nsDeviceSensors.h", + "nsOSPermissionRequestBase.h", +] + +EXPORTS.mozilla.dom += [ + "IOUtils.h", + "PathUtils.h", +] + +UNIFIED_SOURCES += [ + "IOUtils.cpp", + "nsDeviceSensors.cpp", + "nsOSPermissionRequestBase.cpp", + "PathUtils.cpp", +] + +EXTRA_JS_MODULES += [ + "NetworkGeolocationProvider.sys.mjs", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" +# We fire the nsDOMDeviceAcceleration +LOCAL_INCLUDES += [ + "/dom/base", + "/dom/bindings", + "/js/xpconnect/loader", + "/xpcom/base", +] + +MOCHITEST_CHROME_MANIFESTS += ["tests/chrome.toml", "tests/ioutils/chrome.toml"] +MOCHITEST_MANIFESTS += ["tests/mochitest.toml"] diff --git a/dom/system/nsDeviceSensors.cpp b/dom/system/nsDeviceSensors.cpp new file mode 100644 index 0000000000..c7fc67b52c --- /dev/null +++ b/dom/system/nsDeviceSensors.cpp @@ -0,0 +1,557 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Hal.h" +#include "mozilla/HalSensor.h" + +#include "nsContentUtils.h" +#include "nsDeviceSensors.h" + +#include "nsGlobalWindowInner.h" +#include "nsPIDOMWindow.h" +#include "nsIScriptObjectPrincipal.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_device.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/DeviceLightEvent.h" +#include "mozilla/dom/DeviceOrientationEvent.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/UserProximityEvent.h" +#include "mozilla/ErrorResult.h" + +#include <cmath> + +using namespace mozilla; +using namespace mozilla::dom; +using namespace hal; + +class nsIDOMWindow; + +#undef near + +#define DEFAULT_SENSOR_POLL 100 + +static const nsTArray<nsIDOMWindow*>::index_type NoIndex = + nsTArray<nsIDOMWindow*>::NoIndex; + +class nsDeviceSensorData final : public nsIDeviceSensorData { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIDEVICESENSORDATA + + nsDeviceSensorData(unsigned long type, double x, double y, double z); + + private: + ~nsDeviceSensorData(); + + protected: + unsigned long mType; + double mX, mY, mZ; +}; + +nsDeviceSensorData::nsDeviceSensorData(unsigned long type, double x, double y, + double z) + : mType(type), mX(x), mY(y), mZ(z) {} + +NS_INTERFACE_MAP_BEGIN(nsDeviceSensorData) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDeviceSensorData) +NS_INTERFACE_MAP_END + +NS_IMPL_ADDREF(nsDeviceSensorData) +NS_IMPL_RELEASE(nsDeviceSensorData) + +nsDeviceSensorData::~nsDeviceSensorData() = default; + +NS_IMETHODIMP nsDeviceSensorData::GetType(uint32_t* aType) { + NS_ENSURE_ARG_POINTER(aType); + *aType = mType; + return NS_OK; +} + +NS_IMETHODIMP nsDeviceSensorData::GetX(double* aX) { + NS_ENSURE_ARG_POINTER(aX); + *aX = mX; + return NS_OK; +} + +NS_IMETHODIMP nsDeviceSensorData::GetY(double* aY) { + NS_ENSURE_ARG_POINTER(aY); + *aY = mY; + return NS_OK; +} + +NS_IMETHODIMP nsDeviceSensorData::GetZ(double* aZ) { + NS_ENSURE_ARG_POINTER(aZ); + *aZ = mZ; + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsDeviceSensors, nsIDeviceSensors) + +nsDeviceSensors::nsDeviceSensors() { + mIsUserProximityNear = false; + mLastDOMMotionEventTime = TimeStamp::Now(); + + for (int i = 0; i < NUM_SENSOR_TYPE; i++) { + nsTArray<nsIDOMWindow*>* windows = new nsTArray<nsIDOMWindow*>(); + mWindowListeners.AppendElement(windows); + } + + mLastDOMMotionEventTime = TimeStamp::Now(); +} + +nsDeviceSensors::~nsDeviceSensors() { + for (int i = 0; i < NUM_SENSOR_TYPE; i++) { + if (IsSensorEnabled(i)) UnregisterSensorObserver((SensorType)i, this); + } + + for (int i = 0; i < NUM_SENSOR_TYPE; i++) { + delete mWindowListeners[i]; + } +} + +NS_IMETHODIMP nsDeviceSensors::HasWindowListener(uint32_t aType, + nsIDOMWindow* aWindow, + bool* aRetVal) { + if (!IsSensorAllowedByPref(aType, aWindow)) + *aRetVal = false; + else + *aRetVal = mWindowListeners[aType]->IndexOf(aWindow) != NoIndex; + + return NS_OK; +} + +class DeviceSensorTestEvent : public Runnable { + public: + DeviceSensorTestEvent(nsDeviceSensors* aTarget, uint32_t aType) + : mozilla::Runnable("DeviceSensorTestEvent"), + mTarget(aTarget), + mType(aType) {} + + NS_IMETHOD Run() override { + SensorData sensorData; + sensorData.sensor() = static_cast<SensorType>(mType); + sensorData.timestamp() = PR_Now(); + sensorData.values().AppendElement(0.5f); + sensorData.values().AppendElement(0.5f); + sensorData.values().AppendElement(0.5f); + sensorData.values().AppendElement(0.5f); + mTarget->Notify(sensorData); + return NS_OK; + } + + private: + RefPtr<nsDeviceSensors> mTarget; + uint32_t mType; +}; + +NS_IMETHODIMP nsDeviceSensors::AddWindowListener(uint32_t aType, + nsIDOMWindow* aWindow) { + if (!IsSensorAllowedByPref(aType, aWindow)) return NS_OK; + + if (mWindowListeners[aType]->IndexOf(aWindow) != NoIndex) return NS_OK; + + if (!IsSensorEnabled(aType)) { + RegisterSensorObserver((SensorType)aType, this); + } + + mWindowListeners[aType]->AppendElement(aWindow); + + if (StaticPrefs::device_sensors_test_events()) { + nsCOMPtr<nsIRunnable> event = new DeviceSensorTestEvent(this, aType); + NS_DispatchToCurrentThread(event); + } + + return NS_OK; +} + +NS_IMETHODIMP nsDeviceSensors::RemoveWindowListener(uint32_t aType, + nsIDOMWindow* aWindow) { + if (mWindowListeners[aType]->IndexOf(aWindow) == NoIndex) return NS_OK; + + mWindowListeners[aType]->RemoveElement(aWindow); + + if (mWindowListeners[aType]->Length() == 0) + UnregisterSensorObserver((SensorType)aType, this); + + return NS_OK; +} + +NS_IMETHODIMP nsDeviceSensors::RemoveWindowAsListener(nsIDOMWindow* aWindow) { + for (int i = 0; i < NUM_SENSOR_TYPE; i++) { + RemoveWindowListener((SensorType)i, aWindow); + } + return NS_OK; +} + +static bool WindowCannotReceiveSensorEvent(nsPIDOMWindowInner* aWindow) { + // Check to see if this window is in the background. + if (!aWindow || !aWindow->IsCurrentInnerWindow()) { + return true; + } + + nsPIDOMWindowOuter* windowOuter = aWindow->GetOuterWindow(); + BrowsingContext* topBC = aWindow->GetBrowsingContext()->Top(); + if (windowOuter->IsBackground() || !topBC->GetIsActiveBrowserWindow()) { + return true; + } + + // Check to see if this window is a cross-origin iframe: + if (!topBC->IsInProcess()) { + return true; + } + + nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aWindow); + nsCOMPtr<nsIScriptObjectPrincipal> topSop = + do_QueryInterface(topBC->GetDOMWindow()); + if (!sop || !topSop) { + return true; + } + + nsIPrincipal* principal = sop->GetPrincipal(); + nsIPrincipal* topPrincipal = topSop->GetPrincipal(); + if (!principal || !topPrincipal) { + return true; + } + + return !principal->Subsumes(topPrincipal); +} + +// Holds the device orientation in Euler angle degrees (azimuth, pitch, roll). +struct Orientation { + enum OrientationReference { kRelative = 0, kAbsolute }; + + static Orientation RadToDeg(const Orientation& aOrient) { + const static double kRadToDeg = 180.0 / M_PI; + return {aOrient.alpha * kRadToDeg, aOrient.beta * kRadToDeg, + aOrient.gamma * kRadToDeg}; + } + + double alpha; + double beta; + double gamma; +}; + +static Orientation RotationVectorToOrientation(double aX, double aY, double aZ, + double aW) { + double mat[9]; + + mat[0] = 1 - 2 * aY * aY - 2 * aZ * aZ; + mat[1] = 2 * aX * aY - 2 * aZ * aW; + mat[2] = 2 * aX * aZ + 2 * aY * aW; + + mat[3] = 2 * aX * aY + 2 * aZ * aW; + mat[4] = 1 - 2 * aX * aX - 2 * aZ * aZ; + mat[5] = 2 * aY * aZ - 2 * aX * aW; + + mat[6] = 2 * aX * aZ - 2 * aY * aW; + mat[7] = 2 * aY * aZ + 2 * aX * aW; + mat[8] = 1 - 2 * aX * aX - 2 * aY * aY; + + Orientation orient; + + if (mat[8] > 0) { + orient.alpha = atan2(-mat[1], mat[4]); + orient.beta = asin(mat[7]); + orient.gamma = atan2(-mat[6], mat[8]); + } else if (mat[8] < 0) { + orient.alpha = atan2(mat[1], -mat[4]); + orient.beta = -asin(mat[7]); + orient.beta += (orient.beta >= 0) ? -M_PI : M_PI; + orient.gamma = atan2(mat[6], -mat[8]); + } else { + if (mat[6] > 0) { + orient.alpha = atan2(-mat[1], mat[4]); + orient.beta = asin(mat[7]); + orient.gamma = -M_PI_2; + } else if (mat[6] < 0) { + orient.alpha = atan2(mat[1], -mat[4]); + orient.beta = -asin(mat[7]); + orient.beta += (orient.beta >= 0) ? -M_PI : M_PI; + orient.gamma = -M_PI_2; + } else { + orient.alpha = atan2(mat[3], mat[0]); + orient.beta = (mat[7] > 0) ? M_PI_2 : -M_PI_2; + orient.gamma = 0; + } + } + + if (orient.alpha < 0) { + orient.alpha += 2 * M_PI; + } + + return Orientation::RadToDeg(orient); +} + +void nsDeviceSensors::Notify(const mozilla::hal::SensorData& aSensorData) { + uint32_t type = aSensorData.sensor(); + + const nsTArray<float>& values = aSensorData.values(); + size_t len = values.Length(); + double x = len > 0 ? values[0] : 0.0; + double y = len > 1 ? values[1] : 0.0; + double z = len > 2 ? values[2] : 0.0; + double w = len > 3 ? values[3] : 0.0; + PRTime timestamp = aSensorData.timestamp(); + + nsCOMArray<nsIDOMWindow> windowListeners; + for (uint32_t i = 0; i < mWindowListeners[type]->Length(); i++) { + windowListeners.AppendObject(mWindowListeners[type]->SafeElementAt(i)); + } + + for (uint32_t i = windowListeners.Count(); i > 0;) { + --i; + + nsCOMPtr<nsPIDOMWindowInner> pwindow = + do_QueryInterface(windowListeners[i]); + if (WindowCannotReceiveSensorEvent(pwindow)) { + continue; + } + + if (nsCOMPtr<Document> doc = pwindow->GetDoc()) { + nsCOMPtr<mozilla::dom::EventTarget> target = + do_QueryInterface(windowListeners[i]); + if (type == nsIDeviceSensorData::TYPE_ACCELERATION || + type == nsIDeviceSensorData::TYPE_LINEAR_ACCELERATION || + type == nsIDeviceSensorData::TYPE_GYROSCOPE) { + FireDOMMotionEvent(doc, target, type, timestamp, x, y, z); + } else if (type == nsIDeviceSensorData::TYPE_ORIENTATION) { + FireDOMOrientationEvent(target, x, y, z, Orientation::kAbsolute); + } else if (type == nsIDeviceSensorData::TYPE_ROTATION_VECTOR) { + const Orientation orient = RotationVectorToOrientation(x, y, z, w); + FireDOMOrientationEvent(target, orient.alpha, orient.beta, orient.gamma, + Orientation::kAbsolute); + } else if (type == nsIDeviceSensorData::TYPE_GAME_ROTATION_VECTOR) { + const Orientation orient = RotationVectorToOrientation(x, y, z, w); + FireDOMOrientationEvent(target, orient.alpha, orient.beta, orient.gamma, + Orientation::kRelative); + } else if (type == nsIDeviceSensorData::TYPE_PROXIMITY) { + MaybeFireDOMUserProximityEvent(target, x, z); + } else if (type == nsIDeviceSensorData::TYPE_LIGHT) { + FireDOMLightEvent(target, x); + } + } + } +} + +void nsDeviceSensors::FireDOMLightEvent(mozilla::dom::EventTarget* aTarget, + double aValue) { + DeviceLightEventInit init; + init.mBubbles = true; + init.mCancelable = false; + init.mValue = round(aValue); + RefPtr<DeviceLightEvent> event = + DeviceLightEvent::Constructor(aTarget, u"devicelight"_ns, init); + + event->SetTrusted(true); + + aTarget->DispatchEvent(*event); +} + +void nsDeviceSensors::MaybeFireDOMUserProximityEvent( + mozilla::dom::EventTarget* aTarget, double aValue, double aMax) { + bool near = (aValue < aMax); + if (mIsUserProximityNear != near) { + mIsUserProximityNear = near; + FireDOMUserProximityEvent(aTarget, mIsUserProximityNear); + } +} + +void nsDeviceSensors::FireDOMUserProximityEvent( + mozilla::dom::EventTarget* aTarget, bool aNear) { + UserProximityEventInit init; + init.mBubbles = true; + init.mCancelable = false; + init.mNear = aNear; + RefPtr<UserProximityEvent> event = + UserProximityEvent::Constructor(aTarget, u"userproximity"_ns, init); + + event->SetTrusted(true); + + aTarget->DispatchEvent(*event); +} + +void nsDeviceSensors::FireDOMOrientationEvent(EventTarget* aTarget, + double aAlpha, double aBeta, + double aGamma, bool aIsAbsolute) { + DeviceOrientationEventInit init; + init.mBubbles = true; + init.mCancelable = false; + init.mAlpha.SetValue(aAlpha); + init.mBeta.SetValue(aBeta); + init.mGamma.SetValue(aGamma); + init.mAbsolute = aIsAbsolute; + + auto Dispatch = [&](EventTarget* aEventTarget, const nsAString& aType) { + RefPtr<DeviceOrientationEvent> event = + DeviceOrientationEvent::Constructor(aEventTarget, aType, init); + event->SetTrusted(true); + aEventTarget->DispatchEvent(*event); + }; + + Dispatch(aTarget, aIsAbsolute ? u"deviceorientationabsolute"_ns + : u"deviceorientation"_ns); + + // This is used to determine whether relative events have been dispatched + // during the current session, in which case we don't dispatch the additional + // compatibility events. + static bool sIsDispatchingRelativeEvents = false; + sIsDispatchingRelativeEvents = sIsDispatchingRelativeEvents || !aIsAbsolute; + + // Android devices with SENSOR_GAME_ROTATION_VECTOR support dispatch + // relative events for "deviceorientation" by default, while other platforms + // and devices without such support dispatch absolute events by default. + if (aIsAbsolute && !sIsDispatchingRelativeEvents) { + // For absolute events on devices without support for relative events, + // we need to additionally dispatch type "deviceorientation" to keep + // backwards-compatibility. + Dispatch(aTarget, u"deviceorientation"_ns); + } +} + +void nsDeviceSensors::FireDOMMotionEvent(Document* doc, EventTarget* target, + uint32_t type, PRTime timestamp, + double x, double y, double z) { + // Attempt to coalesce events + TimeDuration sensorPollDuration = + TimeDuration::FromMilliseconds(DEFAULT_SENSOR_POLL); + bool fireEvent = + (TimeStamp::Now() > mLastDOMMotionEventTime + sensorPollDuration) || + StaticPrefs::device_sensors_test_events(); + + switch (type) { + case nsIDeviceSensorData::TYPE_LINEAR_ACCELERATION: + if (!mLastAcceleration) { + mLastAcceleration.emplace(); + } + mLastAcceleration->mX.SetValue(x); + mLastAcceleration->mY.SetValue(y); + mLastAcceleration->mZ.SetValue(z); + break; + case nsIDeviceSensorData::TYPE_ACCELERATION: + if (!mLastAccelerationIncludingGravity) { + mLastAccelerationIncludingGravity.emplace(); + } + mLastAccelerationIncludingGravity->mX.SetValue(x); + mLastAccelerationIncludingGravity->mY.SetValue(y); + mLastAccelerationIncludingGravity->mZ.SetValue(z); + break; + case nsIDeviceSensorData::TYPE_GYROSCOPE: + if (!mLastRotationRate) { + mLastRotationRate.emplace(); + } + mLastRotationRate->mAlpha.SetValue(x); + mLastRotationRate->mBeta.SetValue(y); + mLastRotationRate->mGamma.SetValue(z); + break; + } + + if (fireEvent) { + if (!mLastAcceleration) { + mLastAcceleration.emplace(); + } + if (!mLastAccelerationIncludingGravity) { + mLastAccelerationIncludingGravity.emplace(); + } + if (!mLastRotationRate) { + mLastRotationRate.emplace(); + } + } else if (!mLastAcceleration || !mLastAccelerationIncludingGravity || + !mLastRotationRate) { + return; + } + + IgnoredErrorResult ignored; + RefPtr<Event> event = + doc->CreateEvent(u"DeviceMotionEvent"_ns, CallerType::System, ignored); + if (!event) { + return; + } + + DeviceMotionEvent* me = static_cast<DeviceMotionEvent*>(event.get()); + + me->InitDeviceMotionEvent( + u"devicemotion"_ns, true, false, *mLastAcceleration, + *mLastAccelerationIncludingGravity, *mLastRotationRate, + Nullable<double>(DEFAULT_SENSOR_POLL), Nullable<uint64_t>(timestamp)); + + event->SetTrusted(true); + + target->DispatchEvent(*event); + + mLastRotationRate.reset(); + mLastAccelerationIncludingGravity.reset(); + mLastAcceleration.reset(); + mLastDOMMotionEventTime = TimeStamp::Now(); +} + +bool nsDeviceSensors::IsSensorAllowedByPref(uint32_t aType, + nsIDOMWindow* aWindow) { + // checks "device.sensors.enabled" master pref + if (!StaticPrefs::device_sensors_enabled()) { + return false; + } + + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aWindow); + nsCOMPtr<Document> doc; + if (window) { + doc = window->GetExtantDoc(); + } + + switch (aType) { + case nsIDeviceSensorData::TYPE_LINEAR_ACCELERATION: + case nsIDeviceSensorData::TYPE_ACCELERATION: + case nsIDeviceSensorData::TYPE_GYROSCOPE: + // checks "device.sensors.motion.enabled" pref + if (!StaticPrefs::device_sensors_motion_enabled()) { + return false; + } + if (doc) { + doc->WarnOnceAbout(DeprecatedOperations::eMotionEvent); + } + break; + case nsIDeviceSensorData::TYPE_GAME_ROTATION_VECTOR: + case nsIDeviceSensorData::TYPE_ORIENTATION: + case nsIDeviceSensorData::TYPE_ROTATION_VECTOR: + // checks "device.sensors.orientation.enabled" pref + if (!StaticPrefs::device_sensors_orientation_enabled()) { + return false; + } + if (doc) { + doc->WarnOnceAbout(DeprecatedOperations::eOrientationEvent); + } + break; + case nsIDeviceSensorData::TYPE_PROXIMITY: + // checks "device.sensors.proximity.enabled" pref + if (!StaticPrefs::device_sensors_proximity_enabled()) { + return false; + } + if (doc) { + doc->WarnOnceAbout(DeprecatedOperations::eProximityEvent, true); + } + break; + case nsIDeviceSensorData::TYPE_LIGHT: + // checks "device.sensors.ambientLight.enabled" pref + if (!StaticPrefs::device_sensors_ambientLight_enabled()) { + return false; + } + if (doc) { + doc->WarnOnceAbout(DeprecatedOperations::eAmbientLightEvent, true); + } + break; + default: + MOZ_ASSERT_UNREACHABLE("Device sensor type not recognised"); + return false; + } + + if (!window) { + return true; + } + return !nsGlobalWindowInner::Cast(window)->ShouldResistFingerprinting( + RFPTarget::DeviceSensors); +} diff --git a/dom/system/nsDeviceSensors.h b/dom/system/nsDeviceSensors.h new file mode 100644 index 0000000000..dd079088e6 --- /dev/null +++ b/dom/system/nsDeviceSensors.h @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsDeviceSensors_h +#define nsDeviceSensors_h + +#include "nsIDeviceSensors.h" +#include "nsCOMArray.h" +#include "nsTArray.h" +#include "nsCOMPtr.h" +#include "mozilla/dom/DeviceMotionEvent.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/HalSensor.h" + +class nsIDOMWindow; + +namespace mozilla::dom { +class Document; +class EventTarget; +} // namespace mozilla::dom + +class nsDeviceSensors : public nsIDeviceSensors, + public mozilla::hal::ISensorObserver { + using DeviceAccelerationInit = mozilla::dom::DeviceAccelerationInit; + using DeviceRotationRateInit = mozilla::dom::DeviceRotationRateInit; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIDEVICESENSORS + + nsDeviceSensors(); + + void Notify(const mozilla::hal::SensorData& aSensorData) override; + + private: + virtual ~nsDeviceSensors(); + + // sensor -> window listener + nsTArray<nsTArray<nsIDOMWindow*>*> mWindowListeners; + + void FireDOMLightEvent(mozilla::dom::EventTarget* aTarget, double value); + + void MaybeFireDOMUserProximityEvent(mozilla::dom::EventTarget* aTarget, + double aValue, double aMax); + + void FireDOMUserProximityEvent(mozilla::dom::EventTarget* aTarget, + bool aNear); + + void FireDOMOrientationEvent(mozilla::dom::EventTarget* target, double aAlpha, + double aBeta, double aGamma, bool aIsAbsolute); + + void FireDOMMotionEvent(mozilla::dom::Document* domDoc, + mozilla::dom::EventTarget* target, uint32_t type, + PRTime timestamp, double x, double y, double z); + + inline bool IsSensorEnabled(uint32_t aType) { + return mWindowListeners[aType]->Length() > 0; + } + + bool IsSensorAllowedByPref(uint32_t aType, nsIDOMWindow* aWindow); + + mozilla::TimeStamp mLastDOMMotionEventTime; + bool mIsUserProximityNear; + mozilla::Maybe<DeviceAccelerationInit> mLastAcceleration; + mozilla::Maybe<DeviceAccelerationInit> mLastAccelerationIncludingGravity; + mozilla::Maybe<DeviceRotationRateInit> mLastRotationRate; +}; + +#endif diff --git a/dom/system/nsIOSPermissionRequest.idl b/dom/system/nsIOSPermissionRequest.idl new file mode 100644 index 0000000000..e0d7b531c7 --- /dev/null +++ b/dom/system/nsIOSPermissionRequest.idl @@ -0,0 +1,69 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=40: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[scriptable, uuid(95790842-75a0-430d-98bf-f5ce3788ea6d)] +interface nsIOSPermissionRequest: nsISupports +{ + /* + * The permission state is not known. As an example, on macOS + * this is used to indicate the user has not been prompted to + * authorize or deny access and there is no policy in place to + * deny access. + */ + const uint16_t PERMISSION_STATE_NOTDETERMINED = 0; + + /* A policy prevents the application from accessing the resource */ + const uint16_t PERMISSION_STATE_RESTRICTED = 1; + + /* Access to the resource is denied */ + const uint16_t PERMISSION_STATE_DENIED = 2; + + /* Access to the resource is allowed */ + const uint16_t PERMISSION_STATE_AUTHORIZED = 3; + + /* Get the permission state for both audio and video capture */ + void getMediaCapturePermissionState(out uint16_t aVideo, + out uint16_t aAudio); + + /* Get the permission state for audio capture */ + void getAudioCapturePermissionState(out uint16_t aAudio); + + /* Get the permission state for video capture */ + void getVideoCapturePermissionState(out uint16_t aVideo); + + /* Get the permission state for screen capture */ + void getScreenCapturePermissionState(out uint16_t aScreen); + + /* + * Request permission to access video capture devices. Returns a + * promise that resolves with |true| after the browser has been + * granted permission to capture video. If capture access is denied, + * the promise is resolved with |false|. The promise is rejected if + * an error occurs. + */ + [implicit_jscontext, must_use] + Promise requestVideoCapturePermission(); + + /* + * Request permission to access audio capture devices. Returns a + * promise with the same semantics as |requestVideoCapturePermission|. + */ + [implicit_jscontext, must_use] + Promise requestAudioCapturePermission(); + + /* + * Request permission to capture the screen using an unreliable method. + * Attemps to trigger a screen capture permission dialog. Whether or not + * the dialog is displayed and whether or not the user grants permission + * to record the screen is not available to the caller. This method has + * limited utility because it does not block to wait for a dialog + * prompt or the user's reponse if a dialog is displayed. And the dialog + * is not guaranteed to be displayed per OS restrictions. + */ + void maybeRequestScreenCapturePermission(); +}; diff --git a/dom/system/nsOSPermissionRequest.h b/dom/system/nsOSPermissionRequest.h new file mode 100644 index 0000000000..660434c863 --- /dev/null +++ b/dom/system/nsOSPermissionRequest.h @@ -0,0 +1,18 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOSPermissionRequest_h__ +#define nsOSPermissionRequest_h__ + +#include "nsOSPermissionRequestBase.h" + +/* + * The default implementation of nsOSPermissionRequestBase used on platforms + * that don't have a platform-specific version. + */ +class nsOSPermissionRequest : public nsOSPermissionRequestBase {}; + +#endif /* nsOSPermissionRequest_h__ */ diff --git a/dom/system/nsOSPermissionRequestBase.cpp b/dom/system/nsOSPermissionRequestBase.cpp new file mode 100644 index 0000000000..32b32c38cd --- /dev/null +++ b/dom/system/nsOSPermissionRequestBase.cpp @@ -0,0 +1,96 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsOSPermissionRequestBase.h" + +#include "mozilla/dom/Promise.h" + +using namespace mozilla; + +using mozilla::dom::Promise; + +NS_IMPL_ISUPPORTS(nsOSPermissionRequestBase, nsIOSPermissionRequest, + nsISupportsWeakReference) + +NS_IMETHODIMP +nsOSPermissionRequestBase::GetMediaCapturePermissionState( + uint16_t* aCamera, uint16_t* aMicrophone) { + nsresult rv = GetVideoCapturePermissionState(aCamera); + if (NS_FAILED(rv)) { + return rv; + } + return GetAudioCapturePermissionState(aMicrophone); +} + +NS_IMETHODIMP +nsOSPermissionRequestBase::GetAudioCapturePermissionState(uint16_t* aAudio) { + MOZ_ASSERT(aAudio); + *aAudio = PERMISSION_STATE_AUTHORIZED; + return NS_OK; +} + +NS_IMETHODIMP +nsOSPermissionRequestBase::GetVideoCapturePermissionState(uint16_t* aVideo) { + MOZ_ASSERT(aVideo); + *aVideo = PERMISSION_STATE_AUTHORIZED; + return NS_OK; +} + +NS_IMETHODIMP +nsOSPermissionRequestBase::GetScreenCapturePermissionState(uint16_t* aScreen) { + MOZ_ASSERT(aScreen); + *aScreen = PERMISSION_STATE_AUTHORIZED; + return NS_OK; +} + +nsresult nsOSPermissionRequestBase::GetPromise(JSContext* aCx, + RefPtr<Promise>& aPromiseOut) { + nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx); + if (NS_WARN_IF(!globalObject)) { + return NS_ERROR_UNEXPECTED; + } + + ErrorResult result; + aPromiseOut = Promise::Create(globalObject, result); + if (NS_WARN_IF(result.Failed())) { + return result.StealNSResult(); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsOSPermissionRequestBase::RequestVideoCapturePermission( + JSContext* aCx, Promise** aPromiseOut) { + RefPtr<Promise> promiseHandle; + nsresult rv = GetPromise(aCx, promiseHandle); + if (NS_FAILED(rv)) { + return rv; + } + + promiseHandle->MaybeResolve(true /* access authorized */); + promiseHandle.forget(aPromiseOut); + return NS_OK; +} + +NS_IMETHODIMP +nsOSPermissionRequestBase::RequestAudioCapturePermission( + JSContext* aCx, Promise** aPromiseOut) { + RefPtr<Promise> promiseHandle; + nsresult rv = GetPromise(aCx, promiseHandle); + if (NS_FAILED(rv)) { + return rv; + } + + promiseHandle->MaybeResolve(true /* access authorized */); + promiseHandle.forget(aPromiseOut); + return NS_OK; +} + +NS_IMETHODIMP +nsOSPermissionRequestBase::MaybeRequestScreenCapturePermission() { + return NS_OK; +} diff --git a/dom/system/nsOSPermissionRequestBase.h b/dom/system/nsOSPermissionRequestBase.h new file mode 100644 index 0000000000..e1037b6047 --- /dev/null +++ b/dom/system/nsOSPermissionRequestBase.h @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOSPermissionRequestBase_h__ +#define nsOSPermissionRequestBase_h__ + +#include "nsIOSPermissionRequest.h" +#include "nsWeakReference.h" + +namespace mozilla::dom { +class Promise; +} // namespace mozilla::dom + +using mozilla::dom::Promise; + +/* + * The base implementation of nsIOSPermissionRequest to be subclassed on + * platforms that require permission requests for access to resources such + * as media captures devices. This implementation always returns results + * indicating access is permitted. + */ +class nsOSPermissionRequestBase : public nsIOSPermissionRequest, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOSPERMISSIONREQUEST + + nsOSPermissionRequestBase() = default; + + protected: + nsresult GetPromise(JSContext* aCx, RefPtr<Promise>& aPromiseOut); + virtual ~nsOSPermissionRequestBase() = default; +}; + +#endif diff --git a/dom/system/tests/chrome.toml b/dom/system/tests/chrome.toml new file mode 100644 index 0000000000..87f782b465 --- /dev/null +++ b/dom/system/tests/chrome.toml @@ -0,0 +1,6 @@ +[DEFAULT] + +["test_pathutils.html"] + +["test_pathutils_worker.xhtml"] +support-files = ["pathutils_worker.js"] diff --git a/dom/system/tests/file_bug1197901.html b/dom/system/tests/file_bug1197901.html new file mode 100644 index 0000000000..e4ca9fd380 --- /dev/null +++ b/dom/system/tests/file_bug1197901.html @@ -0,0 +1,16 @@ +<pre>Sensor events testing</pre> +<script> + +window.onmessage = function(event) { + if (event.data.command == "addEventListener") { + window.addEventListener( + "devicemotion", function() { + event.source.postMessage({ result: event.data.expected, + message: event.data.message }, + "*"); + } + ); + } +}; + +</script> diff --git a/dom/system/tests/ioutils/chrome.toml b/dom/system/tests/ioutils/chrome.toml new file mode 100644 index 0000000000..16434b5bb6 --- /dev/null +++ b/dom/system/tests/ioutils/chrome.toml @@ -0,0 +1,39 @@ +[DEFAULT] +support-files = [ + "file_ioutils_test_fixtures.js", + "file_ioutils_worker.js", +] + +["test_ioutils.html"] + +["test_ioutils_compute_hex_digest.html"] + +["test_ioutils_copy_move.html"] + +["test_ioutils_create_unique.html"] + +["test_ioutils_dir_iteration.html"] + +["test_ioutils_getfile.html"] + +["test_ioutils_mac_xattr.html"] +skip-if = ["(os != 'mac')"] + +["test_ioutils_mkdir.html"] + +["test_ioutils_read_write.html"] + +["test_ioutils_read_write_json.html"] + +["test_ioutils_read_write_utf8.html"] + +["test_ioutils_remove.html"] + +["test_ioutils_set_permissions.html"] + +["test_ioutils_stat_set_modification_time.html"] + +["test_ioutils_windows_file_attributes.html"] +skip-if = ["(os != 'win')"] + +["test_ioutils_worker.xhtml"] diff --git a/dom/system/tests/ioutils/file_ioutils_test_fixtures.js b/dom/system/tests/ioutils/file_ioutils_test_fixtures.js new file mode 100644 index 0000000000..5d2e5011c9 --- /dev/null +++ b/dom/system/tests/ioutils/file_ioutils_test_fixtures.js @@ -0,0 +1,78 @@ +// Utility functions. + +Uint8Array.prototype.equals = function equals(other) { + if (this.byteLength !== other.byteLength) { + return false; + } + return this.every((val, i) => val === other[i]); +}; + +async function createFile(location, contents = "") { + if (typeof contents === "string") { + contents = new TextEncoder().encode(contents); + } + await IOUtils.write(location, contents); + const exists = await fileExists(location); + ok(exists, `Created temporary file at: ${location}`); +} + +async function createDir(location) { + await IOUtils.makeDirectory(location, { + ignoreExisting: true, + createAncestors: true, + }); + const exists = await dirExists(location); + ok(exists, `Created temporary directory at: ${location}`); +} + +async function fileHasBinaryContents(location, expectedContents) { + if (!(expectedContents instanceof Uint8Array)) { + throw new TypeError("expectedContents must be a byte array"); + } + info(`Opening ${location} for reading`); + const bytes = await IOUtils.read(location); + return bytes.equals(expectedContents); +} + +async function fileHasTextContents(location, expectedContents) { + if (typeof expectedContents !== "string") { + throw new TypeError("expectedContents must be a string"); + } + info(`Opening ${location} for reading`); + const bytes = await IOUtils.read(location); + const contents = new TextDecoder().decode(bytes); + return contents === expectedContents; +} + +async function fileExists(file) { + try { + let { type } = await IOUtils.stat(file); + return type === "regular"; + } catch (ex) { + return false; + } +} + +async function dirExists(dir) { + try { + let { type } = await IOUtils.stat(dir); + return type === "directory"; + } catch (ex) { + return false; + } +} + +async function cleanup(...files) { + for (const file of files) { + await IOUtils.remove(file, { + ignoreAbsent: true, + recursive: true, + }); + const exists = await IOUtils.exists(file); + ok(!exists, `Removed temporary file: ${file}`); + } +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/dom/system/tests/ioutils/file_ioutils_worker.js b/dom/system/tests/ioutils/file_ioutils_worker.js new file mode 100644 index 0000000000..e367eb4d99 --- /dev/null +++ b/dom/system/tests/ioutils/file_ioutils_worker.js @@ -0,0 +1,219 @@ +// Any copyright is dedicated to the Public Domain. +// - http://creativecommons.org/publicdomain/zero/1.0/ + +// Portions of this file are originally from narwhal.js (http://narwhaljs.org) +// Copyright (c) 2009 Thomas Robinson <280north.com> +// MIT license: http://opensource.org/licenses/MIT + +/* eslint-env worker */ + +"use strict"; + +/* import-globals-from /testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js */ +importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js"); + +importScripts("file_ioutils_test_fixtures.js"); + +self.onmessage = async function (msg) { + const tmpDir = await PathUtils.getTempDir(); + + // IOUtils functionality is the same when called from the main thread, or a + // web worker. These tests are a modified subset of the main thread tests, and + // serve as a confidence check that the implementation is thread-safe. + await test_api_is_available_on_worker(); + await test_full_read_and_write(); + await test_move_file(); + await test_copy_file(); + await test_make_directory(); + + finish(); + info("test_ioutils_worker.xhtml: Test finished"); + + async function test_api_is_available_on_worker() { + ok(self.IOUtils, "IOUtils is present in web workers"); + } + + async function test_full_read_and_write() { + // Write a file. + const tmpFileName = PathUtils.join(tmpDir, "test_ioutils_numbers.tmp"); + const bytes = Uint8Array.of(...new Array(50).keys()); + const bytesWritten = await IOUtils.write(tmpFileName, bytes); + is(bytesWritten, 50, "IOUtils::write can write entire byte array to file"); + + // Read it back. + let fileContents = await IOUtils.read(tmpFileName); + ok( + _deepEqual(bytes, fileContents) && bytes.length == fileContents.length, + "IOUtils::read can read back entire file" + ); + + const tooManyBytes = bytes.length + 1; + fileContents = await IOUtils.read(tmpFileName, { maxBytes: tooManyBytes }); + ok( + _deepEqual(bytes, fileContents) && fileContents.length == bytes.length, + "IOUtils::read can read entire file when requested maxBytes is too large" + ); + + await cleanup(tmpFileName); + } + + async function test_move_file() { + const src = PathUtils.join(tmpDir, "test_move_file_src.tmp"); + const dest = PathUtils.join(tmpDir, "test_move_file_dest.tmp"); + const bytes = Uint8Array.of(...new Array(50).keys()); + await IOUtils.write(src, bytes); + + await IOUtils.move(src, dest); + ok( + !(await fileExists(src)) && (await fileExists(dest)), + "IOUtils::move can move files from a worker" + ); + + await cleanup(dest); + } + + async function test_copy_file() { + const tmpFileName = PathUtils.join(tmpDir, "test_ioutils_orig.tmp"); + const destFileName = PathUtils.join(tmpDir, "test_ioutils_copy.tmp"); + await createFile(tmpFileName, "original"); + + await IOUtils.copy(tmpFileName, destFileName); + ok( + (await fileExists(tmpFileName)) && + (await fileHasTextContents(destFileName, "original")), + "IOUtils::copy can copy source to dest in same directory" + ); + + await cleanup(tmpFileName, destFileName); + } + + async function test_make_directory() { + const dir = PathUtils.join(tmpDir, "test_make_dir.tmp.d"); + await IOUtils.makeDirectory(dir); + const stat = await IOUtils.stat(dir); + is( + stat.type, + "directory", + "IOUtils::makeDirectory can make a new directory from a worker" + ); + + await cleanup(dir); + } +}; + +// This is copied from the ObjectUtils module, as it is difficult to translate +// file_ioutils_test_fixtures.js into a ES module and have it used in non-module +// contexts. + +// ... Start of previously MIT-licensed code. +// This deepEqual implementation is originally from narwhal.js (http://narwhaljs.org) +// Copyright (c) 2009 Thomas Robinson <280north.com> +// MIT license: http://opensource.org/licenses/MIT + +function _deepEqual(a, b) { + // The numbering below refers to sections in the CommonJS spec. + + // 7.1 All identical values are equivalent, as determined by ===. + if (a === b) { + return true; + // 7.2 If the b value is a Date object, the a value is + // equivalent if it is also a Date object that refers to the same time. + } + let aIsDate = instanceOf(a, "Date"); + let bIsDate = instanceOf(b, "Date"); + if (aIsDate || bIsDate) { + if (!aIsDate || !bIsDate) { + return false; + } + if (isNaN(a.getTime()) && isNaN(b.getTime())) { + return true; + } + return a.getTime() === b.getTime(); + // 7.3 If the b value is a RegExp object, the a value is + // equivalent if it is also a RegExp object with the same source and + // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`). + } + let aIsRegExp = instanceOf(a, "RegExp"); + let bIsRegExp = instanceOf(b, "RegExp"); + if (aIsRegExp || bIsRegExp) { + return ( + aIsRegExp && + bIsRegExp && + a.source === b.source && + a.global === b.global && + a.multiline === b.multiline && + a.lastIndex === b.lastIndex && + a.ignoreCase === b.ignoreCase + ); + // 7.4 Other pairs that do not both pass typeof value == "object", + // equivalence is determined by ==. + } + if (typeof a != "object" || typeof b != "object") { + return a == b; + } + // 7.5 For all other Object pairs, including Array objects, equivalence is + // determined by having the same number of owned properties (as verified + // with Object.prototype.hasOwnProperty.call), the same set of keys + // (although not necessarily the same order), equivalent values for every + // corresponding key, and an identical 'prototype' property. Note: this + // accounts for both named and indexed properties on Arrays. + return objEquiv(a, b); +} + +function instanceOf(object, type) { + return Object.prototype.toString.call(object) == "[object " + type + "]"; +} + +function isUndefinedOrNull(value) { + return value === null || value === undefined; +} + +function isArguments(object) { + return instanceOf(object, "Arguments"); +} + +function objEquiv(a, b) { + if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) { + return false; + } + // An identical 'prototype' property. + if ((a.prototype || undefined) != (b.prototype || undefined)) { + return false; + } + // Object.keys may be broken through screwy arguments passing. Converting to + // an array solves the problem. + if (isArguments(a)) { + if (!isArguments(b)) { + return false; + } + a = Array.prototype.slice.call(a); + b = Array.prototype.slice.call(b); + return _deepEqual(a, b); + } + let ka, kb; + try { + ka = Object.keys(a); + kb = Object.keys(b); + } catch (e) { + // Happens when one is a string literal and the other isn't + return false; + } + // Having the same number of owned properties (keys incorporates + // hasOwnProperty) + if (ka.length != kb.length) { + return false; + } + // The same set of keys (although not necessarily the same order), + ka.sort(); + kb.sort(); + // Equivalent values for every corresponding key, and possibly expensive deep + // test + for (let key of ka) { + if (!_deepEqual(a[key], b[key])) { + return false; + } + } + return true; +} + +// ... End of previously MIT-licensed code. diff --git a/dom/system/tests/ioutils/test_ioutils.html b/dom/system/tests/ioutils/test_ioutils.html new file mode 100644 index 0000000000..cf62c4c388 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils.html @@ -0,0 +1,26 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script> + "use strict"; + + add_task(async function test_api_is_available_on_window() { + ok(window.IOUtils, "IOUtils is present on the window"); + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_compute_hex_digest.html b/dom/system/tests/ioutils/test_ioutils_compute_hex_digest.html new file mode 100644 index 0000000000..7a98f83a1f --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_compute_hex_digest.html @@ -0,0 +1,55 @@ + +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + add_task(async function test_computeHexDigest() { + const tempDir = PathUtils.join(PathUtils.tempDir, "ioutils-test-compute-hex-digest.tmp.d"); + await createDir(tempDir); + + const path = PathUtils.join(tempDir, "file"); + await IOUtils.writeUTF8(path, "hello world\n"); + + const DIGESTS = [ + "a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447", + "6b3b69ff0a404f28d75e98a066d3fc64fffd9940870cc68bece28545b9a75086b343d7a1366838083e4b8f3ca6fd3c80", + "db3974a97f2407b7cae1ae637c0030687a11913274d578492558e39c16c017de84eacdc8c62fe34ee4e12b4b1428817f09b6a2760c3f8a664ceae94d2434a593", + ]; + const ALGORITHMS = ["sha256", "sha384", "sha512"]; + + for (let i = 0; i < ALGORITHMS.length; i++) { + const alg = ALGORITHMS[i]; + const expected = DIGESTS[i]; + + Assert.equal( + await IOUtils.computeHexDigest(path, alg), + expected, + `IOUtils.hashFile() has expected value for ${alg}`); + } + + await cleanup(tempDir); + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_copy_move.html b/dom/system/tests/ioutils/test_ioutils_copy_move.html new file mode 100644 index 0000000000..408bb82f39 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_copy_move.html @@ -0,0 +1,360 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + add_task(async function test_move_relative_path() { + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_move_relative_path.tmp"); + const dest = "relative_to_cwd.tmp"; + await createFile(tmpFileName, "source"); + + info("Test moving a file to a relative destination"); + await Assert.rejects( + IOUtils.move(tmpFileName, dest), + /Could not parse path/, + "IOUtils::move only works with absolute paths" + ); + ok( + await fileHasTextContents(tmpFileName, "source"), + "IOUtils::move doesn't change source file when move fails" + ); + + await cleanup(tmpFileName); + }); + + add_task(async function test_move_rename() { + // Set up. + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_move_src.tmp"); + const destFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_move_dest.tmp"); + await createFile(tmpFileName, "dest"); + // Test. + info("Test move to new file in same directory"); + await IOUtils.move(tmpFileName, destFileName); + info(`Moved ${tmpFileName} to ${destFileName}`); + ok( + !await fileExists(tmpFileName) + && await fileHasTextContents(destFileName, "dest"), + "IOUtils::move can move source to dest in same directory" + ) + + // Set up. + info("Test move to existing file with no overwrite"); + await createFile(tmpFileName, "source"); + // Test. + await Assert.rejects( + IOUtils.move(tmpFileName, destFileName, { noOverwrite: true }), + /Could not move source file\(.*\) to destination\(.*\) because the destination already exists and overwrites are not allowed/, + "IOUtils::move will refuse to move a file if overwrites are disabled" + ); + ok( + await fileExists(tmpFileName) + && await fileHasTextContents(destFileName, "dest"), + "Failed IOUtils::move doesn't move the source file" + ); + + // Test. + info("Test move to existing file with overwrite"); + await IOUtils.move(tmpFileName, destFileName, { noOverwrite: false }); + ok(!await fileExists(tmpFileName), "IOUtils::move moved source"); + ok( + await fileHasTextContents(destFileName, "source"), + "IOUtils::move overwrote the destination with the source" + ); + + // Clean up. + await cleanup(tmpFileName, destFileName); + }); + + add_task(async function test_move_to_dir() { + // Set up. + info("Test move and rename to non-existing directory"); + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_move_to_dir.tmp"); + const destDir = PathUtils.join(PathUtils.tempDir, "test_move_to_dir.tmp.d"); + const dest = PathUtils.join(destDir, "dest.tmp"); + await createFile(tmpFileName); + // Test. + ok(!await IOUtils.exists(destDir), "Expected path not to exist"); + await IOUtils.move(tmpFileName, dest); + ok( + !await fileExists(tmpFileName) && await fileExists(dest), + "IOUtils::move creates non-existing parents if needed" + ); + + // Set up. + info("Test move and rename to existing directory.") + await createFile(tmpFileName); + // Test. + ok(await dirExists(destDir), `Expected ${destDir} to be a directory`); + await IOUtils.move(tmpFileName, dest); + ok( + !await fileExists(tmpFileName) + && await fileExists(dest), + "IOUtils::move can move/rename a file into an existing dir" + ); + + // Set up. + info("Test move to existing directory without specifying leaf name.") + await createFile(tmpFileName); + // Test. + await IOUtils.move(tmpFileName, destDir); + ok(await dirExists(destDir), `Expected ${destDir} to be a directory`); + ok( + !await fileExists(tmpFileName) + && await fileExists(PathUtils.join(destDir, PathUtils.filename(tmpFileName))), + "IOUtils::move can move a file into an existing dir" + ); + + // Clean up. + await cleanup(destDir); + }); + + add_task(async function test_move_dir() { + // Set up. + info("Test rename an empty directory"); + const srcDir = PathUtils.join(PathUtils.tempDir, "test_move_dir.tmp.d"); + const destDir = PathUtils.join(PathUtils.tempDir, "test_move_dir_dest.tmp.d"); + await createDir(srcDir); + // Test. + await IOUtils.move(srcDir, destDir); + ok( + !await IOUtils.exists(srcDir) && await dirExists(destDir), + "IOUtils::move can rename directories" + ); + + // Set up. + info("Test move directory and its content into another directory"); + await createDir(srcDir); + await createFile(PathUtils.join(srcDir, "file.tmp"), "foo"); + // Test. + await IOUtils.move(srcDir, destDir); + const destFile = PathUtils.join(destDir, PathUtils.filename(srcDir), "file.tmp"); + ok( + !await IOUtils.exists(srcDir) + && await dirExists(destDir) + && await dirExists(PathUtils.join(destDir, PathUtils.filename(srcDir))) + && await fileHasTextContents(destFile, "foo"), + "IOUtils::move can move a directory and its contents into another one" + ) + + // Clean up. + await cleanup(srcDir, destDir); + }); + + add_task(async function test_move_failures() { + // Set up. + info("Test attempt to rename a non-existent source file"); + const notExistsSrc = PathUtils.join(PathUtils.tempDir, "not_exists_src.tmp"); + const notExistsDest = PathUtils.join(PathUtils.tempDir, "not_exists_dest.tmp"); + // Test. + await Assert.rejects( + IOUtils.move(notExistsSrc, notExistsDest), + /Could not move source file\(.*\) because it does not exist/, + "IOUtils::move throws if source file does not exist" + ); + ok( + !await fileExists(notExistsSrc) && !await fileExists(notExistsDest), + "IOUtils::move fails if source file does not exist" + ); + + // Set up. + info("Test attempt to move a directory to a file"); + const destFile = PathUtils.join(PathUtils.tempDir, "test_move_failures_file_dest.tmp"); + const srcDir = PathUtils.join(PathUtils.tempDir, "test_move_failure_src.tmp.d"); + await createFile(destFile); + await createDir(srcDir); + // Test. + await Assert.rejects( + IOUtils.move(srcDir, destFile), + /Could not move the source directory\(.*\) to the destination\(.*\) because the destination is not a directory/, + "IOUtils::move throws if try to move dir into an existing file" + ); + + // Clean up. + await cleanup(destFile, srcDir); + }); + + add_task(async function test_copy() { + // Set up. + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_orig.tmp"); + const destFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_copy.tmp"); + await createFile(tmpFileName, "original"); + // Test. + info("Test copy to new file in same directory"); + await IOUtils.copy(tmpFileName, destFileName); + ok( + await fileExists(tmpFileName) + && await fileHasTextContents(destFileName, "original"), + "IOUtils::copy can copy source to dest in same directory" + ); + + // Set up. + info("Test copy to existing file with no overwrite"); + await createFile(tmpFileName, "new contents"); + // Test. + await Assert.rejects( + IOUtils.copy(tmpFileName, destFileName, { noOverwrite: true }), + /Could not copy source file\(.*\) to destination\(.*\) because the destination already exists and overwrites are not allowed/, + "IOUtils::copy will refuse to copy to existing destination if overwrites are disabled" + ); + ok( + await fileExists(tmpFileName) + && await fileHasTextContents(destFileName, "original"), + "Failed IOUtils::move doesn't move the source file" + ); + + // Test. + info("Test copy to existing file with overwrite"); + await IOUtils.copy(tmpFileName, destFileName, { noOverwrite: false }); + ok(await fileExists(tmpFileName), "IOUtils::copy retains source"); + ok( + await fileHasTextContents(destFileName, "new contents"), + "IOUtils::copy overwrote the destination with the source" + ); + + // Clean up. + await cleanup(tmpFileName, destFileName); + }); + + add_task(async function test_copy_file_to_dir() { + // Set up. + info("Test copy file to non-existing directory"); + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_copy_file_to_dir.tmp"); + const destDir = PathUtils.join(PathUtils.tempDir, "test_copy_file_to_dir.tmp.d"); + const dest = PathUtils.join(destDir, "dest.tmp"); + await createFile(tmpFileName); + // Test. + ok(!await IOUtils.exists(destDir), "Expected path not to exist"); + await IOUtils.copy(tmpFileName, dest); + ok( + await fileExists(tmpFileName) && await fileExists(dest), + "IOUtils::copy creates non-existing parents if needed" + ); + + // Set up. + info("Test copy file to existing directory") + await createFile(tmpFileName); + // Test. + ok(await dirExists(destDir), `Expected ${destDir} to be a directory`); + await IOUtils.copy(tmpFileName, dest); + ok( + await fileExists(tmpFileName) + && await fileExists(dest), + "IOUtils::copy can copy a file into an existing dir" + ); + + // Set up. + info("Test copy file to existing directory without specifying leaf name") + await createFile(tmpFileName); + // Test. + await IOUtils.copy(tmpFileName, destDir); + ok(await dirExists(destDir), `Expected ${destDir} to be a directory`); + ok( + await fileExists(tmpFileName) + && await fileExists(PathUtils.join(destDir, PathUtils.filename(tmpFileName))), + "IOUtils::copy can copy a file into an existing dir" + ); + + // Clean up. + await cleanup(tmpFileName, destDir); + }); + + add_task(async function test_copy_dir_recursive() { + // Set up. + info("Test rename an empty directory"); + const srcDir = PathUtils.join(PathUtils.tempDir, "test_copy_dir.tmp.d"); + const destDir = PathUtils.join(PathUtils.tempDir, "test_copy_dir_dest.tmp.d"); + await createDir(srcDir); + // Test. + await IOUtils.copy(srcDir, destDir, { recursive: true }); + ok( + await dirExists(srcDir) && await dirExists(destDir), + "IOUtils::copy can recursively copy entire directories" + ); + + // Set up. + info("Test copy directory and its content into another directory"); + await createDir(srcDir); + await createFile(PathUtils.join(srcDir, "file.tmp"), "foo"); + // Test. + await IOUtils.copy(srcDir, destDir, { recursive: true }); + const destFile = PathUtils.join(destDir, PathUtils.filename(srcDir), "file.tmp"); + ok( + await dirExists(srcDir) + && await dirExists(destDir) + && await dirExists(PathUtils.join(destDir, PathUtils.filename(srcDir))) + && await fileHasTextContents(destFile, "foo"), + "IOUtils::copy can move a directory and its contents into another one" + ) + + // Clean up. + await cleanup(srcDir, destDir); + }); + + add_task(async function test_copy_failures() { + // Set up. + info("Test attempt to copy a non-existent source file"); + const notExistsSrc = PathUtils.join(PathUtils.tempDir, "test_copy_not_exists_src.tmp"); + const notExistsDest = PathUtils.join(PathUtils.tempDir, "test_copy_not_exists_dest.tmp"); + // Test. + await Assert.rejects( + IOUtils.copy(notExistsSrc, notExistsDest), + /Could not copy source file\(.*\) because it does not exist/, + "IOUtils::copy throws if source file does not exist" + ); + ok( + !await fileExists(notExistsSrc) && !await fileExists(notExistsDest), + "IOUtils::copy failure due to missing source file does not affect destination" + ); + + // Set up. + info("Test attempt to copy a directory to a file"); + const destFile = PathUtils.join(PathUtils.tempDir, "test_copy_failures_file_dest.tmp"); + const srcDir = PathUtils.join(PathUtils.tempDir, "test_copy_failure_src.tmp.d"); + await createFile(destFile); + await createDir(srcDir); + // Test. + await Assert.rejects( + IOUtils.copy(srcDir, destFile, { recursive: true }), + /Could not copy the source directory\(.*\) to the destination\(.*\) because the destination is not a directory/, + "IOUtils::copy throws if try to move dir into an existing file" + ); + ok(await fileHasTextContents(destFile, ""), "IOUtils::copy failure does not affect destination"); + + // Set up. + info("Test copy directory without recursive option"); + await createDir(srcDir); + // Test. + await Assert.rejects( + IOUtils.copy(srcDir, notExistsDest, { recursive: false }), + /Refused to copy source directory\(.*\) to the destination\(.*\)/, + "IOUtils::copy throws if try to copy a directory with { recursive: false }" + ); + console.log(`${notExistsDest} exists?`, await IOUtils.exists(notExistsDest)) + ok(!await IOUtils.exists(notExistsDest), "IOUtils::copy failure does not affect destination"); + + // Clean up. + await cleanup(destFile, srcDir); + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_create_unique.html b/dom/system/tests/ioutils/test_ioutils_create_unique.html new file mode 100644 index 0000000000..be7ab23697 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_create_unique.html @@ -0,0 +1,86 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + function octalFormat(n) { + let s = n.toString(8); + while (s.length < 3) { + s = `0${s}`; + } + return `0o${s}`; + } + + async function check(method, path, prefix, type, perms) { + const filename = PathUtils.filename(path); + + ok(filename.startsWith(prefix), `IOUtils.${method} uses the prefix`); + ok(await IOUtils.exists(path), `IOUtils.${method} creates a file`); + + const stat = await IOUtils.stat(path); + is(stat.type, type, `IOUtils.${method} creates a "${type}" file`); + + is( + octalFormat(stat.permissions), + octalFormat(perms), + `IOUtils.${method} creates a file with the correct permissions` + ); + } + + add_task(async function test_createUnique() { + const tempDir = PathUtils.join( + PathUtils.tempDir, + "test_createUnique.tmp.d" + ); + + const filesToChmod = []; + + SimpleTest.registerCleanupFunction(async function test_createUnique_cleanup() { + for (const file of filesToChmod) { + if (await IOUtils.exists(file)) { + await IOUtils.setPermissions(file, 0o666); + } + } + + await IOUtils.remove(tempDir, { recursive: true }); + }); + + const isWindows = Services.appinfo.OS === "WINNT"; + + info("Creating a unique directory") + const dir = await IOUtils.createUniqueDirectory(tempDir, "unique-dir", 0o600); + await check("createUniqueDirectory", dir, "unique-dir", "directory", isWindows ? 0o666 : 0o600); + + info("Creating a unique directory with the same prefix") + const dir2 = await IOUtils.createUniqueDirectory(tempDir, "unique-dir", 0o700); + await check("createUniqueDirectory", dir2, "unique-dir", "directory", isWindows ? 0o666 : 0o700); + ok(dir !== dir2, "IOUtils.createUniqueDirectory creates unique paths"); + + info("Creating a unique file"); + const file = await IOUtils.createUniqueFile(tempDir, "unique-file", 0o641); + await check("createUniqueFile", file, "unique-file", "regular", isWindows ? 0o666 : 0o641); + + info("Creating a unique file with the same prefix"); + const file2 = await IOUtils.createUniqueFile(tempDir, "unique-file", 0o400); + filesToChmod.push(file2); + await check("createUniqueFile", file2, "unique-file", "regular", isWindows ? 0o444 : 0o400); + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_dir_iteration.html b/dom/system/tests/ioutils/test_ioutils_dir_iteration.html new file mode 100644 index 0000000000..54168235b0 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_dir_iteration.html @@ -0,0 +1,96 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + add_task(async function iterate_dir_failure() { + let notExists = PathUtils.join(PathUtils.tempDir, 'does_not_exist_dir.tmp.d'); + + await Assert.rejects( + IOUtils.getChildren(notExists), + /Could not get children of file\(.*\) because it does not exist/, + "IOUtils::getChildren rejects if the file does not exist" + ); + ok(!await fileExists(notExists), `Expected ${notExists} not to exist`); + + info('Try to get the children of a regular file'); + + let tmpFileName = PathUtils.join(PathUtils.tempDir, 'iterator_file.tmp'); + await createFile(tmpFileName) + await Assert.rejects(IOUtils.getChildren(tmpFileName), + /Could not get children of file\(.*\) because it is not a directory/, + "IOUtils::getChildren rejects if the file is not a dir" + ); + + await cleanup(tmpFileName); + }); + + add_task(async function iterate_dir() { + info('Try to get the children of a multi-level directory hierarchy'); + + let root = PathUtils.join(PathUtils.tempDir, 'iterator.tmp.d'); + let child1 = PathUtils.join(root, 'child1.tmp'); + let child2 = PathUtils.join(root, 'child2.tmp'); + let grandchild = PathUtils.join(child1, 'grandchild.tmp'); + + await createDir(grandchild); // Ancestors will be created. + await createDir(child2); + + let entries = await IOUtils.getChildren(root); + + is(entries.length, 2, `Expected 2 entries below the path at ${root}`); + ok(!entries.includes(grandchild), "IOUtils::getChildren does not enter subdirectories"); + + await cleanup(root); + }); + + add_task(async function iterate_empty_dir() { + info('Try to get the children of an empty directory'); + + let emptyDir = PathUtils.join(PathUtils.tempDir, 'iterator_empty_dir.tmp.d'); + await createDir(emptyDir); + + is( + (await IOUtils.getChildren(emptyDir)).length, + 0, + "IOUtils::getChildren return an empty array when called on an empty dir" + ); + + await cleanup(emptyDir); + }); + + add_task(async function iterate_ignore_missing_dir() { + info("Try to get the children of a missing file with ignoreAbsent"); + + const notExists = PathUtils.join(PathUtils.tempDir, "does_not_exist_dir.tmp.d"); + + is( + (await IOUtils.getChildren(notExists, { ignoreAbsent: true })).length, + 0, + "IOUtils::getChildren returns an empty array when called with ignoreAbsent on a missing file" + ); + ok(!await fileExists(notExists), `Expected ${notExists} not to exist`); + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_getfile.html b/dom/system/tests/ioutils/test_ioutils_getfile.html new file mode 100644 index 0000000000..077f0b5c1c --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_getfile.html @@ -0,0 +1,84 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script> + "use strict"; + + const TEST_PATH = PathUtils.join(PathUtils.tempDir, "test-ioutils-getfile"); + + add_task(async function test_getFile() { + const expectedPath = PathUtils.join(TEST_PATH, "foo", "bar", "baz", "get-file.txt"); + const parentPath = PathUtils.parent(expectedPath); + + ok(!(await IOUtils.exists(parentPath)), "Parent directory should not exist"); + + const file = await IOUtils.getFile(TEST_PATH, "foo", "bar", "baz", "get-file.txt"); + const path = file.path; + + is(path, expectedPath, "Should have the correct path"); + ok(await IOUtils.exists(parentPath), "Parent directory should be created"); + ok(!(await IOUtils.exists(path)), "File should not be created"); + + await IOUtils.remove(TEST_PATH, { recursive: true }); + }); + + add_task(async function test_getFile_exists() { + const expectedPath = PathUtils.join(TEST_PATH, "foo", "bar", "baz", "get-file-exists.txt"); + + await IOUtils.makeDirectory(PathUtils.parent(expectedPath)); + await IOUtils.writeUTF8(expectedPath, "hello world"); + + const file = await IOUtils.getFile(TEST_PATH, "foo", "bar", "baz", "get-file-exists.txt"); + is(file.path, expectedPath, "Should have the correct path"); + is(await IOUtils.readUTF8(file.path), "hello world", "Contents should be unchanged"); + + await IOUtils.remove(TEST_PATH, { recursive: true }); + }); + + add_task(async function test_getDirectory() { + const expectedPath = PathUtils.join(TEST_PATH, "qux", "quux", "corge"); + + ok(!(await IOUtils.exists(PathUtils.parent(expectedPath))), "Parent directory should not exist"); + + const file = await IOUtils.getDirectory(TEST_PATH, "qux", "quux", "corge"); + + is(file.path, expectedPath, "Should have the correct path"); + ok(await IOUtils.exists(expectedPath), "Directory should be created"); + + const info = await IOUtils.stat(expectedPath); + is(info.type, "directory", "Should create a directory"); + + await IOUtils.remove(TEST_PATH, { recursive: true }); + }); + + add_task(async function test_getDirectory_exists() { + const expectedPath = PathUtils.join(TEST_PATH, "qux", "quux", "corge"); + + await IOUtils.makeDirectory(expectedPath); + + const file = await IOUtils.getDirectory(TEST_PATH, "qux", "quux", "corge"); + is(file.path, expectedPath, "Should have the correct path"); + ok(await IOUtils.exists(expectedPath), "Directory should still exist"); + + const info = await IOUtils.stat(expectedPath); + is(info.type, "directory", "Should still be a directory"); + + await IOUtils.remove(TEST_PATH, { recursive: true }); + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_mac_xattr.html b/dom/system/tests/ioutils/test_ioutils_mac_xattr.html new file mode 100644 index 0000000000..6af9b2e6f8 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_mac_xattr.html @@ -0,0 +1,91 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + const ATTR = "bogus.attr"; + const VALUE = new TextEncoder().encode("bogus"); + + add_task(async function test_macXAttr() { + const tmpDir = PathUtils.join(PathUtils.tempDir, "ioutils-macos-xattr.tmp.d"); + + await createDir(tmpDir); + + const path = PathUtils.join(tmpDir, "file.tmp"); + ok(!await IOUtils.exists(path), "File should not exist"); + await IOUtils.writeUTF8(path, ""); + + ok( + !await IOUtils.hasMacXAttr(path, ATTR), + "File does not have an extended attribute at creation" + ); + + info("Testing getting an attribute that does not exist"); + await Assert.rejects( + IOUtils.getMacXAttr(path, ATTR), + /NotFoundError: The file `.+' does not have an extended attribute/, + "IOUtils::getMacXAttr rejects when the attribute does not exist" + ); + + info("Testing setting an attribute"); + await IOUtils.setMacXAttr(path, ATTR, VALUE); + ok( + await IOUtils.hasMacXAttr(path, ATTR), + "File has extended attribute after setting" + ); + + { + info("Testing getting an attribute") + const value = await IOUtils.getMacXAttr(path, ATTR); + Assert.deepEqual( + Array.from(value), + Array.from(VALUE), + "Attribute value should match" + ); + } + + info("Testing removing an attribute"); + await IOUtils.delMacXAttr(path, ATTR); + await Assert.rejects( + IOUtils.getMacXAttr(path, ATTR), + /NotFoundError: The file `.+' does not have an extended attribute/, + "IOUtils::delMacXAttr removes the attribute" + ); + + ok( + !await IOUtils.hasMacXAttr(path, ATTR), + "File does not have extended attribute after removing" + ); + + info("Testing removing an attribute that does not exist"); + await Assert.rejects( + IOUtils.delMacXAttr(path, ATTR), + /NotFoundError: The file `.+' does not have an extended attribute/, + "IOUtils::delMacXAttr rejects when the attribute does not exist" + ); + + await cleanup(tmpDir); + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_mkdir.html b/dom/system/tests/ioutils/test_ioutils_mkdir.html new file mode 100644 index 0000000000..6827b24cc6 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_mkdir.html @@ -0,0 +1,135 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + add_task(async function test_make_directory() { + info("Test creating a new directory"); + const newDirectoryName = PathUtils.join(PathUtils.tempDir, "test_ioutils_new_dir.tmp.d"); + await IOUtils.makeDirectory(newDirectoryName); + ok( + await IOUtils.exists(newDirectoryName), + "IOUtils::makeDirectory can create a new directory" + ); + + info("Test creating an existing directory"); + await IOUtils.makeDirectory(newDirectoryName, { ignoreExisting: true }); + ok( + await IOUtils.exists(newDirectoryName), + "IOUtils::makeDirectory can ignore existing directories" + ); + await Assert.rejects( + IOUtils.makeDirectory(newDirectoryName, { ignoreExisting: false }), + /Could not create directory because it already exists at .*/, + "IOUtils::makeDirectory can throw if the target dir exists" + ) + + info("Test creating a nested directory"); + const parentDirName = PathUtils.join(PathUtils.tempDir, "test_ioutils_mkdir_parent.tmp.d"); + const nestedDirName = PathUtils.join( + parentDirName, + "test_ioutils_mkdir_child.tmp.d" + ); + await Assert.rejects( + IOUtils.makeDirectory(nestedDirName, { createAncestors: false }), + /Could not create directory at .*/, + "IOUtils::makeDirectory can fail if the target is missing parents" + ); + ok(!await IOUtils.exists(nestedDirName), `Expected ${nestedDirName} not to exist`); + await IOUtils.makeDirectory(nestedDirName, { createAncestors: true }); + ok( + await IOUtils.exists(nestedDirName), + "IOUtils::makeDirectory can create ancestors of the target directory" + ); + + await cleanup(newDirectoryName, parentDirName); + }); + + add_task(async function test_make_directory_failure() { + info("Try to create a directory where a file already exists"); + const notADirFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_not_a_dir.tmp"); + await createFile(notADirFileName); + + await Assert.rejects( + IOUtils.makeDirectory(notADirFileName, { ignoreExisting: false }), + /Could not create directory because the target file\(.*\) exists and is not a directory/, + "IOUtils::makeDirectory [ignoreExisting: false] throws when the target is an existing file" + ); + ok(await fileExists(notADirFileName), `Expected ${notADirFileName} to exist`); + + await Assert.rejects( + IOUtils.makeDirectory(notADirFileName, { ignoreExisting: true }), + /Could not create directory because the target file\(.*\) exists and is not a directory/, + "IOUtils::makeDirectory [ignoreExisting: true] throws when the target is an existing file" + ); + ok(await fileExists(notADirFileName), `Expected ${notADirFileName} to exist`); + + await cleanup(notADirFileName); + }); + + add_task(async function test_make_directory_permissions() { + if (Services.appinfo.OS === "WINNT") { + ok(true, "Skipping test on unsupported platform (Windows)"); + return; + } + + const newDir = PathUtils.join(PathUtils.tempDir, "test_ioutils_mkdir_perms.tmp.d"); + + ok(!await IOUtils.exists(newDir), "Directory does not exist before creation"); + await IOUtils.makeDirectory(newDir, { permissions: 0o751 }); + ok(await IOUtils.exists(newDir), "Directory created"); + + const stat = await IOUtils.stat(newDir); + is(stat.type, "directory", "Directory stat() as directory"); + is(stat.permissions, 0o751, "Directory created with expected permissions"); + + await cleanup(newDir); + }); + + add_task(async function test_make_directory_root() { + if (Services.appinfo.OS === "WINNT") { + // We don't actually know the root drive, but we can find the root drive + // of the profile directory. + let current = PathUtils.profileDir; + let parent = PathUtils.parent(current); + while (parent !== null) { + current = parent; + parent = PathUtils.parent(current); + } + // `current` will now be a valid root directory. + ok(await IOUtils.exists(current), "Root directory should exist"); + + const DRIVE_RE = /^[A-Za-z]:$/; + ok( + current.startsWith("\\\\") || DRIVE_RE.test(current), + `Root directory (${current}) should be a UNC path or drive`, + ); + await IOUtils.makeDirectory(current, {createAncestors: false}); + } else { + ok(await IOUtils.exists("/"), "Root directory should exist"); + await IOUtils.makeDirectory("/", {createAncestors: false}); + } + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_read_write.html b/dom/system/tests/ioutils/test_ioutils_read_write.html new file mode 100644 index 0000000000..2243eb1eda --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_read_write.html @@ -0,0 +1,524 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + const { ObjectUtils } = ChromeUtils.importESModule( + "resource://gre/modules/ObjectUtils.sys.mjs" + ); + + add_task(async function test_read_failure() { + const doesNotExist = PathUtils.join(PathUtils.tempDir, "does_not_exist.tmp"); + await Assert.rejects( + IOUtils.read(doesNotExist), + /Could not open the file at .*/, + "IOUtils::read rejects when file does not exist" + ); + }); + + add_task(async function test_write_no_overwrite() { + // Make a new file, and try to write to it with overwrites disabled. + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_overwrite.tmp"); + const untouchableContents = new TextEncoder().encode("Can't touch this!\n"); + + let exists = await IOUtils.exists(tmpFileName); + ok(!exists, `File ${tmpFileName} should not exist before writing`); + + await IOUtils.write(tmpFileName, untouchableContents); + + exists = await IOUtils.exists(tmpFileName); + ok(exists, `File ${tmpFileName} should exist after writing`); + + const newContents = new TextEncoder().encode("Nah nah nah!\n"); + await Assert.rejects( + IOUtils.write(tmpFileName, newContents, { + mode: "create", + }), + /Refusing to overwrite the file at */, + "IOUtils::write rejects writing to existing file if overwrites are disabled" + ); + ok( + await fileHasBinaryContents(tmpFileName, untouchableContents), + "IOUtils::write doesn't change target file when overwrite is refused" + ); + + const bytesWritten = await IOUtils.write( + tmpFileName, + newContents, + { mode: "overwrite" } + ); + is( + bytesWritten, + newContents.length, + "IOUtils::write can overwrite files if specified" + ); + + await cleanup(tmpFileName); + }); + + add_task(async function test_write_with_backup() { + info("Test backup file option with non-existing file"); + + let fileContents = new TextEncoder().encode("Original file contents"); + let destFileName = PathUtils.join(PathUtils.tempDir, "test_write_with_backup_option.tmp"); + let backupFileName = destFileName + ".backup"; + let bytesWritten = + await IOUtils.write(destFileName, fileContents, { + backupFile: backupFileName, + }); + ok( + await fileHasTextContents(destFileName, "Original file contents"), + "IOUtils::write creates a new file with the correct contents" + ); + ok( + !await fileExists(backupFileName), + "IOUtils::write does not create a backup if the target file does not exist" + ); + is( + bytesWritten, + fileContents.length, + "IOUtils::write correctly writes to a new file without performing a backup" + ); + + info("Test backup file option with existing destination"); + let newFileContents = new TextEncoder().encode("New file contents"); + ok(await fileExists(destFileName), `Expected ${destFileName} to exist`); + bytesWritten = + await IOUtils.write(destFileName, newFileContents, { + backupFile: backupFileName, + }); + ok( + await fileHasTextContents(backupFileName, "Original file contents"), + "IOUtils::write can backup an existing file before writing" + ); + ok( + await fileHasTextContents(destFileName, "New file contents"), + "IOUtils::write can create the target with the correct contents" + ); + is( + bytesWritten, + newFileContents.length, + "IOUtils::write correctly writes to the target after taking a backup" + ); + + await cleanup(destFileName, backupFileName); + }); + + add_task(async function test_write_with_backup_and_tmp() { + info("Test backup with tmp and backup file options, non-existing destination"); + + let fileContents = new TextEncoder().encode("Original file contents"); + let destFileName = PathUtils.join(PathUtils.tempDir, "test_write_with_backup_and_tmp_options.tmp"); + let backupFileName = destFileName + ".backup"; + let tmpFileName = PathUtils.join(PathUtils.tempDir, "temp_file.tmp"); + let bytesWritten = + await IOUtils.write(destFileName, fileContents, { + backupFile: backupFileName, + tmpPath: tmpFileName, + }); + ok(!await fileExists(tmpFileName), "IOUtils::write cleans up the tmpFile"); + ok( + !await fileExists(backupFileName), + "IOUtils::write does not create a backup if the target file does not exist" + ); + ok( + await fileHasTextContents(destFileName, "Original file contents"), + "IOUtils::write can write to the destination when a temporary file is used" + ); + is( + bytesWritten, + fileContents.length, + "IOUtils::write can copy tmp file to destination without performing a backup" + ); + + info("Test backup with tmp and backup file options, existing destination"); + let newFileContents = new TextEncoder().encode("New file contents"); + ok(await fileExists(destFileName), `Expected ${destFileName} to exist`); + bytesWritten = + await IOUtils.write(destFileName, newFileContents, { + backupFile: backupFileName, + tmpPath: tmpFileName, + }); + + ok(!await fileExists(tmpFileName), "IOUtils::write cleans up the tmpFile"); + ok( + await fileHasTextContents(backupFileName, "Original file contents"), + "IOUtils::write can create a backup if the target file exists" + ); + ok( + await fileHasTextContents(destFileName, "New file contents"), + "IOUtils::write can write to the destination when a temporary file is used" + ); + is( + bytesWritten, + newFileContents.length, + "IOUtils::write IOUtils::write can move tmp file to destination after performing a backup" + ); + + info("Test backup with tmp and backup file options, existing destination and backup"); + newFileContents = new TextEncoder().encode("Updated new file contents"); + ok(await fileExists(destFileName), `Expected ${destFileName} to exist`); + ok(await fileExists(backupFileName), `Expected ${backupFileName} to exist`); + bytesWritten = + await IOUtils.write(destFileName, newFileContents, { + backupFile: backupFileName, + tmpPath: tmpFileName, + }); + + ok(!await fileExists(tmpFileName), "IOUtils::write cleans up the tmpFile"); + ok( + await fileHasTextContents(backupFileName, "New file contents"), + "IOUtils::write can create a backup if the target file exists" + ); + ok( + await fileHasTextContents(destFileName, "Updated new file contents"), + "IOUtils::write can write to the destination when a temporary file is used" + ); + is( + bytesWritten, + newFileContents.length, + "IOUtils::write IOUtils::write can move tmp file to destination after performing a backup" + ); + + await cleanup(destFileName, backupFileName); + }); + + add_task(async function test_partial_read() { + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_partial_read.tmp"); + const bytes = Uint8Array.of(...new Array(50).keys()); + const bytesWritten = await IOUtils.write(tmpFileName, bytes); + is( + bytesWritten, + 50, + "IOUtils::write can write entire byte array to file" + ); + + // Read just the first 10 bytes. + const first10 = bytes.slice(0, 10); + const bytes10 = await IOUtils.read(tmpFileName, { maxBytes: 10 }); + ok( + ObjectUtils.deepEqual(bytes10, first10), + "IOUtils::read can read part of a file, up to specified max bytes" + ); + + // Trying to explicitly read nothing isn't useful, but it should still + // succeed. + const bytes0 = await IOUtils.read(tmpFileName, { maxBytes: 0 }); + is(bytes0.length, 0, "IOUtils::read can read 0 bytes"); + + await cleanup(tmpFileName); + }); + + add_task(async function test_empty_read_and_write() { + // Trying to write an empty file isn't very useful, but it should still + // succeed. + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_empty.tmp"); + const emptyByteArray = new Uint8Array(0); + const bytesWritten = await IOUtils.write( + tmpFileName, + emptyByteArray + ); + is(bytesWritten, 0, "IOUtils::write can create an empty file"); + + // Trying to explicitly read nothing isn't useful, but it should still + // succeed. + const bytes0 = await IOUtils.read(tmpFileName, { maxBytes: 0 }); + is(bytes0.length, 0, "IOUtils::read can read 0 bytes"); + + // Implicitly try to read nothing. + const nothing = await IOUtils.read(tmpFileName); + is(nothing.length, 0, "IOUtils:: read can read empty files"); + + await cleanup(tmpFileName); + }); + + add_task(async function test_full_read_and_write() { + // Write a file. + + info("Test writing to a new binary file"); + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_numbers.tmp"); + const bytes = Uint8Array.of(...new Array(50).keys()); + const bytesWritten = await IOUtils.write(tmpFileName, bytes); + is( + bytesWritten, + 50, + "IOUtils::write can write entire byte array to file" + ); + + // Read it back. + info("Test reading a binary file"); + let fileContents = await IOUtils.read(tmpFileName); + ok( + ObjectUtils.deepEqual(bytes, fileContents) && + bytes.length == fileContents.length, + "IOUtils::read can read back entire file" + ); + + const tooManyBytes = bytes.length + 1; + fileContents = await IOUtils.read(tmpFileName, { maxBytes: tooManyBytes }); + ok( + ObjectUtils.deepEqual(bytes, fileContents) && + fileContents.length == bytes.length, + "IOUtils::read can read entire file when requested maxBytes is too large" + ); + + // Clean up. + await cleanup(tmpFileName); + }); + + add_task(async function test_write_relative_path() { + const tmpFileName = "test_ioutils_write_relative_path.tmp"; + const bytes = Uint8Array.of(...new Array(50).keys()); + + info("Test writing a file at a relative destination"); + await Assert.rejects( + IOUtils.write(tmpFileName, bytes), + /Could not parse path/, + "IOUtils::write only works with absolute paths" + ); + }); + + add_task(async function test_read_relative_path() { + const tmpFileName = "test_ioutils_read_relative_path.tmp"; + + info("Test reading a file at a relative destination"); + await Assert.rejects( + IOUtils.read(tmpFileName), + /Could not parse path/, + "IOUtils::write only works with absolute paths" + ); + }); + + add_task(async function test_lz4() { + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_lz4.tmp"); + + info("Test writing lz4 encoded data"); + const varyingBytes = Uint8Array.of(...new Array(50).keys()); + let bytesWritten = await IOUtils.write(tmpFileName, varyingBytes, { compress: true }); + is(bytesWritten, 64, "Expected to write 64 bytes"); + + info("Test reading lz4 encoded data"); + let readData = await IOUtils.read(tmpFileName, { decompress: true }); + ok(readData.equals(varyingBytes), "IOUtils can write and read back LZ4 encoded data"); + + info("Test writing lz4 compressed data"); + const repeatedBytes = Uint8Array.of(...new Array(50).fill(1)); + bytesWritten = await IOUtils.write(tmpFileName, repeatedBytes, { compress: true }); + is(bytesWritten, 23, "Expected 50 bytes to compress to 23 bytes"); + + info("Test reading lz4 encoded data"); + readData = await IOUtils.read(tmpFileName, { decompress: true }); + ok(readData.equals(repeatedBytes), "IOUtils can write and read back LZ4 compressed data"); + + info("Test writing empty lz4 compressed data") + const empty = new Uint8Array(); + bytesWritten = await IOUtils.write(tmpFileName, empty, { compress: true }); + is(bytesWritten, 12, "Expected to write just the LZ4 header, with a content length of 0"); + + + info("Test reading empty lz4 compressed data") + const readEmpty = await IOUtils.read(tmpFileName, { decompress: true }); + ok(readEmpty.equals(empty), "IOUtils can write and read back empty buffers with LZ4"); + const readEmptyRaw = await IOUtils.read(tmpFileName, { decompress: false }); + is(readEmptyRaw.length, 12, "Expected to read back just the LZ4 header"); + const expectedHeader = Uint8Array.of(109, 111, 122, 76, 122, 52, 48, 0, 0, 0, 0, 0); // "mozLz40\0\0\0\0" + ok(readEmptyRaw.equals(expectedHeader), "Expected to read header with content length of 0"); + + await cleanup(tmpFileName); + }); + + add_task(async function test_lz4_bad_call() { + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_lz4_bad_call.tmp"); + + info("Test decompression with invalid options"); + const varyingBytes = Uint8Array.of(...new Array(50).keys()); + let bytesWritten = await IOUtils.write(tmpFileName, varyingBytes, { compress: true }); + is(bytesWritten, 64, "Expected to write 64 bytes"); + await Assert.rejects( + IOUtils.read(tmpFileName, { maxBytes: 4, decompress: true }), + /The `maxBytes` and `decompress` options are not compatible/, + "IOUtils::read rejects when maxBytes and decompress options are both used" + ); + + await cleanup(tmpFileName) + }); + + add_task(async function test_lz4_failure() { + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_lz4_fail.tmp"); + + info("Test decompression of non-lz4 data"); + const repeatedBytes = Uint8Array.of(...new Array(50).fill(1)); + await IOUtils.write(tmpFileName, repeatedBytes, { compress: false }); + + await Assert.rejects( + IOUtils.read(tmpFileName, { decompress: true }), + (actual) => { + is(actual.constructor, DOMException, + "rejection reason constructor for decompress with bad header"); + is(actual.name, "NotReadableError", + "rejection error name for decompress with bad header"); + ok(/Could not decompress file because it has an invalid LZ4 header \(wrong magic number: .*\)/ + .test(actual.message), + "rejection error message for decompress with bad header. Got " + + actual.message); + return true; + }, + "IOUtils::read fails to decompress LZ4 data with a bad header" + ); + + info("Test decompression of short byte buffer"); + const elevenBytes = Uint8Array.of(...new Array(11).fill(1)); + await IOUtils.write(tmpFileName, elevenBytes, { compress: false }); + + await Assert.rejects( + IOUtils.read(tmpFileName, { decompress: true }), + /Could not decompress file because the buffer is too short/, + "IOUtils::read fails to decompress LZ4 data with missing header" + ); + + info("Test decompression of valid header, but corrupt contents"); + const headerFor10bytes = [109, 111, 122, 76, 122, 52, 48, 0, 10, 0, 0, 0] // "mozlz40\0" + 4 byte length + const badContents = new Array(11).fill(255); // Bad leading byte, followed by uncompressed stream. + const goodHeaderBadContents = Uint8Array.of(...headerFor10bytes, ...badContents); + await IOUtils.write(tmpFileName, goodHeaderBadContents, { compress: false }); + + await Assert.rejects( + IOUtils.read(tmpFileName, { decompress: true }), + /Could not decompress file contents, the file may be corrupt/, + "IOUtils::read fails to read corrupt LZ4 contents with a correct header" + ); + + await cleanup(tmpFileName); + }); + + add_task(async function test_write_directory() { + const fileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_directory.tmp"); + const tmpPath = `${fileName}.tmp`; + const bytes = Uint8Array.of(1, 2, 3, 4); + + await IOUtils.makeDirectory(fileName); + await Assert.rejects( + IOUtils.write(fileName, bytes), + /NotAllowedError: Could not open the file at .+ for writing/); + + await Assert.rejects( + IOUtils.write(fileName, bytes, { tmpPath }), + /NotAllowedError: Could not open the file at .+ for writing/); + + ok(!await IOUtils.exists(PathUtils.join(fileName, PathUtils.filename(tmpPath)))); + }); + + add_task(async function test_read_offset() { + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_read_offset.tmp"); + + const bytes = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + const byteArray = Uint8Array.of(...bytes); + + await IOUtils.write(tmpFileName, byteArray); + + for (const offset of [0, 5]) { + info(`Reading bytes from offset ${offset}`); + + const readBytes = await IOUtils.read(tmpFileName, { offset }); + Assert.deepEqual( + Array.from(readBytes), + bytes.slice(offset), + `should have read bytes from offset ${offset}` + ); + } + + for (const offset of [0, 5]) { + info(`Reading up to 5 bytes from offset ${offset}`); + + const readBytes = await IOUtils.read(tmpFileName, {offset, maxBytes: 5}); + Assert.deepEqual( + Array.from(readBytes), + bytes.slice(offset, offset + 5), + `should have read 5 bytes from offset ${offset}` + ); + } + + { + info(`Reading bytes from offset 10`); + const readBytes = await IOUtils.read(tmpFileName, {offset: 10}); + is(readBytes.length, 0, "should have read 0 bytes"); + } + + { + info(`Reading up to 10 bytes from offset 5`); + const readBytes = await IOUtils.read(tmpFileName, {offset: 5, maxBytes: 10}); + is(readBytes.length, 5, "should have read 5 bytes"); + Assert.deepEqual( + Array.from(readBytes), + bytes.slice(5, 10), + "should have read last 5 bytes" + ); + } + }); + + add_task(async function test_write_appendOrCreate() { + const fileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_appendOrCreate.tmp"); + + await IOUtils.write(fileName, Uint8Array.of(0, 1, 2, 3, 4), { mode: "appendOrCreate" }); + + { + const contents = await IOUtils.read(fileName); + Assert.deepEqual(Array.from(contents), [0, 1, 2, 3, 4], "read bytes should be equal"); + } + + await IOUtils.write(fileName, Uint8Array.of(5, 6, 7, 8, 9), { mode: "appendOrCreate" }); + + { + const contents = await IOUtils.read(fileName); + Assert.deepEqual(Array.from(contents), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "read bytes should be equal after appendOrCreateing"); + } + + await cleanup(fileName); + }); + + add_task(async function test_write_append() { + const fileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_append.tmp"); + + await IOUtils.write(fileName, Uint8Array.of(0, 1, 2, 3, 4)); + + const beforeAppend = await IOUtils.read(fileName); + Assert.deepEqual(Array.from(beforeAppend), [0, 1, 2, 3, 4], "read bytes should be equal"); + + await IOUtils.write(fileName, Uint8Array.of(5, 6, 7, 8, 9), { mode: "append" }); + + const afterAppend = await IOUtils.read(fileName); + Assert.deepEqual(Array.from(afterAppend), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "read bytes should be equal after appending"); + + await cleanup(fileName); + }); + + add_task(async function test_write_append_no_create() { + const fileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_append_no_create.tmp"); + + await Assert.rejects( + IOUtils.write(fileName, Uint8Array.of(5, 6, 7, 8, 9), { mode: "append" }), + /NotFoundError: Could not open the file at .*/ + ); + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_read_write_json.html b/dom/system/tests/ioutils/test_ioutils_read_write_json.html new file mode 100644 index 0000000000..0acb191e1b --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_read_write_json.html @@ -0,0 +1,193 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + const { ObjectUtils } = ChromeUtils.importESModule( + "resource://gre/modules/ObjectUtils.sys.mjs" + ); + + const OBJECT = { + "foo": [ + "bar", + 123, + 456.789, + true, + false, + null, + ], + "bar": { + "baz": {}, + }, + }; + + const ARRAY = [1, 2.3, true, false, null, { "foo": "bar" }]; + + const PRIMITIVES = [123, true, false, "hello, world", null]; + + add_task(async function read_json() { + const filename = PathUtils.join(PathUtils.tempDir, "test_ioutils_read_json.tmp"); + + info("Testing IOUtils.readJSON() with a serialized object..."); + await IOUtils.writeUTF8(filename, JSON.stringify(OBJECT)); + const readObject = await IOUtils.readJSON(filename); + const parsedObject = JSON.parse(await IOUtils.readUTF8(filename)); + ok(ObjectUtils.deepEqual(OBJECT, readObject), "JSON objects should round-trip"); + ok( + ObjectUtils.deepEqual(parsedObject, readObject), + "IOUtils.readJSON() equivalent to JSON.parse() for objects" + ); + + info("Testing IOUtils.readJSON() with a serialized array..."); + await IOUtils.writeUTF8(filename, JSON.stringify(ARRAY)); + const readArray = await IOUtils.readJSON(filename); + const parsedArray = JSON.parse(await IOUtils.readUTF8(filename)); + ok(ObjectUtils.deepEqual(ARRAY, readArray), "JSON arrays should round-trip"); + ok( + ObjectUtils.deepEqual(parsedArray, readArray), + "IOUtils.readJSON() equivalent to JSON.parse(IOUtils.readUTF8()) for arrays" + ); + + info("Testing IOUtils.readJSON() with serialized primitives..."); + for (const primitive of PRIMITIVES) { + await IOUtils.writeUTF8(filename, JSON.stringify(primitive)); + const readPrimitive = await IOUtils.readJSON(filename); + const parsedPrimitive = JSON.parse(await IOUtils.readUTF8(filename)); + ok(primitive === readPrimitive, `JSON primitive ${primitive} should round trip`); + ok( + readPrimitive === parsedPrimitive, + `${readPrimitive} === ${parsedPrimitive} -- IOUtils.readJSON() equivalent to JSON.parse() for primitive` + ); + } + + info("Testing IOUtils.readJSON() with a file that does not exist..."); + const notExistsFilename = PathUtils.join(PathUtils.tempDir, "test_ioutils_read_json_not_exists.tmp"); + ok(!await IOUtils.exists(notExistsFilename), `${notExistsFilename} should not exist`); + await Assert.rejects( + IOUtils.readJSON(notExistsFilename), + /NotFoundError: Could not open the file at/, + "IOUtils::readJSON rejects when file does not exist" + ); + + info("Testing IOUtils.readJSON() with a file that does not contain JSON"); + const invalidFilename = PathUtils.join(PathUtils.tempDir, "test_ioutils_read_json_invalid.tmp"); + await IOUtils.writeUTF8(invalidFilename, ":)"); + + await Assert.rejects( + IOUtils.readJSON(invalidFilename), + /SyntaxError: JSON\.parse/, + "IOUTils::readJSON rejects when the file contains invalid JSON" + ); + + await cleanup(filename, invalidFilename); + }); + + add_task(async function write_json() { + const filename = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_json.tmp"); + + info("Testing IOUtils.writeJSON() with an object..."); + await IOUtils.writeJSON(filename, OBJECT); + const readObject = await IOUtils.readJSON(filename); + const readObjectStr = await IOUtils.readUTF8(filename); + ok(ObjectUtils.deepEqual(OBJECT, readObject), "JSON objects should round-trip"); + ok( + readObjectStr === JSON.stringify(OBJECT), + "IOUtils.writeJSON() eqvuialent to JSON.stringify() for an object" + ); + + info("Testing IOUtils.writeJSON() with an array..."); + await IOUtils.writeJSON(filename, ARRAY); + const readArray = await IOUtils.readJSON(filename); + const readArrayStr = await IOUtils.readUTF8(filename); + ok(ObjectUtils.deepEqual(ARRAY, readArray), "JSON arrays should round-trip"); + ok( + readArrayStr === JSON.stringify(ARRAY), + "IOUtils.writeJSON() equivalent to JSON.stringify() for an array" + ); + + info("Testing IOUtils.writeJSON() with primitives..."); + for (const primitive of PRIMITIVES) { + await IOUtils.writeJSON(filename, primitive); + const readPrimitive = await IOUtils.readJSON(filename); + const readPrimitiveStr = await IOUtils.readUTF8(filename); + ok( + primitive === readPrimitive, + `${primitive} === ${readPrimitive} -- IOUtils.writeJSON() should round trip primitive` + ); + ok( + readPrimitiveStr === JSON.stringify(primitive), + `${readPrimitiveStr} === ${JSON.stringify(primitive)} -- IOUtils.writeJSON() equivalent to JSON.stringify for primitive` + ); + } + + info("Testing IOUtils.writeJSON() with unserializable objects..."); + await Assert.rejects( + IOUtils.writeJSON(filename, window), + /TypeError: cyclic object value/, + "IOUtils.writeJSON() cannot write cyclic objects" + ); + + await cleanup(filename); + }); + + add_task(async function test_append_json() { + const filename = PathUtils.join(PathUtils.tempDir, "test_ioutils_append_json.tmp"); + + await IOUtils.writeJSON(filename, OBJECT); + + await Assert.rejects( + IOUtils.writeJSON(filename, OBJECT, {mode: "append"}), + /NotSupportedError: IOUtils.writeJSON does not support appending to files/, + "IOUtils.writeJSON() cannot append" + ); + + await cleanup(filename); + }); + + add_task(async function test_read_json_bom() { + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_read_json_bom.tmp"); + const raw = `\uFEFF${JSON.stringify({hello: "world"})}`; + await IOUtils.writeUTF8(tmpFileName, raw); + + ok( + ObjectUtils.deepEqual( + await IOUtils.readJSON(tmpFileName), + { hello: "world" }, + ), + "IOUtils.readJSON should skip BOM" + ); + + await IOUtils.writeUTF8(tmpFileName, raw, { compress: true }); + + ok( + ObjectUtils.deepEqual( + await IOUtils.readJSON(tmpFileName, { decompress: true }), + { hello: "world" }, + ), + "IOUtils.readJSON should skip BOM for compressed files" + ); + + await cleanup(tmpFileName); + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html b/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html new file mode 100644 index 0000000000..cdea016732 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html @@ -0,0 +1,384 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + // This is an impossible sequence of bytes in an UTF-8 encoded file. + // See section 3.5.3 of this text: + // https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt + const invalidUTF8 = Uint8Array.of(0xfe, 0xfe, 0xff, 0xff); + + add_task(async function test_read_utf8_failure() { + info("Test attempt to read non-existent file (UTF8)"); + const doesNotExist = PathUtils.join(PathUtils.tempDir, "does_not_exist.tmp"); + await Assert.rejects( + IOUtils.readUTF8(doesNotExist), + /Could not open the file at .*/, + "IOUtils::readUTF8 rejects when file does not exist" + ); + + info("Test attempt to read invalid UTF-8"); + const invalidUTF8File = PathUtils.join(PathUtils.tempDir, "invalid_utf8.tmp"); + + // Deliberately write the invalid byte sequence to file. + await IOUtils.write(invalidUTF8File, invalidUTF8); + + await Assert.rejects( + IOUtils.readUTF8(invalidUTF8File), + /Could not read file\(.*\) because it is not UTF-8 encoded/, + "IOUtils::readUTF8 will reject when reading a file that is not valid UTF-8" + ); + + await cleanup(invalidUTF8File); + }); + + add_task(async function test_write_utf8_no_overwrite() { + // Make a new file, and try to write to it with overwrites disabled. + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_utf8_overwrite.tmp"); + const untouchableContents = "Can't touch this!\n"; + await IOUtils.writeUTF8(tmpFileName, untouchableContents); + + const newContents = "Nah nah nah!\n"; + await Assert.rejects( + IOUtils.writeUTF8(tmpFileName, newContents, { + mode: "create", + }), + /Refusing to overwrite the file at */, + "IOUtils::writeUTF8 rejects writing to existing file if overwrites are disabled" + ); + ok( + await fileHasTextContents(tmpFileName, untouchableContents), + "IOUtils::writeUTF8 doesn't change target file when overwrite is refused" + ); + + const bytesWritten = await IOUtils.writeUTF8( + tmpFileName, + newContents, + { mode: "overwrite" } + ); + is( + bytesWritten, + newContents.length, + "IOUtils::writeUTF8 can overwrite files if specified" + ); + ok( + await fileHasTextContents(tmpFileName, newContents), + "IOUtils::writeUTF8 overwrites with the expected contents" + ); + + await cleanup(tmpFileName); + }); + + add_task(async function test_write_with_backup() { + info("Test backup file option with non-existing file"); + let fileContents = "Original file contents"; + let destFileName = PathUtils.join(PathUtils.tempDir, "test_write_utf8_with_backup_option.tmp"); + let backupFileName = destFileName + ".backup"; + let bytesWritten = + await IOUtils.writeUTF8(destFileName, fileContents, { + backupFile: backupFileName, + }); + ok( + await fileHasTextContents(destFileName, "Original file contents"), + "IOUtils::writeUTF8 creates a new file with the correct contents" + ); + ok( + !await fileExists(backupFileName), + "IOUtils::writeUTF8 does not create a backup if the target file does not exist" + ); + is( + bytesWritten, + fileContents.length, + "IOUtils::write correctly writes to a new file without performing a backup" + ); + + info("Test backup file option with existing destination"); + let newFileContents = "New file contents"; + ok(await fileExists(destFileName), `Expected ${destFileName} to exist`); + bytesWritten = + await IOUtils.writeUTF8(destFileName, newFileContents, { + backupFile: backupFileName, + }); + ok( + await fileHasTextContents(backupFileName, "Original file contents"), + "IOUtils::writeUTF8 can backup an existing file before writing" + ); + ok( + await fileHasTextContents(destFileName, "New file contents"), + "IOUtils::writeUTF8 can create the target with the correct contents" + ); + is( + bytesWritten, + newFileContents.length, + "IOUtils::writeUTF8 correctly writes to the target after taking a backup" + ); + + await cleanup(destFileName, backupFileName); + }); + + add_task(async function test_write_with_backup_and_tmp() { + info("Test backup with tmp and backup file options, non-existing destination"); + let fileContents = "Original file contents"; + let destFileName = PathUtils.join(PathUtils.tempDir, "test_write_utf8_with_backup_and_tmp_options.tmp"); + let backupFileName = destFileName + ".backup"; + let tmpFileName = PathUtils.join(PathUtils.tempDir, "temp_file.tmp"); + let bytesWritten = + await IOUtils.writeUTF8(destFileName, fileContents, { + backupFile: backupFileName, + tmpPath: tmpFileName, + }); + ok(!await fileExists(tmpFileName), "IOUtils::writeUTF8 cleans up the tmpFile"); + ok( + !await fileExists(backupFileName), + "IOUtils::writeUTF8 does not create a backup if the target file does not exist" + ); + ok( + await fileHasTextContents(destFileName, "Original file contents"), + "IOUtils::writeUTF8 can write to the destination when a temporary file is used" + ); + is( + bytesWritten, + fileContents.length, + "IOUtils::writeUTF8 can copy tmp file to destination without performing a backup" + ); + + info("Test backup with tmp and backup file options, existing destination"); + let newFileContents = "New file contents"; + bytesWritten = + await IOUtils.writeUTF8(destFileName, newFileContents, { + backupFile: backupFileName, + tmpPath: tmpFileName, + }); + + ok(!await fileExists(tmpFileName), "IOUtils::writeUTF8 cleans up the tmpFile"); + ok( + await fileHasTextContents(backupFileName, "Original file contents"), + "IOUtils::writeUTF8 can create a backup if the target file exists" + ); + ok( + await fileHasTextContents(destFileName, "New file contents"), + "IOUtils::writeUTF8 can write to the destination when a temporary file is used" + ); + is( + bytesWritten, + newFileContents.length, + "IOUtils::writeUTF8 can move tmp file to destination after performing a backup" + ); + + await cleanup(destFileName, backupFileName); + }); + + add_task(async function test_empty_read_and_write_utf8() { + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_empty_utf8.tmp"); + const emptyString = "" + const bytesWritten = await IOUtils.writeUTF8( + tmpFileName, + emptyString + ); + is(bytesWritten, 0, "IOUtils::writeUTF8 can create an empty file"); + + const nothing = await IOUtils.readUTF8(tmpFileName); + is(nothing.length, 0, "IOUtils::readUTF8 can read empty files"); + + await cleanup(tmpFileName); + }); + + add_task(async function test_full_read_and_write_utf8() { + // Write a file. + info("Test writing emoji file"); + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_emoji.tmp"); + + // Make sure non-ASCII text is supported for writing and reading back. + // For fun, a sampling of space-separated emoji characters from different + // Unicode versions, including multi-byte glyphs that are rendered using + // ZWJ sequences. + const emoji = "☕️ ⚧️ 😀 🖖🏿 🤠 🏳️🌈 🥠 🏴☠️ 🪐"; + const expectedBytes = 71; + const bytesWritten = await IOUtils.writeUTF8(tmpFileName, emoji); + is( + bytesWritten, + expectedBytes, + "IOUtils::writeUTF8 can write emoji to file" + ); + + // Read it back. + info("Test reading emoji from file"); + let fileContents = await IOUtils.readUTF8(tmpFileName); + ok( + emoji == fileContents && + emoji.length == fileContents.length, + "IOUtils::readUTF8 can read back entire file" + ); + + // Clean up. + await cleanup(tmpFileName); + }); + + add_task(async function test_write_utf8_relative_path() { + const tmpFileName = "test_ioutils_write_utf8_relative_path.tmp"; + + info("Test writing a file at a relative destination"); + await Assert.rejects( + IOUtils.writeUTF8(tmpFileName, "foo"), + /Could not parse path/, + "IOUtils::writeUTF8 only works with absolute paths" + ); + }); + + add_task(async function test_read_utf8_relative_path() { + const tmpFileName = "test_ioutils_read_utf8_relative_path.tmp"; + + info("Test reading a file at a relative destination"); + await Assert.rejects( + IOUtils.readUTF8(tmpFileName), + /Could not parse path/, + "IOUtils::readUTF8 only works with absolute paths" + ); + }); + + + add_task(async function test_utf8_lz4() { + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_utf8_lz4.tmp"); + + info("Test writing lz4 encoded UTF-8 string"); + const emoji = "☕️ ⚧️ 😀 🖖🏿 🤠 🏳️🌈 🥠 🏴☠️ 🪐"; + let bytesWritten = await IOUtils.writeUTF8(tmpFileName, emoji, { compress: true }); + is(bytesWritten, 83, "Expected to write 64 bytes"); + + info("Test reading lz4 encoded UTF-8 string"); + let readData = await IOUtils.readUTF8(tmpFileName, { decompress: true }); + is(readData, emoji, "IOUtils can write and read back UTF-8 LZ4 encoded data"); + + info("Test writing lz4 compressed UTF-8 string"); + const lotsOfCoffee = new Array(24).fill("☕️").join(""); // ☕️ is 3 bytes in UTF-8: \0xe2 \0x98 \0x95 + bytesWritten = await IOUtils.writeUTF8(tmpFileName, lotsOfCoffee, { compress: true }); + console.log(bytesWritten); + is(bytesWritten, 28, "Expected 72 bytes to compress to 28 bytes"); + + info("Test reading lz4 encoded UTF-8 string"); + readData = await IOUtils.readUTF8(tmpFileName, { decompress: true }); + is(readData, lotsOfCoffee, "IOUtils can write and read back UTF-8 LZ4 compressed data"); + + info("Test writing empty lz4 compressed UTF-8 string") + const empty = ""; + bytesWritten = await IOUtils.writeUTF8(tmpFileName, empty, { compress: true }); + is(bytesWritten, 12, "Expected to write just the LZ4 header"); + + info("Test reading empty lz4 compressed UTF-8 string") + const readEmpty = await IOUtils.readUTF8(tmpFileName, { decompress: true }); + is(readEmpty, empty, "IOUtils can write and read back empty buffers with LZ4"); + const readEmptyRaw = await IOUtils.readUTF8(tmpFileName, { decompress: false }); + is(readEmptyRaw.length, 12, "Expected to read back just the LZ4 header"); + + await cleanup(tmpFileName); + }); + + add_task(async function test_utf8_lz4_bad_call() { + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_utf8_lz4_bad_call.tmp"); + + info("readUTF8 ignores the maxBytes option if provided"); + const emoji = "☕️ ⚧️ 😀 🖖🏿 🤠 🏳️🌈 🥠 🏴☠️ 🪐"; + let bytesWritten = await IOUtils.writeUTF8(tmpFileName, emoji, { compress: true }); + is(bytesWritten, 83, "Expected to write 83 bytes"); + + let readData = await IOUtils.readUTF8(tmpFileName, { maxBytes: 4, decompress: true }); + is(readData, emoji, "IOUtils can write and read back UTF-8 LZ4 encoded data"); + + await cleanup(tmpFileName) + }); + + add_task(async function test_utf8_lz4_failure() { + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_utf8_lz4_fail.tmp"); + + info("Test decompression of non-lz4 UTF-8 string"); + const repeatedBytes = Uint8Array.of(...new Array(50).fill(1)); + await IOUtils.write(tmpFileName, repeatedBytes, { compress: false }); + + await Assert.rejects( + IOUtils.readUTF8(tmpFileName, { decompress: true }), + /Could not decompress file because it has an invalid LZ4 header \(wrong magic number: .*\)/, + "IOUtils::readUTF8 fails to decompress LZ4 data with a bad header" + ); + + info("Test UTF-8 decompression of short byte buffer"); + const elevenBytes = Uint8Array.of(...new Array(11).fill(1)); + await IOUtils.write(tmpFileName, elevenBytes, { compress: false }); + + await Assert.rejects( + IOUtils.readUTF8(tmpFileName, { decompress: true }), + /Could not decompress file because the buffer is too short/, + "IOUtils::readUTF8 fails to decompress LZ4 data with missing header" + ); + + info("Test UTF-8 decompression of valid header, but corrupt contents"); + const headerFor10bytes = [109, 111, 122, 76, 122, 52, 48, 0, 10, 0, 0, 0] // "mozlz40\0" + 4 byte length + const badContents = new Array(11).fill(255); // Bad leading byte, followed by uncompressed stream. + const goodHeaderBadContents = Uint8Array.of(...headerFor10bytes, ...badContents); + await IOUtils.write(tmpFileName, goodHeaderBadContents, { compress: false }); + + await Assert.rejects( + IOUtils.readUTF8(tmpFileName, { decompress: true }), + /Could not decompress file contents, the file may be corrupt/, + "IOUtils::readUTF8 fails to read corrupt LZ4 contents with a correct header" + ); + + info("Testing decompression of an empty file (no header)"); + { + const n = await IOUtils.writeUTF8(tmpFileName, ""); + ok(n === 0, "Overwrote with empty file"); + } + await Assert.rejects( + IOUtils.readUTF8(tmpFileName, { decompress: true }), + /Could not decompress file because the buffer is too short/, + "IOUtils::readUTF8 fails to decompress empty files" + ); + + await cleanup(tmpFileName); + }); + + add_task(async function test_skipBOM() { + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_readutf8_bom.tmp"); + + const raw = `\uFEFFstring`; + + await IOUtils.writeUTF8(tmpFileName, raw); + + is( + await IOUtils.readUTF8(tmpFileName), + "string", + "IOUtils.readUTF8 should skip BOM by default" + ); + + await IOUtils.writeUTF8(tmpFileName, raw, { compress: true }); + + is( + await IOUtils.readUTF8(tmpFileName, { decompress: true }), + "string", + "IOUtils.readUTF8 should skip BOM by default for compressed files" + ); + + await cleanup(tmpFileName); + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_remove.html b/dom/system/tests/ioutils/test_ioutils_remove.html new file mode 100644 index 0000000000..f368fc09d3 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_remove.html @@ -0,0 +1,118 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + add_task(async function test_create_and_remove_file() { + info("Test creating and removing a single file"); + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_create_and_remove.tmp"); + await IOUtils.write(tmpFileName, new Uint8Array(0)); + ok(await fileExists(tmpFileName), `Expected file ${tmpFileName} to exist`); + + await IOUtils.remove(tmpFileName); + ok(!await fileExists(tmpFileName), "IOUtils::remove can remove files"); + + info("Test creating and removing an empty directory"); + const tempDirName = PathUtils.join(PathUtils.tempDir, "test_ioutils_create_and_remove.tmp.d"); + await IOUtils.makeDirectory(tempDirName); + ok(await dirExists(tempDirName), `Expected directory ${tempDirName} to exist`); + + await IOUtils.remove(tempDirName); + ok(!await dirExists(tempDirName), "IOUtils::remove can remove empty directories"); + }); + + add_task(async function test_remove_non_existing() { + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutil_remove_non_existing.tmp"); + ok(!await fileExists(tmpFileName), `Expected file ${tmpFileName} not to exist`); + + await IOUtils.remove(tmpFileName, { ignoreAbsent: true }); + ok(!await fileExists(tmpFileName), "IOUtils::remove can ignore missing files without error"); + + await Assert.rejects( + IOUtils.remove(tmpFileName, { ignoreAbsent: false }), + /Could not remove the file at .* because it does not exist/, + "IOUtils::remove can throw an error when target file is missing" + ); + ok(!await fileExists(tmpFileName), `Expected file ${tmpFileName} not to exist`); + }); + + add_task(async function test_remove_recursive() { + const tmpParentDir = PathUtils.join(PathUtils.tempDir, "test_ioutils_remove.tmp.d"); + const tmpChildDir = PathUtils.join(tmpParentDir, "child.tmp.d"); + const tmpTopLevelFileName = PathUtils.join(tmpParentDir, "top.tmp"); + const tmpNestedFileName = PathUtils.join(tmpChildDir, "nested.tmp"); + await createDir(tmpChildDir); + await createFile(tmpTopLevelFileName, ""); + await createFile(tmpNestedFileName, ""); + + ok( + await fileExists(tmpTopLevelFileName), + `Expected file ${tmpTopLevelFileName} to exist` + ); + ok( + await fileExists(tmpNestedFileName), + `Expected file ${tmpNestedFileName} to exist` + ); + + await Assert.rejects( + IOUtils.remove(tmpParentDir, { recursive: false }), + /Could not remove the non-empty directory at .*/, + "IOUtils::remove fails if non-recursively removing directory with contents" + ); + + await IOUtils.remove(tmpParentDir, { recursive: true }); + ok( + !await dirExists(tmpParentDir), + "IOUtils::remove can recursively remove a directory" + ); + }); + + if (Services.appinfo.OS === "WINNT") { + add_task(async function test_remove_retry_readonly() { + + const tmpDir = PathUtils.join(PathUtils.tempDir, "test_ioutils_remove_retry_readonly.tmp.d"); + const path = PathUtils.join(tmpDir, "file.txt"); + + await createDir(tmpDir); + await createFile(path, ""); + + await IOUtils.setWindowsAttributes(path, { readOnly: true }); + + await Assert.rejects( + IOUtils.remove(path), + /NotAllowedError/, + "Cannot remove a readonly file by default" + ); + + Assert.ok(await fileExists(path), "File should still exist"); + + await IOUtils.remove(path, { retryReadonly: true }); + + Assert.ok(!await fileExists(path), "File should not exist"); + + await IOUtils.remove(tmpDir); + }); + } + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_set_permissions.html b/dom/system/tests/ioutils/test_ioutils_set_permissions.html new file mode 100644 index 0000000000..36f7dab72a --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_set_permissions.html @@ -0,0 +1,84 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + add_task(async function test_setPermissions() { + const tempFile = PathUtils.join(PathUtils.tempDir, "setPermissions.tmp"); + + await IOUtils.writeUTF8(tempFile, ""); + await IOUtils.setPermissions(tempFile, 0o421); + + let stat = await IOUtils.stat(tempFile); + + if (Services.appinfo.OS === "WINNT") { + // setPermissions ignores the x bit on Windows. + is(stat.permissions, 0o666, "Permissions munged on Windows"); + } else { + let umask = Services.sysinfo.getProperty("umask"); + is(stat.permissions, 0o421 & ~umask, "Permissions match"); + } + + await IOUtils.setPermissions(tempFile, 0o400); + stat = await IOUtils.stat(tempFile); + + if (Services.appinfo.OS === "WINNT") { + is(stat.permissions, 0o444, "Permissions munged on Windows"); + + // We need to make the file writable to delete it on Windows. + await IOUtils.setPermissions(tempFile, 0o600); + } else { + is(stat.permissions, 0o400, "Permissions match"); + } + + await cleanup(tempFile); + }); + + add_task(async function test_setPermissionsWithoutHonoringUmask() { + const tempFile = PathUtils.join(PathUtils.tempDir, "setPermissions.tmp"); + + await IOUtils.writeUTF8(tempFile, ""); + await IOUtils.setPermissions(tempFile, 0o421, false); + + let stat = await IOUtils.stat(tempFile); + + if (Services.appinfo.OS === "WINNT") { + // setPermissions ignores the x bit on Windows. + is(stat.permissions, 0o666, "Permissions munged on Windows"); + } else { + is(stat.permissions, 0o421, "Permissions match"); + } + + await IOUtils.setPermissions(tempFile, 0o400); + stat = await IOUtils.stat(tempFile); + + if (Services.appinfo.OS === "WINNT") { + is(stat.permissions, 0o444, "Permissions munged on Windows"); + + // We need to make the file writable to delete it on Windows. + await IOUtils.setPermissions(tempFile, 0o600); + } else { + is(stat.permissions, 0o400, "Permissions match"); + } + + await cleanup(tempFile); + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_stat_set_modification_time.html b/dom/system/tests/ioutils/test_ioutils_stat_set_modification_time.html new file mode 100644 index 0000000000..e508817a41 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_stat_set_modification_time.html @@ -0,0 +1,242 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + add_task(async function test_stat() { + info("Test attempt to stat a regular empty file"); + + const emptyFileName = PathUtils.join(PathUtils.tempDir, "test_stat_empty.tmp"); + await createFile(emptyFileName); + + const emptyFileInfo = await IOUtils.stat(emptyFileName); + is(emptyFileInfo.size, 0, "IOUtils::stat can get correct (empty) file size"); + is(emptyFileInfo.path, emptyFileName, "IOUtils::stat result contains the path"); + is(emptyFileInfo.type, "regular", "IOUtils::stat can stat regular (empty) files"); + Assert.less( + (emptyFileInfo.lastModified - new Date().valueOf()), + 1000, // Allow for 1 second deviation in case of slow tests. + "IOUtils::stat can get the last modification date for a regular file" + ); + + info("Test attempt to stat a regular binary file"); + const tempFileName = PathUtils.join(PathUtils.tempDir, "test_stat_binary.tmp"); + const bytes = Uint8Array.of(...new Array(50).keys()); + await createFile(tempFileName, bytes); + + const fileInfo = await IOUtils.stat(tempFileName); + is(fileInfo.size, 50, "IOUtils::stat can get correct file size"); + is(fileInfo.path, tempFileName, "IOUtils::stat result contains the path"); + is(fileInfo.type, "regular", "IOUtils::stat can stat regular files"); + Assert.less( + (fileInfo.lastModified - new Date().valueOf()), + 1000, // Allow for 1 second deviation in case of slow tests. + "IOUtils::stat can get the last modification date for a regular file" + ); + + info("Test attempt to stat a directory"); + const tempDirName = PathUtils.join(PathUtils.tempDir, "test_stat_dir.tmp.d"); + await IOUtils.makeDirectory(tempDirName); + + const dirInfo = await IOUtils.stat(tempDirName); + is(dirInfo.size, -1, "IOUtils::stat reports -1 size for directories") + is(fileInfo.path, tempFileName, "IOUtils::stat result contains the path"); + is(fileInfo.type, "regular", "IOUtils::stat can stat directories"); + Assert.less( + (fileInfo.lastModified - new Date().valueOf()), + 1000, // Allow for 1 second deviation in case of slow tests. + "IOUtils::stat can get the last modification date for a regular file" + ); + Assert.less( + (fileInfo.lastAccessed - new Date().valueOf()), + 1000, + "IOUtils::stat can get the last access date for a regular file" + ); + + await cleanup(emptyFileName, tempFileName, tempDirName) + }); + + add_task(async function test_stat_failures() { + info("Test attempt to stat a non-existing file"); + + const notExistsFile = PathUtils.join(PathUtils.tempDir, "test_stat_not_exists.tmp"); + + await Assert.rejects( + IOUtils.stat(notExistsFile), + /Could not stat file\(.*\) because it does not exist/, + "IOUtils::stat throws if the target file does not exist" + ); + }); + + add_task(async function test_setModificationTime_and_stat() { + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_setModificationTime_and_stat.tmp"); + { + info("Test attempt to setModificationTime a file"); + await createFile(tmpFileName); + + const oldFileInfo = await IOUtils.stat(tmpFileName); + await sleep(500); + + // Now update the time stamp. + const stamp = await IOUtils.setModificationTime(tmpFileName); + const newFileInfo = await IOUtils.stat(tmpFileName); + + ok( + newFileInfo.lastModified > oldFileInfo.lastModified, + "IOUtils::setModificationTime can update the lastModified time stamp on the file system" + ); + is( + stamp, + newFileInfo.lastModified, + "IOUtils::setModificationTime returns the updated time stamp." + ); + is( + newFileInfo.lastAccessed, + oldFileInfo.lastAccessed, + "IOUtils::setModificationTime does not change lastAccessed" + ); + + await sleep(500); + + const newerStamp = await IOUtils.setAccessTime(tmpFileName); + const newerFileInfo = await IOUtils.stat(tmpFileName); + + ok( + newerFileInfo.lastAccessed > newFileInfo.lastAccessed, + "IOUtils::setAccessTime can update the lastAccessed time stamp on the file system" + ); + is( + newerStamp, + newerFileInfo.lastAccessed, + "IOUtils::setAccessTime returns the updated time stamp." + ); + is( + newerFileInfo.lastModified, + newFileInfo.lastModified, + "IOUtils::setAccessTime does not change lastModified" + ); + } + + const tmpDirName = PathUtils.join(PathUtils.tempDir, "test_setModificationTime_and_stat.tmp.d"); + { + info("Test attempt to setModificationTime a directory"); + await createDir(tmpDirName); + + const oldFileInfo = await IOUtils.stat(tmpDirName); + await sleep(500); + + const stamp = await IOUtils.setModificationTime(tmpDirName); + const newFileInfo = await IOUtils.stat(tmpDirName); + + ok( + newFileInfo.lastModified > oldFileInfo.lastModified, + "IOUtils::setModificationTime can update the lastModified time stamp on a directory" + ); + is( + stamp, + newFileInfo.lastModified, + "IOUtils::setModificationTime returns the updated time stamp on a directory" + ); + } + + await cleanup(tmpFileName, tmpDirName); + }); + + add_task(async function test_setModificationTime_custom_mod_time() { + const tempFileName = PathUtils.join(PathUtils.tempDir, "test_setModificationTime_custom_mod_time.tmp"); + await createFile(tempFileName); + const originalInfo = await IOUtils.stat(tempFileName); + const now = originalInfo.lastModified; + + const oneMinute = 60 * 1000; // milliseconds + + info("Test attempt to set modification time to the future"); + const future = now + oneMinute; + let newModTime = await IOUtils.setModificationTime(tempFileName, future); + const futureInfo = await IOUtils.stat(tempFileName); + Assert.less(originalInfo.lastModified, futureInfo.lastModified, "IOUtils::setModificationTime can set a future modification time for the file"); + + is(newModTime, futureInfo.lastModified, "IOUtils::setModificationTime returns the updated time stamp"); + is(newModTime, future, "IOUtils::setModificationTime return value matches the argument value exactly"); + + info("Test attempt to set modification time to the past"); + const past = now - 2 * oneMinute; + newModTime = await IOUtils.setModificationTime(tempFileName, past); + const pastInfo = await IOUtils.stat(tempFileName); + Assert.greater(originalInfo.lastModified, pastInfo.lastModified, "IOUtils::setModificationTime can set a past modification time for the file"); + + is(newModTime, pastInfo.lastModified, "IOUtils::setModificationTime returns the updated time stamp"); + is(newModTime, past, "IOUtils::setModificationTime return value matches the argument value exactly"); + + await cleanup(tempFileName); + }); + + add_task(async function test_stat_btime() { + if (["Darwin", "WINNT"].includes(Services.appinfo.OS)) { + const tempFileName = PathUtils.join(PathUtils.tempDir, "test_stat_btime.tmp"); + await createFile(tempFileName); + const originalInfo = await IOUtils.stat(tempFileName); + + const future = originalInfo.lastModified + 6000; + await IOUtils.setModificationTime(tempFileName, future); + const futureInfo = await IOUtils.stat(tempFileName); + + ok(originalInfo.hasOwnProperty("creationTime"), "originalInfo has creationTime field"); + ok(originalInfo.creationTime !== undefined && originalInfo.creationTime !== null, "originalInfo has non-null creationTime"); + + ok(futureInfo.hasOwnProperty("creationTime"), "futureInfo has creationTime field"); + ok(futureInfo.creationTime !== undefined && futureInfo.creationTime !== null, "futureInfo has non-null creationTime"); + + is(originalInfo.creationTime, futureInfo.creationTime, "creationTime matches"); + + await cleanup(tempFileName); + } else { + ok(true, `skipping test_stat_btime() on unsupported platform ${Services.appinfo.OS}`); + } + }); + + add_task(async function test_setModificationTime_failures() { + info("Test attempt to setModificationTime a non-existing file"); + const notExistsFile = PathUtils.join(PathUtils.tempDir, "test_setModificationTime_not_exists.tmp"); + + await Assert.rejects( + IOUtils.setModificationTime(notExistsFile), + /Could not set modification time of file\(.*\) because it does not exist/, + "IOUtils::setModificationTime throws if the target file does not exist" + ); + + info("Test attempt to set modification time to Epoch"); + const tempFileName = PathUtils.join(PathUtils.tempDir, "test_setModificationTime_epoch.tmp"); + await createFile(tempFileName); + + await Assert.rejects( + IOUtils.setModificationTime(tempFileName, 0), + /Refusing to set the modification time of file\(.*\) to 0/, + "IOUtils::setModificationTime cannot set the file modification time to Epoch" + ); + + await cleanup(tempFileName); + }); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_windows_file_attributes.html b/dom/system/tests/ioutils/test_ioutils_windows_file_attributes.html new file mode 100644 index 0000000000..a5b72bd078 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_windows_file_attributes.html @@ -0,0 +1,137 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + add_task(async function getSetWindowsAttributes() { + const tmpDir = PathUtils.join(PathUtils.tempDir, "ioutils-windows-attributes.tmp.d"); + await createDir(tmpDir); + ok(await dirExists(tmpDir), `Expected ${tmpDir} to be a directory`); + + const filePath = PathUtils.join(tmpDir, "file.tmp"); + await createFile(filePath); + ok(await fileExists(filePath), `Expected ${filePath} to exist`); + + { + info("Getting attributes for newly created file."); + const attrs = await IOUtils.getWindowsAttributes(filePath); + + ok(attrs.readOnly === false, `Newly created file ${filePath} is not a read-only file`); + ok(attrs.hidden === false, `Newly created file ${filePath} is not a hidden file`); + ok(attrs.system === false, `Newly created file ${filePath} is not a system file`); + } + + { + info("Setting read-only on an existing file."); + await IOUtils.setWindowsAttributes(filePath, { readOnly: true }) + const attrs = await IOUtils.getWindowsAttributes(filePath); + + ok(attrs.readOnly === true, `Updated file ${filePath} is a read-only file`); + ok(attrs.hidden === false, `Updated file ${filePath} is not a hidden file`); + ok(attrs.system === false, `Updated file ${filePath} is not a system file`); + } + + info("Attempting to write to a read-only file."); + + await Assert.rejects( + IOUtils.writeUTF8(filePath, "hello, world"), + /NotAllowedError: Could not open the file at .+ for writing/, + "IOUtils::writeUTF8 on a read-only file fails." + ); + + { + info("Setting hidden on an existing file."); + await IOUtils.setWindowsAttributes(filePath, { hidden: true }) + const attrs = await IOUtils.getWindowsAttributes(filePath); + + ok(attrs.readOnly === true, `Updated file ${filePath} is still a read-only file`); + ok(attrs.hidden === true, `Updated file ${filePath} is a hidden file`); + ok(attrs.system === false, `Updated file ${filePath} is not a system file`); + } + + { + info("Setting system on an existing file."); + await IOUtils.setWindowsAttributes(filePath, { system: true }) + const attrs = await IOUtils.getWindowsAttributes(filePath); + + ok(attrs.readOnly === true, `Updated file ${filePath} is still a read-only file`); + ok(attrs.hidden === true, `Updated file ${filePath} is still a hidden file`); + ok(attrs.system === true, `Updated file ${filePath} is a system file`); + } + + { + info("Clearing all Windows attributes on an existing file."); + await IOUtils.setWindowsAttributes(filePath, { readOnly: false, hidden: false, system: false }); + const attrs = await IOUtils.getWindowsAttributes(filePath); + + ok(attrs.readOnly === false, `Updated file ${filePath} is not a read-only file`); + ok(attrs.hidden === false, `Updated file ${filePath} is not a hidden file`); + ok(attrs.system === false, `Updated file ${filePath} is not a system file`); + } + + { + info("Setting all Windows attributes on an existing file."); + await IOUtils.setWindowsAttributes(filePath, { readOnly: true, hidden: true, system: true }); + const attrs = await IOUtils.getWindowsAttributes(filePath); + + ok(attrs.readOnly === true, `Updated file ${filePath} is a read-only file`); + ok(attrs.hidden === true, `Updated file ${filePath} is a hidden file`); + ok(attrs.system === true, `Updated file ${filePath} is a system file`); + } + + { + info("Clearing read-only on an existing file."); + await IOUtils.setWindowsAttributes(filePath, { readOnly: false }); + const attrs = await IOUtils.getWindowsAttributes(filePath); + + ok(attrs.readOnly === false, `Updated file ${filePath} is no longer a read-only file`); + ok(attrs.hidden === true, `Updated file ${filePath} is still a hidden file`); + ok(attrs.system === true, `Updated file ${filePath} is still a system file`); + } + + { + info("Clearing hidden on an existing file."); + await IOUtils.setWindowsAttributes(filePath, { hidden: false }); + const attrs = await IOUtils.getWindowsAttributes(filePath); + + ok(attrs.readOnly === false, `Updated file ${filePath} is still not a read-only file`); + ok(attrs.hidden === false, `Updated file ${filePath} is no longer a hidden file`); + ok(attrs.system === true, `Updated file ${filePath} is still a system file`); + } + + { + info("Clearing system on an existing file."); + await IOUtils.setWindowsAttributes(filePath, { system: false }); + const attrs = await IOUtils.getWindowsAttributes(filePath); + + ok(attrs.readOnly === false, `Updated file ${filePath} is still not a read-only file`); + ok(attrs.hidden === false, `Updated file ${filePath} is sitll not a hidden file`); + ok(attrs.system === false, `Updated file ${filePath} is no longer a system file`); + } + + await cleanup(tmpDir); + }); + + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> + +</html> diff --git a/dom/system/tests/ioutils/test_ioutils_worker.xhtml b/dom/system/tests/ioutils/test_ioutils_worker.xhtml new file mode 100644 index 0000000000..df67d48676 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_worker.xhtml @@ -0,0 +1,40 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Testing IOUtils on a chrome worker thread" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onload="test();"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/WorkerHandler.js"/> + + <script type="application/javascript"> + <![CDATA[ + + // Test IOUtils in a chrome worker. + function test() { + // finish() will be called in the worker. + SimpleTest.waitForExplicitFinish(); + info("test_ioutils_worker.xhtml: Starting test"); + + const worker = new ChromeWorker("file_ioutils_worker.js"); + info("test_ioutils_worker.xhtml: Chrome worker created"); + + // Set up the worker with testing facilities, and start it. + listenForTests(worker, { verbose: false }); + worker.postMessage(0); + info("test_ioutils_worker.xhtml: Test in progress"); + }; + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result" /> +</window> diff --git a/dom/system/tests/mochitest.toml b/dom/system/tests/mochitest.toml new file mode 100644 index 0000000000..b607853077 --- /dev/null +++ b/dom/system/tests/mochitest.toml @@ -0,0 +1,11 @@ +[DEFAULT] +tags = "condprof" +scheme = "https" + +support-files = ["file_bug1197901.html"] + +["test_bug1197901.html"] +skip-if = [ + "os == 'android'", + "condprof", #: "Only focused window should get the sensor events" +] diff --git a/dom/system/tests/pathutils_worker.js b/dom/system/tests/pathutils_worker.js new file mode 100644 index 0000000000..9e4742b764 --- /dev/null +++ b/dom/system/tests/pathutils_worker.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env worker */ + +"use strict"; + +/* import-globals-from /testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js */ +importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js"); + +self.onmessage = async function (message) { + let expected = message.data; + info("ON message"); + info(JSON.stringify(expected)); + const profileDir = await PathUtils.getProfileDir(); + is( + profileDir, + expected.profileDir, + "PathUtils.profileDir() in a worker should match PathUtils.profileDir on main thread" + ); + + const localProfileDir = await PathUtils.getLocalProfileDir(); + is( + localProfileDir, + expected.localProfileDir, + "PathUtils.getLocalProfileDir() in a worker should match PathUtils.localProfileDir on main thread" + ); + + const tempDir = await PathUtils.getTempDir(); + is( + tempDir, + expected.tempDir, + "PathUtils.getTempDir() in a worker should match PathUtils.tempDir on main thread" + ); + + finish(); +}; diff --git a/dom/system/tests/test_bug1197901.html b/dom/system/tests/test_bug1197901.html new file mode 100644 index 0000000000..7e1866ffa3 --- /dev/null +++ b/dom/system/tests/test_bug1197901.html @@ -0,0 +1,96 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1197901 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1197901</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1197901 **/ + SimpleTest.requestFlakyTimeout("requestFlakyTimeout is silly"); + SimpleTest.waitForExplicitFinish(); + window.onload = function() { + SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({"set": [["device.sensors.test.events", true]]}, + doTest); + }, window); + }; + + function doTest() { + window.onmessage = function(event) { + ok(event.data.result, event.data.message); + }; + + // Only same-origin iframe should get the events. + var xo = document.getElementById("cross-origin"); + xo.contentWindow.postMessage( + { command: "addEventListener", + expected: false, + message: "Cross-origin iframe shouldn't get the sensor events."}, + "*"); + + var so = document.getElementById("same-origin"); + so.contentWindow.postMessage( + { command: "addEventListener", + expected: true, + message: "Same-origin iframe should get the sensor events." }, + "*"); + + // We need a timeout here to check that something does not happen. + setTimeout(function() { + so.remove(); + xo.remove(); + doWindowTest(); + }, 500); + } + + function doWindowTest() { + var win = window.open("file_bug1197901.html", "w1", "height=100,width=100"); + win.onload = function() { + win.focus(); + SimpleTest.waitForFocus(function() { + var win2 = window.open("file_bug1197901.html", "w2", "height=100,width=100,left=100"); + win2.onload = function() { + win2.focus(); + SimpleTest.waitForFocus(function() { + // Only focused window should get the events. + win.postMessage( + { command: "addEventListener", + expected: false, + message: "Only focused window should get the sensor events." }, + "*"); + win2.postMessage( + { command: "addEventListener", + expected: true, + message: "Focused window should get the sensor events." }, + "*"); + setTimeout(function() { + window.onmessage = null; + win.close(); + win2.close(); + SimpleTest.finish(); + }, 500); + }, win2); + }; + }, win); + }; + } + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<iframe src="file_bug1197901.html" id="same-origin"></iframe> +<iframe src="http://example.com/tests/dom/system/tests/file_bug1197901.html" id="cross-origin"></iframe> +</body> +</html> diff --git a/dom/system/tests/test_pathutils.html b/dom/system/tests/test_pathutils.html new file mode 100644 index 0000000000..4391de77fd --- /dev/null +++ b/dom/system/tests/test_pathutils.html @@ -0,0 +1,602 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> +<html> + +<head> + <meta charset="utf-8"> + <title>PathUtils tests</title> +</head> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<script src="chrome://mochikit/content/tests/SimpleTest/WorkerHandler.js"></script> +<link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +<script> + "use strict"; + + const { Assert } = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + const UNRECOGNIZED_PATH = /Could not initialize path: NS_ERROR_FILE_UNRECOGNIZED_PATH/; + const SPLIT_RELATIVE_ABSOLUTE = /PathUtils.splitRelative requires a relative path/; + const SPLIT_RELATIVE_EMPTY = /PathUtils.splitRelative: Empty directory components \(""\) not allowed by options/; + const SPLIT_RELATIVE_PARENT = /PathUtils.splitRelative: Parent directory components \("\.\."\) not allowed by options/; + const SPLIT_RELATIVE_CURRENT = /PathUtils.splitRelative: Current directory components \("\."\) not allowed by options/; + const EMPTY_PATH = /PathUtils does not support empty paths/; + const JOIN = /Could not append to path/; + + add_task(function test_filename() { + Assert.throws( + () => PathUtils.filename(""), + EMPTY_PATH, + "PathUtils.filename() does not support empty paths" + ); + Assert.throws( + () => PathUtils.filename("foo.txt"), + UNRECOGNIZED_PATH, + "PathUtils.filename() does not support relative paths" + ); + + if (Services.appinfo.OS === "WINNT") { + is( + PathUtils.filename("C:"), + "C:", + "PathUtils.filename() with a drive path" + ); + is( + PathUtils.filename("C:\\"), + "C:", + "PathUtils.filename() with a drive path" + ); + is( + PathUtils.filename("C:\\Windows"), + "Windows", + "PathUtils.filename() with a path with 2 components" + ); + is( + PathUtils.filename("C:\\Windows\\"), + "Windows", + "PathUtils.filename() with a path with 2 components and a trailing slash" + ); + is( + PathUtils.filename("C:\\Windows\\System32"), + "System32", + "PathUtils.filename() with a path with 3 components" + ); + is( + PathUtils.filename("\\\\server"), + "\\\\server", + "PathUtils.filename() with a UNC server path" + ); + is( + PathUtils.filename("C:\\file.dat"), + "file.dat", + "PathUtils.filename() with a file path" + ); + } else { + is( + PathUtils.filename("/"), + "/", + "PathUtils.filename() with a root path" + ); + is( + PathUtils.filename("/usr/"), + "usr", + "PathUtils.filename() with a non-root path" + ); + is( + PathUtils.filename("/usr/lib/libfoo.so"), + "libfoo.so", + "PathUtils.filename() with a path with 3 components" + ); + } + }); + + add_task(function test_parent() { + Assert.throws( + () => PathUtils.parent("."), + UNRECOGNIZED_PATH, + "PathUtils.parent() does not support relative paths" + ); + Assert.throws( + () => PathUtils.parent(""), + EMPTY_PATH, + "PathUtils.parent() does not support empty paths" + ); + + if (Services.appinfo.OS === "WINNT") { + is( + PathUtils.parent("C:"), + null, + "PathUtils.parent() with a drive path" + ); + is( + PathUtils.parent("\\\\server"), + null, + "PathUtils.parent() with a UNC server path" + ); + is( + PathUtils.parent("\\\\server\\foo"), + "\\\\server", + "PathUtils.parent() with a UNC server path and child component" + ); + + Assert.throws( + () => PathUtils.parent("C:", -1), + /^NotSupportedError: PathUtils.parent: A depth of at least 1 is required/, + "PathUtils.parent() with a negative depth throws" + ); + Assert.throws( + () => PathUtils.parent("C:", 0), + /^NotSupportedError: PathUtils.parent: A depth of at least 1 is required/, + "PathUtils.parent() with a zero depth throws" + ); + + { + const path = "C:\\Users\\User\\AppData\\Local\\Mozilla\\Firefox\\Profiles\\foo.default"; + + const expected = [ + "C:\\Users\\User\\AppData\\Local\\Mozilla\\Firefox\\Profiles", + "C:\\Users\\User\\AppData\\Local\\Mozilla\\Firefox", + "C:\\Users\\User\\AppData\\Local\\Mozilla", + "C:\\Users\\User\\AppData\\Local", + "C:\\Users\\User\\AppData", + "C:\\Users\\User", + "C:\\Users", + "C:", + null, + ]; + + for (const [i, parent] of expected.entries()) { + is(PathUtils.parent(path, i + 1), parent, `PathUtils.parent() with depth=${i + 1}`) + } + } + } else { + is( + PathUtils.parent("/"), + null, + "PathUtils.parent() with a root path" + ); + is( + PathUtils.parent("/var"), + "/", + "PathUtils.parent() with a 2 component path" + ); + is( + PathUtils.parent("/var/run"), + "/var", + "PathUtils.parent() with a 3 component path" + ); + + Assert.throws( + () => PathUtils.parent("/", -1), + /^NotSupportedError: PathUtils.parent: A depth of at least 1 is required/, + "PathUtils.parent() with a negative depth throws" + ); + Assert.throws( + () => PathUtils.parent("/", 0), + /^NotSupportedError: PathUtils.parent: A depth of at least 1 is required/, + "PathUtils.parent() with a zero depth throws" + ); + + { + const path = "/home/user/.mozilla/firefox/foo.default"; + const expected = [ + "/home/user/.mozilla/firefox", + "/home/user/.mozilla", + "/home/user", + "/home", + "/", + null, + ]; + + for (const [i, parent] of expected.entries()) { + is( + PathUtils.parent(path, i + 1), + parent, + `PathUtils.parent() with depth=${i + 1}` + ); + } + } + } + }); + + add_task(function test_join() { + Assert.throws( + () => PathUtils.join(), + EMPTY_PATH, + "PathUtils.join() does not support empty paths" + ); + Assert.throws( + () => PathUtils.join(""), + EMPTY_PATH, + "PathUtils.join() does not support empty paths" + ); + Assert.throws( + () => PathUtils.join("foo", "bar"), + UNRECOGNIZED_PATH, + "PathUtils.join() does not support relative paths" + ); + Assert.throws( + () => PathUtils.join("."), + UNRECOGNIZED_PATH, + "PathUtils.join() does not support relative paths" + ); + + if (Services.appinfo.OS === "WINNT") { + is( + PathUtils.join("C:"), + "C:", + "PathUtils.join() with a single path" + ); + is( + PathUtils.join("C:\\Windows", "System32"), + "C:\\Windows\\System32", + "PathUtils.join() with a 2 component path and an additional component" + ); + is( + PathUtils.join("C:", "Users", "Example"), + "C:\\Users\\Example", + "PathUtils.join() with a root path and two additional components" + ); + is( + PathUtils.join("\\\\server", "Files", "Example.dat"), + "\\\\server\\Files\\Example.dat", + "PathUtils.join() with a server path" + ); + } else { + is( + PathUtils.join("/"), + "/", + "PathUtils.join() with a root path" + ); + is( + PathUtils.join("/usr", "lib"), + "/usr/lib", + "PathUtils.join() with a 2 component path and an additional component" + ); + is( + PathUtils.join("/", "home", "example"), + "/home/example", + "PathUtils.join() with a root path and two additional components" + ); + } + }); + + add_task(function test_join_relative() { + if (Services.appinfo.OS === "WINNT") { + is( + PathUtils.joinRelative("C:", ""), + "C:", + "PathUtils.joinRelative() with an empty relative path" + ); + + is( + PathUtils.joinRelative("C:", "foo\\bar\\baz"), + "C:\\foo\\bar\\baz", + "PathUtils.joinRelative() with a relative path containing path separators" + ); + } else { + is( + PathUtils.joinRelative("/", ""), + "/", + "PathUtils.joinRelative() with an empty relative path" + ); + + is( + PathUtils.joinRelative("/", "foo/bar/baz"), + "/foo/bar/baz", + "PathUtils.joinRelative() with a relative path containing path separators" + ); + } + }); + + add_task(async function test_normalize() { + Assert.throws( + () => PathUtils.normalize(""), + EMPTY_PATH, + "PathUtils.normalize() does not support empty paths" + ); + Assert.throws( + () => PathUtils.normalize("."), + UNRECOGNIZED_PATH, + "PathUtils.normalize() does not support relative paths" + ); + + if (Services.appinfo.OS === "WINNT") { + is( + PathUtils.normalize("C:\\\\Windows\\\\..\\\\\\.\\Users\\..\\Windows"), + "C:\\Windows", + "PathUtils.normalize() with a non-normalized path" + ); + } else { + // nsLocalFileUnix::Normalize() calls realpath, which resolves symlinks + // and requires the file to exist. + // + // On Darwin, the temp directory is located in `/private/var`, which is a + // symlink to `/var`, so we need to pre-normalize our temporary directory + // or expected paths won't match. + const tmpDir = PathUtils.join( + PathUtils.normalize(PathUtils.tempDir), + "pathutils_test" + ); + + await IOUtils.makeDirectory(tmpDir, { ignoreExisting: true }); + info(`created tmpDir ${tmpDir}`); + SimpleTest.registerCleanupFunction(async () => { + await IOUtils.remove(tmpDir, { + recursive: true, + }); + }); + + await IOUtils.makeDirectory(PathUtils.join(tmpDir, "foo", "bar"), { + createAncestors: true, + }); + + is( + PathUtils.normalize("/"), + "/", + "PathUtils.normalize() with a normalized path" + ); + + is( + PathUtils.normalize( + PathUtils.join( + tmpDir, + "foo", + ".", + "bar", + ".", + ) + ), + PathUtils.join(tmpDir, "foo", "bar"), + "PathUtils.normalize() with a non-normalized path" + ); + } + }); + + add_task(function test_split() { + Assert.throws( + () => PathUtils.split("foo"), + UNRECOGNIZED_PATH, + "PathUtils.split() does not support relative paths" + ); + Assert.throws( + () => PathUtils.split(""), + EMPTY_PATH, + "PathUtils.split() does not support empty paths" + ); + + if (Services.appinfo.OS === "WINNT") { + Assert.deepEqual( + PathUtils.split("C:\\Users\\Example"), + ["C:", "Users", "Example"], + "PathUtils.split() on an absolute path" + ); + + Assert.deepEqual( + PathUtils.split("C:\\Users\\Example\\"), + ["C:", "Users", "Example"], + "PathUtils.split() on an absolute path with a trailing slash" + ); + + Assert.deepEqual( + PathUtils.split("\\\\server\\Files\\Example.dat"), + ["\\\\server", "Files", "Example.dat"], + "PathUtils.split() with a server as the root" + ); + } else { + Assert.deepEqual( + PathUtils.split("/home/foo"), + ["/", "home", "foo"], + "PathUtils.split() on absolute path" + ); + + Assert.deepEqual( + PathUtils.split("/home/foo/"), + ["/", "home", "foo"], + "PathUtils.split() on absolute path with trailing slash" + ); + } + }); + + add_task(function test_splitRelative() { + Assert.throws( + () => PathUtils.splitRelative(""), + EMPTY_PATH, + "PathUtils.splitRelative() should throw with empty path" + ); + + if (Services.appinfo.OS === "WINNT") { + Assert.throws( + () => PathUtils.splitRelative("C:\\"), + SPLIT_RELATIVE_ABSOLUTE, + "PathUtils.splitRelative() should throw with a drive path" + ); + + Assert.throws( + () => PathUtils.splitRelative("\\\\server\\share\\"), + SPLIT_RELATIVE_ABSOLUTE, + "PathUtils.splitRelative() should throw with a UNC path" + ); + + Assert.throws( + () => PathUtils.splitRelative("foo\\\\bar"), + SPLIT_RELATIVE_EMPTY, + "PathUtils.splitRelative() should throw with empty component by default" + ); + + Assert.deepEqual( + PathUtils.splitRelative("foo\\\\bar", { allowEmpty: true }), + ["foo", "", "bar"], + "PathUtils.splitRelative() with an empty component is allowed with option" + ); + + Assert.deepEqual( + PathUtils.splitRelative("foo"), + ["foo"], + "PathUtils.splitRelative() on a relative path with one component" + ); + + Assert.deepEqual( + PathUtils.splitRelative("foo\\"), + ["foo"], + "PathUtils.splitRelative() on a relative path with one component and a trailing slash" + ); + + Assert.deepEqual( + PathUtils.splitRelative("foo\\bar"), + ["foo", "bar"], + "PathUtils.splitRelative() on a relative path with two components" + ); + } else { + Assert.throws( + () => PathUtils.splitRelative("/foo/bar"), + SPLIT_RELATIVE_ABSOLUTE, + "PathUtils.splitRelative() should throw with an absolute path" + ); + + Assert.throws( + () => PathUtils.splitRelative("foo//bar"), + SPLIT_RELATIVE_EMPTY, + "PathUtils.splitRelative() should throw with empty component by default" + ); + + Assert.deepEqual( + PathUtils.splitRelative("foo//bar", { allowEmpty: true }), + ["foo", "", "bar"], + "PathUtils.splitRelative() with an empty component is allowed with option" + ); + + Assert.deepEqual( + PathUtils.splitRelative("foo"), + ["foo"], + "PathUtils.splitRelative() on a relative path with one component" + ); + + Assert.deepEqual( + PathUtils.splitRelative("foo/"), + ["foo"], + "PathUtils.splitRelative() on a relative path with one component and a trailing slash" + ); + + Assert.deepEqual( + PathUtils.splitRelative("foo/bar"), + ["foo", "bar"], + "PathUtils.splitRelative() on a relative path with two components", + ); + } + + Assert.throws( + () => PathUtils.splitRelative("."), + SPLIT_RELATIVE_CURRENT, + "PathUtils.splitRelative() with a current dir component is disallowed by default" + ); + + Assert.throws( + () => PathUtils.splitRelative(".."), + SPLIT_RELATIVE_PARENT, + "PathUtils.splitRelative() with a parent dir component is disallowed by default" + ); + + Assert.deepEqual( + PathUtils.splitRelative(".", { allowCurrentDir: true }), + ["."], + "PathUtils.splitRelative() with a current dir component is allowed with option" + ); + + Assert.deepEqual( + PathUtils.splitRelative("..", { allowParentDir: true }), + [".."], + "PathUtils.splitRelative() with a parent dir component is allowed with option" + ); + }); + + add_task(function test_toFileURI() { + Assert.throws( + () => PathUtils.toFileURI("."), + UNRECOGNIZED_PATH, + "PathUtils.toFileURI() does not support relative paths" + ); + Assert.throws( + () => PathUtils.toFileURI(""), + EMPTY_PATH, + "PathUtils.toFileURI() does not support empty paths" + ); + + if (Services.appinfo.OS === "WINNT") { + is( + PathUtils.toFileURI("C:\\"), + "file:///C:", + "PathUtils.toFileURI() with a root path" + ); + + is( + PathUtils.toFileURI("C:\\Windows\\"), + "file:///C:/Windows", + "PathUtils.toFileURI() with a non-root directory path" + ); + + is( + PathUtils.toFileURI("C:\\Windows\\system32\\notepad.exe"), + "file:///C:/Windows/system32/notepad.exe", + "PathUtils.toFileURI() with a file path" + ); + } else { + is( + PathUtils.toFileURI("/"), + "file:///", + "PathUtils.toFileURI() with a root path" + ); + + is( + PathUtils.toFileURI("/bin"), + "file:///bin", + "PathUtils.toFileURI() with a non-root directory path" + ); + + is( + PathUtils.toFileURI("/bin/ls"), + "file:///bin/ls", + "PathUtils.toFileURI() with a file path" + ); + } + }); + + add_task(async function test_isAbsolute() { + if (Services.appinfo.OS === "WINNT") { + ok(PathUtils.isAbsolute("C:"), "Drive paths are absolute paths on Windows"); + ok(PathUtils.isAbsolute("C:\\Windows"), "Paths from the root are absolute paths on Windows"); + ok(!PathUtils.isAbsolute("foo"), "Paths containing a single item are not absolute paths on Windows"); + ok(!PathUtils.isAbsolute(".\\foo"), "Paths relative to the current working directory are not absolute paths on Windows"); + ok(!PathUtils.isAbsolute("..\\foo"), "Paths relative to the parent directory are not absolute paths on Windows"); + } else { + ok(PathUtils.isAbsolute("/"), "Root paths are absolute paths"); + ok(PathUtils.isAbsolute("/home"), "Paths with a root stem are absolute paths"); + ok(!PathUtils.isAbsolute("foo"), "Paths containing a single non-root item are not absolute paths"); + ok(!PathUtils.isAbsolute("./foo"), "Paths relative to the current working directory are not absolute paths"); + ok(!PathUtils.isAbsolute("../foo"), "Paths relative to the parent directory are not absolute paths"); + } + }); + + add_task(async function test_getDirectories() { + // See: nsAppDirectoryServiceDefs.h + const tests = [ + ["profileDir", "ProfD"], + ["localProfileDir", "ProfLD"], + ["tempDir", "TmpD"], + ]; + + for (const [attrName, dirConstant] of tests) { + const expected = Services.dirsvc.get(dirConstant, Ci.nsIFile).path; + + const attrValue = PathUtils[attrName]; + is(attrValue, expected, `PathUtils.${attrName} == Services.dirsvc.get("${dirConstant}", Ci.nsIFile).path`); + } + }); +</script> + +<body> +</body> + +</html> diff --git a/dom/system/tests/test_pathutils_worker.xhtml b/dom/system/tests/test_pathutils_worker.xhtml new file mode 100644 index 0000000000..dc181ce07d --- /dev/null +++ b/dom/system/tests/test_pathutils_worker.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<window title="Testing PathUtils on a chrome worker thread" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/WorkerHandler.js"></script> + + <script type="application/javascript"> + <![CDATA[ + function test() { + SimpleTest.waitForExplicitFinish(); + + info("test_pathtuils_worker.xhtml: Starting test"); + + const worker = new ChromeWorker("pathutils_worker.js"); + info("test_pathtuils_worker.xhtml: ChromeWorker created"); + + listenForTests(worker, { verbose: false }); + worker.postMessage({ + profileDir: PathUtils.profileDir, + localProfileDir: PathUtils.localProfileDir, + tempDir: PathUtils.tempDir, + }); + + info("test_pathtuils_worker.xhtml: Test running..."); + } + ]]> + </script> + <body xmlns="https://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> + </body> + <label id="test-result" /> +</window> diff --git a/dom/system/windows/PWindowsUtils.ipdl b/dom/system/windows/PWindowsUtils.ipdl new file mode 100644 index 0000000000..fa5b800f3b --- /dev/null +++ b/dom/system/windows/PWindowsUtils.ipdl @@ -0,0 +1,23 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et ft=cpp : */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PWindowsLocation; + +namespace mozilla { +namespace dom { + +// Manager of utility actors that run in the windows utility process. +[ChildProc=Utility] +protocol PWindowsUtils { + manages PWindowsLocation; + +child: + // Proxies the ILocation COM API for geolocation + async PWindowsLocation(); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/system/windows/WindowsUtilsChild.h b/dom/system/windows/WindowsUtilsChild.h new file mode 100644 index 0000000000..eb6c65251b --- /dev/null +++ b/dom/system/windows/WindowsUtilsChild.h @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WindowsUtilsChild_h__ +#define mozilla_dom_WindowsUtilsChild_h__ + +#include "mozilla/dom/PWindowsUtilsChild.h" +#include "mozilla/dom/WindowsLocationChild.h" + +namespace mozilla::dom { + +// Manager for utilities in the WindowsUtils utility process. +class WindowsUtilsChild final : public PWindowsUtilsChild { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WindowsUtilsChild, override); + + public: + already_AddRefed<PWindowsLocationChild> AllocPWindowsLocationChild() { + return MakeAndAddRef<WindowsLocationChild>(); + } + + protected: + ~WindowsUtilsChild() = default; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_WindowsUtilsChild_h__ diff --git a/dom/system/windows/WindowsUtilsParent.h b/dom/system/windows/WindowsUtilsParent.h new file mode 100644 index 0000000000..bc447a1167 --- /dev/null +++ b/dom/system/windows/WindowsUtilsParent.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WindowsUtilsParent_h__ +#define mozilla_dom_WindowsUtilsParent_h__ + +#include "mozilla/dom/PWindowsUtilsParent.h" +#include "mozilla/ProcInfo.h" +#include "mozilla/ipc/Endpoint.h" +#include "mozilla/ipc/UtilityProcessParent.h" + +namespace mozilla::dom { + +// Main-process manager for utilities in the WindowsUtils utility process. +class WindowsUtilsParent final : public PWindowsUtilsParent { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WindowsUtilsParent, override); + + nsresult BindToUtilityProcess( + RefPtr<mozilla::ipc::UtilityProcessParent> aUtilityParent) { + Endpoint<PWindowsUtilsParent> parentEnd; + Endpoint<PWindowsUtilsChild> childEnd; + nsresult rv = PWindowsUtils::CreateEndpoints(base::GetCurrentProcId(), + aUtilityParent->OtherPid(), + &parentEnd, &childEnd); + + if (NS_FAILED(rv)) { + MOZ_ASSERT(false, "Protocol endpoints failure"); + return NS_ERROR_FAILURE; + } + + if (!aUtilityParent->SendStartWindowsUtilsService(std::move(childEnd))) { + MOZ_ASSERT(false, "SendStartWindowsUtilsService failed"); + return NS_ERROR_FAILURE; + } + + DebugOnly<bool> ok = parentEnd.Bind(this); + MOZ_ASSERT(ok); + return NS_OK; + } + + UtilityActorName GetActorName() { return UtilityActorName::WindowsUtils; } + + protected: + ~WindowsUtilsParent() = default; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_WindowsUtilsParent_h__ diff --git a/dom/system/windows/location/PWindowsLocation.ipdl b/dom/system/windows/location/PWindowsLocation.ipdl new file mode 100644 index 0000000000..db53b5c805 --- /dev/null +++ b/dom/system/windows/location/PWindowsLocation.ipdl @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PWindowsUtils;
+
+[RefCounted] using class nsIDOMGeoPosition from "nsGeoPositionIPCSerialiser.h";
+
+namespace mozilla {
+namespace dom {
+
+// Proxies geolocation functions to a utility process so that we
+// can safely handle crashes in the ILocation API. Messages to the child
+// are proxies for the ILocation COM object. Messages to the parent
+// are proxied nsIGeolocationUpdate callbacks.
+protocol PWindowsLocation {
+ manager PWindowsUtils;
+
+child:
+ async Startup();
+ async RegisterForReport();
+ async UnregisterForReport();
+ async SetHighAccuracy(bool aEnable);
+
+ async __delete__();
+
+parent:
+ // Update geolocation with new position information.
+ async Update(nullable nsIDOMGeoPosition aPosition);
+
+ // The geolocation API has reported an error.
+ async Failed(uint16_t aError);
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/system/windows/location/WindowsLocationChild.cpp b/dom/system/windows/location/WindowsLocationChild.cpp new file mode 100644 index 0000000000..514ae4610f --- /dev/null +++ b/dom/system/windows/location/WindowsLocationChild.cpp @@ -0,0 +1,257 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "WindowsLocationChild.h" +#include "nsCOMPtr.h" +#include "WindowsLocationProvider.h" +#include "mozilla/dom/GeolocationPosition.h" +#include "mozilla/dom/GeolocationPositionErrorBinding.h" +#include "mozilla/Telemetry.h" +#include "nsIGeolocationProvider.h" + +#include <locationapi.h> + +namespace mozilla::dom { + +extern LazyLogModule gWindowsLocationProviderLog; +#define LOG(...) \ + MOZ_LOG(gWindowsLocationProviderLog, LogLevel::Debug, (__VA_ARGS__)) + +class LocationEvent final : public ILocationEvents { + public: + explicit LocationEvent(WindowsLocationChild* aActor) + : mActor(aActor), mRefCnt(0) {} + + // IUnknown interface + STDMETHODIMP_(ULONG) AddRef() override; + STDMETHODIMP_(ULONG) Release() override; + STDMETHODIMP QueryInterface(REFIID iid, void** ppv) override; + + // ILocationEvents interface + STDMETHODIMP OnStatusChanged(REFIID aReportType, + LOCATION_REPORT_STATUS aStatus) override; + STDMETHODIMP OnLocationChanged(REFIID aReportType, + ILocationReport* aReport) override; + + private: + // Making this a WeakPtr breaks the following cycle of strong references: + // WindowsLocationChild -> ILocation -> ILocationEvents (this) + // -> WindowsLocationChild. + WeakPtr<WindowsLocationChild> mActor; + + ULONG mRefCnt; +}; + +STDMETHODIMP_(ULONG) +LocationEvent::AddRef() { return InterlockedIncrement(&mRefCnt); } + +STDMETHODIMP_(ULONG) +LocationEvent::Release() { + ULONG count = InterlockedDecrement(&mRefCnt); + if (!count) { + delete this; + return 0; + } + return count; +} + +STDMETHODIMP +LocationEvent::QueryInterface(REFIID iid, void** ppv) { + if (!ppv) { + return E_INVALIDARG; + } + + if (iid == IID_IUnknown) { + *ppv = static_cast<IUnknown*>(this); + } else if (iid == IID_ILocationEvents) { + *ppv = static_cast<ILocationEvents*>(this); + } else { + *ppv = nullptr; + return E_NOINTERFACE; + } + + AddRef(); + return S_OK; +} + +STDMETHODIMP +LocationEvent::OnStatusChanged(REFIID aReportType, + LOCATION_REPORT_STATUS aStatus) { + LOG("LocationEvent::OnStatusChanged(%p, %p, %s, %04x)", this, mActor.get(), + aReportType == IID_ILatLongReport ? "true" : "false", + static_cast<uint32_t>(aStatus)); + + if (!mActor || aReportType != IID_ILatLongReport) { + return S_OK; + } + + // When registering event, REPORT_INITIALIZING is fired at first. + // Then, when the location is found, REPORT_RUNNING is fired. + // We ignore those messages. + uint16_t err; + switch (aStatus) { + case REPORT_ACCESS_DENIED: + err = GeolocationPositionError_Binding::PERMISSION_DENIED; + break; + case REPORT_NOT_SUPPORTED: + case REPORT_ERROR: + err = GeolocationPositionError_Binding::POSITION_UNAVAILABLE; + break; + default: + return S_OK; + } + + mActor->SendFailed(err); + return S_OK; +} + +STDMETHODIMP +LocationEvent::OnLocationChanged(REFIID aReportType, ILocationReport* aReport) { + LOG("LocationEvent::OnLocationChanged(%p, %p, %s)", this, mActor.get(), + aReportType == IID_ILatLongReport ? "true" : "false"); + + if (!mActor || aReportType != IID_ILatLongReport) { + return S_OK; + } + + RefPtr<ILatLongReport> latLongReport; + if (FAILED(aReport->QueryInterface(IID_ILatLongReport, + getter_AddRefs(latLongReport)))) { + return E_FAIL; + } + + DOUBLE latitude = 0.0; + latLongReport->GetLatitude(&latitude); + + DOUBLE longitude = 0.0; + latLongReport->GetLongitude(&longitude); + + DOUBLE alt = UnspecifiedNaN<double>(); + latLongReport->GetAltitude(&alt); + + DOUBLE herror = 0.0; + latLongReport->GetErrorRadius(&herror); + + DOUBLE verror = UnspecifiedNaN<double>(); + latLongReport->GetAltitudeError(&verror); + + double heading = UnspecifiedNaN<double>(); + double speed = UnspecifiedNaN<double>(); + + // nsGeoPositionCoords will convert NaNs to null for optional properties of + // the JavaScript Coordinates object. + RefPtr<nsGeoPosition> position = + new nsGeoPosition(latitude, longitude, alt, herror, verror, heading, + speed, PR_Now() / PR_USEC_PER_MSEC); + mActor->SendUpdate(position); + + return S_OK; +} + +WindowsLocationChild::WindowsLocationChild() { + LOG("WindowsLocationChild::WindowsLocationChild(%p)", this); +} + +WindowsLocationChild::~WindowsLocationChild() { + LOG("WindowsLocationChild::~WindowsLocationChild(%p)", this); +} + +::mozilla::ipc::IPCResult WindowsLocationChild::RecvStartup() { + LOG("WindowsLocationChild::RecvStartup(%p, %p)", this, mLocation.get()); + if (mLocation) { + return IPC_OK(); + } + + RefPtr<ILocation> location; + if (FAILED(::CoCreateInstance(CLSID_Location, nullptr, CLSCTX_INPROC_SERVER, + IID_ILocation, getter_AddRefs(location)))) { + LOG("WindowsLocationChild(%p) failed to create ILocation", this); + // We will use MLS provider + SendFailed(GeolocationPositionError_Binding::POSITION_UNAVAILABLE); + return IPC_OK(); + } + + IID reportTypes[] = {IID_ILatLongReport}; + if (FAILED(location->RequestPermissions(nullptr, reportTypes, 1, FALSE))) { + LOG("WindowsLocationChild(%p) failed to set ILocation permissions", this); + // We will use MLS provider + SendFailed(GeolocationPositionError_Binding::POSITION_UNAVAILABLE); + return IPC_OK(); + } + + mLocation = location; + return IPC_OK(); +} + +::mozilla::ipc::IPCResult WindowsLocationChild::RecvSetHighAccuracy( + bool aEnable) { + LOG("WindowsLocationChild::RecvSetHighAccuracy(%p, %p, %s)", this, + mLocation.get(), aEnable ? "true" : "false"); + + // We sometimes call SetHighAccuracy before Startup, so we save the + // request and set it later, in RegisterForReport. + mHighAccuracy = aEnable; + + return IPC_OK(); +} + +::mozilla::ipc::IPCResult WindowsLocationChild::RecvRegisterForReport() { + LOG("WindowsLocationChild::RecvRegisterForReport(%p, %p)", this, + mLocation.get()); + + if (!mLocation) { + SendFailed(GeolocationPositionError_Binding::POSITION_UNAVAILABLE); + return IPC_OK(); + } + + LOCATION_DESIRED_ACCURACY desiredAccuracy; + if (mHighAccuracy) { + desiredAccuracy = LOCATION_DESIRED_ACCURACY_HIGH; + } else { + desiredAccuracy = LOCATION_DESIRED_ACCURACY_DEFAULT; + } + + if (NS_WARN_IF(FAILED(mLocation->SetDesiredAccuracy(IID_ILatLongReport, + desiredAccuracy)))) { + SendFailed(GeolocationPositionError_Binding::POSITION_UNAVAILABLE); + return IPC_OK(); + } + + auto event = MakeRefPtr<LocationEvent>(this); + if (NS_WARN_IF( + FAILED(mLocation->RegisterForReport(event, IID_ILatLongReport, 0)))) { + SendFailed(GeolocationPositionError_Binding::POSITION_UNAVAILABLE); + } + + LOG("WindowsLocationChild::RecvRegisterForReport successfully registered"); + return IPC_OK(); +} + +::mozilla::ipc::IPCResult WindowsLocationChild::RecvUnregisterForReport() { + LOG("WindowsLocationChild::RecvUnregisterForReport(%p, %p)", this, + mLocation.get()); + + if (!mLocation) { + return IPC_OK(); + } + + // This will free the LocationEvent we created in RecvRegisterForReport. + Unused << NS_WARN_IF( + FAILED(mLocation->UnregisterForReport(IID_ILatLongReport))); + + // The ILocation object is not reusable. Unregistering, restarting and + // re-registering for reports does not work; the callback is never + // called in that case. For that reason, we re-create the ILocation + // object with a call to Startup after unregistering if we need it again. + mLocation = nullptr; + return IPC_OK(); +} + +void WindowsLocationChild::ActorDestroy(ActorDestroyReason aWhy) { + LOG("WindowsLocationChild::ActorDestroy(%p, %p)", this, mLocation.get()); + mLocation = nullptr; +} + +} // namespace mozilla::dom diff --git a/dom/system/windows/location/WindowsLocationChild.h b/dom/system/windows/location/WindowsLocationChild.h new file mode 100644 index 0000000000..ac51cff213 --- /dev/null +++ b/dom/system/windows/location/WindowsLocationChild.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WindowsLocationChild_h__ +#define mozilla_dom_WindowsLocationChild_h__ + +#include "mozilla/dom/PWindowsLocationChild.h" +#include "mozilla/WeakPtr.h" + +class ILocation; + +namespace mozilla::dom { + +// Geolocation actor in utility process. +class WindowsLocationChild final : public PWindowsLocationChild, + public SupportsWeakPtr { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WindowsLocationChild, override); + + public: + WindowsLocationChild(); + + using IPCResult = ::mozilla::ipc::IPCResult; + + IPCResult RecvStartup(); + IPCResult RecvRegisterForReport(); + IPCResult RecvUnregisterForReport(); + IPCResult RecvSetHighAccuracy(bool aEnable); + void ActorDestroy(ActorDestroyReason aWhy) override; + + private: + ~WindowsLocationChild() override; + + // The COM object the actors are proxying calls for. + RefPtr<ILocation> mLocation; + + bool mHighAccuracy = false; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_WindowsLocationChild_h__ diff --git a/dom/system/windows/location/WindowsLocationParent.cpp b/dom/system/windows/location/WindowsLocationParent.cpp new file mode 100644 index 0000000000..f19bcb8b0d --- /dev/null +++ b/dom/system/windows/location/WindowsLocationParent.cpp @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "WindowsLocationParent.h" +#include "nsIDOMGeoPosition.h" +#include "WindowsLocationProvider.h" + +namespace mozilla::dom { + +::mozilla::ipc::IPCResult WindowsLocationParent::RecvUpdate( + RefPtr<nsIDOMGeoPosition> aGeoPosition) { + if (mProvider) { + mProvider->RecvUpdate(aGeoPosition); + } + return IPC_OK(); +} + +// A failure occurred. This may be translated into a +// nsIGeolocationUpdate::NotifyError or may be ignored if the MLS fallback +// is available. +::mozilla::ipc::IPCResult WindowsLocationParent::RecvFailed(uint16_t err) { + if (mProvider) { + mProvider->RecvFailed(err); + } + return IPC_OK(); +} + +void WindowsLocationParent::ActorDestroy(ActorDestroyReason aReason) { + if (mProvider) { + mProvider->ActorStopped(); + } +} + +} // namespace mozilla::dom diff --git a/dom/system/windows/location/WindowsLocationParent.h b/dom/system/windows/location/WindowsLocationParent.h new file mode 100644 index 0000000000..01538246ea --- /dev/null +++ b/dom/system/windows/location/WindowsLocationParent.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WindowsLocationParent_h__ +#define mozilla_dom_WindowsLocationParent_h__ + +#include "nsCOMPtr.h" +#include "mozilla/dom/PWindowsLocationParent.h" + +class nsGeoPosition; +class nsIGeolocationUpdate; + +namespace mozilla::dom { + +class WindowsLocationProvider; + +// Geolocation actor in main process. +// This may receive messages asynchronously, even after it sends Unregister +// to the child. +class WindowsLocationParent final : public PWindowsLocationParent { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WindowsLocationParent, override); + + using IPCResult = ::mozilla::ipc::IPCResult; + + explicit WindowsLocationParent(WindowsLocationProvider* aProvider) + : mProvider(aProvider) {} + + // Update geolocation with new position information. + IPCResult RecvUpdate(RefPtr<nsIDOMGeoPosition> aGeoPosition); + + // A failure occurred. This may be translated into a + // nsIGeolocationUpdate::NotifyError or may be ignored if the MLS fallback + // is available. + IPCResult RecvFailed(uint16_t err); + + void ActorDestroy(ActorDestroyReason aReason) override; + + // After this, the actor will simply ignore any incoming messages. + void DetachFromLocationProvider() { mProvider = nullptr; } + + private: + ~WindowsLocationParent() override = default; + + WindowsLocationProvider* mProvider; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_WindowsLocationParent_h__ diff --git a/dom/system/windows/location/WindowsLocationProvider.cpp b/dom/system/windows/location/WindowsLocationProvider.cpp new file mode 100644 index 0000000000..92a6f2c9cc --- /dev/null +++ b/dom/system/windows/location/WindowsLocationProvider.cpp @@ -0,0 +1,350 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WindowsLocationProvider.h" +#include "WindowsLocationParent.h" +#include "mozilla/dom/WindowsUtilsParent.h" +#include "GeolocationPosition.h" +#include "nsComponentManagerUtils.h" +#include "mozilla/ipc/UtilityProcessManager.h" +#include "mozilla/ipc/UtilityProcessSandboxing.h" +#include "prtime.h" +#include "MLSFallback.h" +#include "mozilla/Attributes.h" +#include "mozilla/Logging.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/Telemetry.h" +#include "mozilla/dom/GeolocationPositionErrorBinding.h" + +namespace mozilla::dom { + +LazyLogModule gWindowsLocationProviderLog("WindowsLocationProvider"); +#define LOG(...) \ + MOZ_LOG(gWindowsLocationProviderLog, LogLevel::Debug, (__VA_ARGS__)) + +class MLSUpdate : public nsIGeolocationUpdate { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONUPDATE + explicit MLSUpdate(nsIGeolocationUpdate* aCallback) : mCallback(aCallback) {} + + private: + nsCOMPtr<nsIGeolocationUpdate> mCallback; + virtual ~MLSUpdate() {} +}; + +NS_IMPL_ISUPPORTS(MLSUpdate, nsIGeolocationUpdate); + +NS_IMETHODIMP +MLSUpdate::Update(nsIDOMGeoPosition* aPosition) { + if (!mCallback) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIDOMGeoPositionCoords> coords; + aPosition->GetCoords(getter_AddRefs(coords)); + if (!coords) { + return NS_ERROR_FAILURE; + } + Telemetry::Accumulate(Telemetry::GEOLOCATION_WIN8_SOURCE_IS_MLS, true); + return mCallback->Update(aPosition); +} +NS_IMETHODIMP +MLSUpdate::NotifyError(uint16_t aError) { + if (!mCallback) { + return NS_ERROR_FAILURE; + } + nsCOMPtr<nsIGeolocationUpdate> callback(mCallback); + return callback->NotifyError(aError); +} + +NS_IMPL_ISUPPORTS(WindowsLocationProvider, nsIGeolocationProvider) + +WindowsLocationProvider::WindowsLocationProvider() { + LOG("WindowsLocationProvider::WindowsLocationProvider(%p)", this); + MOZ_ASSERT(XRE_IsParentProcess()); + MaybeCreateLocationActor(); +} + +WindowsLocationProvider::~WindowsLocationProvider() { + LOG("WindowsLocationProvider::~WindowsLocationProvider(%p,%p,%p)", this, + mActor.get(), mActorPromise.get()); + Send__delete__(); + ReleaseUtilityProcess(); + CancelMLSProvider(); +} + +void WindowsLocationProvider::MaybeCreateLocationActor() { + LOG("WindowsLocationProvider::MaybeCreateLocationActor(%p)", this); + if (mActor || mActorPromise) { + return; + } + + auto utilityProc = mozilla::ipc::UtilityProcessManager::GetSingleton(); + MOZ_ASSERT(utilityProc); + + // Create a PWindowsLocation actor in the Windows utility process. + // This will attempt to launch the process if it doesn't already exist. + RefPtr<WindowsLocationProvider> self = this; + auto wuPromise = utilityProc->GetWindowsUtilsPromise(); + mActorPromise = wuPromise->Then( + GetCurrentSerialEventTarget(), __func__, + [self](RefPtr<WindowsUtilsParent> wup) { + self->mActorPromise = nullptr; + auto actor = MakeRefPtr<WindowsLocationParent>(self); + if (!wup->SendPWindowsLocationConstructor(actor)) { + LOG("WindowsLocationProvider(%p) SendPWindowsLocationConstructor " + "failed", + self.get()); + actor->DetachFromLocationProvider(); + self->mActor = nullptr; + return WindowsLocationPromise::CreateAndReject(false, __func__); + } + LOG("WindowsLocationProvider connected to actor (%p,%p,%p)", self.get(), + self->mActor.get(), self->mActorPromise.get()); + self->mActor = actor; + return WindowsLocationPromise::CreateAndResolve(self->mActor, __func__); + }, + + [self](nsresult aError) { + LOG("WindowsLocationProvider failed to connect to actor (%p,%p,%p)", + self.get(), self->mActor.get(), self->mActorPromise.get()); + self->mActorPromise = nullptr; + return WindowsLocationPromise::CreateAndReject(false, __func__); + }); + + if (mActor) { + // Utility process already existed and mActorPromise was resolved + // immediately. + mActorPromise = nullptr; + } +} + +void WindowsLocationProvider::ReleaseUtilityProcess() { + LOG("WindowsLocationProvider::ReleaseUtilityProcess(%p)", this); + auto utilityProc = mozilla::ipc::UtilityProcessManager::GetIfExists(); + if (utilityProc) { + utilityProc->ReleaseWindowsUtils(); + } +} + +template <typename Fn> +bool WindowsLocationProvider::WhenActorIsReady(Fn&& fn) { + if (mActor) { + return fn(mActor); + } + + if (mActorPromise) { + mActorPromise->Then( + GetCurrentSerialEventTarget(), __func__, + [fn](const RefPtr<WindowsLocationParent>& actor) { + Unused << fn(actor.get()); + return actor; + }, + [](bool) { return false; }); + return true; + } + + // The remote process failed to start. + return false; +} + +bool WindowsLocationProvider::SendStartup() { + LOG("WindowsLocationProvider::SendStartup(%p)", this); + MaybeCreateLocationActor(); + return WhenActorIsReady( + [](WindowsLocationParent* actor) { return actor->SendStartup(); }); +} + +bool WindowsLocationProvider::SendRegisterForReport( + nsIGeolocationUpdate* aCallback) { + LOG("WindowsLocationProvider::SendRegisterForReport(%p)", this); + RefPtr<WindowsLocationProvider> self = this; + RefPtr<nsIGeolocationUpdate> cb = aCallback; + return WhenActorIsReady([self, cb](WindowsLocationParent* actor) { + MOZ_ASSERT(!self->mCallback); + if (actor->SendRegisterForReport()) { + self->mCallback = cb; + return true; + } + return false; + }); +} + +bool WindowsLocationProvider::SendUnregisterForReport() { + LOG("WindowsLocationProvider::SendUnregisterForReport(%p)", this); + RefPtr<WindowsLocationProvider> self = this; + return WhenActorIsReady([self](WindowsLocationParent* actor) { + self->mCallback = nullptr; + if (actor->SendUnregisterForReport()) { + return true; + } + return false; + }); +} + +bool WindowsLocationProvider::SendSetHighAccuracy(bool aEnable) { + LOG("WindowsLocationProvider::SendSetHighAccuracy(%p)", this); + return WhenActorIsReady([aEnable](WindowsLocationParent* actor) { + return actor->SendSetHighAccuracy(aEnable); + }); +} + +bool WindowsLocationProvider::Send__delete__() { + LOG("WindowsLocationProvider::Send__delete__(%p)", this); + return WhenActorIsReady([self = RefPtr{this}](WindowsLocationParent*) { + if (WindowsLocationParent::Send__delete__(self->mActor)) { + if (self->mActor) { + self->mActor->DetachFromLocationProvider(); + self->mActor = nullptr; + } + return true; + } + return false; + }); +} + +void WindowsLocationProvider::RecvUpdate( + RefPtr<nsIDOMGeoPosition> aGeoPosition) { + LOG("WindowsLocationProvider::RecvUpdate(%p)", this); + if (!mCallback) { + return; + } + + mCallback->Update(aGeoPosition.get()); + + Telemetry::Accumulate(Telemetry::GEOLOCATION_WIN8_SOURCE_IS_MLS, false); +} + +void WindowsLocationProvider::RecvFailed(uint16_t err) { + LOG("WindowsLocationProvider::RecvFailed(%p)", this); + // Cannot get current location at this time. We use MLS instead. + if (mMLSProvider || !mCallback) { + return; + } + + if (NS_SUCCEEDED(CreateAndWatchMLSProvider(mCallback))) { + return; + } + + // No ILocation and no MLS, so we have failed completely. + // We keep strong references to objects that we need to guarantee + // will live past the NotifyError callback. + RefPtr<WindowsLocationProvider> self = this; + nsCOMPtr<nsIGeolocationUpdate> callback = mCallback; + callback->NotifyError(err); +} + +void WindowsLocationProvider::ActorStopped() { + // ActorDestroy has run. Make sure UtilityProcessHost no longer tries to use + // it. + ReleaseUtilityProcess(); + + if (mWatching) { + // Treat as remote geolocation error, which will cause it to fallback + // to MLS if it hasn't already. + mWatching = false; + RecvFailed(GeolocationPositionError_Binding::POSITION_UNAVAILABLE); + return; + } + + MOZ_ASSERT(!mActorPromise); + if (mActor) { + mActor->DetachFromLocationProvider(); + mActor = nullptr; + } +} + +NS_IMETHODIMP +WindowsLocationProvider::Startup() { + LOG("WindowsLocationProvider::Startup(%p, %p, %p)", this, mActor.get(), + mActorPromise.get()); + // If this fails, we will use the MLS fallback. + SendStartup(); + return NS_OK; +} + +NS_IMETHODIMP +WindowsLocationProvider::Watch(nsIGeolocationUpdate* aCallback) { + LOG("WindowsLocationProvider::Watch(%p, %p, %p, %p, %d)", this, mActor.get(), + mActorPromise.get(), aCallback, mWatching); + if (mWatching) { + return NS_OK; + } + + if (SendRegisterForReport(aCallback)) { + mWatching = true; + return NS_OK; + } + + // Couldn't send request. We will use MLS instead. + return CreateAndWatchMLSProvider(aCallback); +} + +NS_IMETHODIMP +WindowsLocationProvider::Shutdown() { + LOG("WindowsLocationProvider::Shutdown(%p, %p, %p)", this, mActor.get(), + mActorPromise.get()); + + if (mWatching) { + SendUnregisterForReport(); + mWatching = false; + } + + CancelMLSProvider(); + return NS_OK; +} + +NS_IMETHODIMP +WindowsLocationProvider::SetHighAccuracy(bool enable) { + LOG("WindowsLocationProvider::SetHighAccuracy(%p, %p, %p, %s)", this, + mActor.get(), mActorPromise.get(), enable ? "true" : "false"); + if (mMLSProvider) { + // Ignored when running MLS fallback. + return NS_OK; + } + + if (!SendSetHighAccuracy(enable)) { + return NS_ERROR_FAILURE; + } + + // Since we SendSetHighAccuracy asynchronously, we cannot say for sure + // that it will succeed. If it does fail then we will get a + // RecvFailed IPC message, which will cause a fallback to MLS. + return NS_OK; +} + +nsresult WindowsLocationProvider::CreateAndWatchMLSProvider( + nsIGeolocationUpdate* aCallback) { + LOG("WindowsLocationProvider::CreateAndWatchMLSProvider" + "(%p, %p, %p, %p, %p)", + this, mMLSProvider.get(), mActor.get(), mActorPromise.get(), aCallback); + + if (mMLSProvider) { + return NS_OK; + } + + mMLSProvider = new MLSFallback(0); + return mMLSProvider->Startup(new MLSUpdate(aCallback)); +} + +void WindowsLocationProvider::CancelMLSProvider() { + LOG("WindowsLocationProvider::CancelMLSProvider" + "(%p, %p, %p, %p, %p)", + this, mMLSProvider.get(), mActor.get(), mActorPromise.get(), + mCallback.get()); + + if (!mMLSProvider) { + return; + } + + mMLSProvider->Shutdown(); + mMLSProvider = nullptr; +} + +#undef LOG + +} // namespace mozilla::dom diff --git a/dom/system/windows/location/WindowsLocationProvider.h b/dom/system/windows/location/WindowsLocationProvider.h new file mode 100644 index 0000000000..d1e4dfa936 --- /dev/null +++ b/dom/system/windows/location/WindowsLocationProvider.h @@ -0,0 +1,78 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WindowsLocationProvider_h__ +#define mozilla_dom_WindowsLocationProvider_h__ + +#include "nsCOMPtr.h" +#include "nsIGeolocationProvider.h" +#include "mozilla/MozPromise.h" + +class MLSFallback; + +namespace mozilla::dom { + +class WindowsLocationParent; + +// Uses a PWindowsLocation actor to subscribe to geolocation updates from the +// Windows utility process and falls back to MLS when it is not available or +// fails. +class WindowsLocationProvider final : public nsIGeolocationProvider { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONPROVIDER + + WindowsLocationProvider(); + + private: + friend WindowsLocationParent; + + ~WindowsLocationProvider(); + + nsresult CreateAndWatchMLSProvider(nsIGeolocationUpdate* aCallback); + void CancelMLSProvider(); + + void MaybeCreateLocationActor(); + void ReleaseUtilityProcess(); + + // These methods either send the message on the existing actor or queue + // the messages to be sent (in order) once the actor exists. + bool SendStartup(); + bool SendRegisterForReport(nsIGeolocationUpdate* aCallback); + bool SendUnregisterForReport(); + bool SendSetHighAccuracy(bool aEnable); + bool Send__delete__(); + + void RecvUpdate(RefPtr<nsIDOMGeoPosition> aGeoPosition); + // See bug 1539864 for MOZ_CAN_RUN_SCRIPT_BOUNDARY justification. + MOZ_CAN_RUN_SCRIPT_BOUNDARY void RecvFailed(uint16_t err); + + // The utility process actor has ended its connection, either successfully + // or with an error. + void ActorStopped(); + + // Run fn once actor is ready to send messages, which may be immediately. + template <typename Fn> + bool WhenActorIsReady(Fn&& fn); + + RefPtr<MLSFallback> mMLSProvider; + + nsCOMPtr<nsIGeolocationUpdate> mCallback; + + using WindowsLocationPromise = + MozPromise<RefPtr<WindowsLocationParent>, bool, false>; + + // Before the utility process exists, we have a promise that we will get our + // location actor. mActor and mActorPromise are never both set. + RefPtr<WindowsLocationPromise> mActorPromise; + RefPtr<WindowsLocationParent> mActor; + + bool mWatching = false; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_WindowsLocationProvider_h__ diff --git a/dom/system/windows/location/moz.build b/dom/system/windows/location/moz.build new file mode 100644 index 0000000000..8fafb99fb1 --- /dev/null +++ b/dom/system/windows/location/moz.build @@ -0,0 +1,29 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS.mozilla.dom += [ + "WindowsLocationChild.h", + "WindowsLocationParent.h", +] + +UNIFIED_SOURCES += [ + "WindowsLocationParent.cpp", + "WindowsLocationProvider.cpp", +] + +SOURCES += [ + "WindowsLocationChild.cpp", # includes locationapi.h +] + +IPDL_SOURCES += [ + "PWindowsLocation.ipdl", +] + +LOCAL_INCLUDES += ["/dom/geolocation"] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/system/windows/moz.build b/dom/system/windows/moz.build new file mode 100644 index 0000000000..0ee12d4cce --- /dev/null +++ b/dom/system/windows/moz.build @@ -0,0 +1,24 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS.mozilla.dom += [ + "WindowsUtilsChild.h", + "WindowsUtilsParent.h", +] + +UNIFIED_SOURCES += [ + "nsHapticFeedback.cpp", +] + +IPDL_SOURCES += [ + "PWindowsUtils.ipdl", +] + +DIRS += ["location"] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/system/windows/nsHapticFeedback.cpp b/dom/system/windows/nsHapticFeedback.cpp new file mode 100644 index 0000000000..f85c5889d9 --- /dev/null +++ b/dom/system/windows/nsHapticFeedback.cpp @@ -0,0 +1,15 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsHapticFeedback.h" + +NS_IMPL_ISUPPORTS(nsHapticFeedback, nsIHapticFeedback) + +NS_IMETHODIMP +nsHapticFeedback::PerformSimpleAction(int32_t aType) { + // Todo + return NS_OK; +} diff --git a/dom/system/windows/nsHapticFeedback.h b/dom/system/windows/nsHapticFeedback.h new file mode 100644 index 0000000000..b15cb00f31 --- /dev/null +++ b/dom/system/windows/nsHapticFeedback.h @@ -0,0 +1,15 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIHapticFeedback.h" + +class nsHapticFeedback final : public nsIHapticFeedback { + ~nsHapticFeedback() {} + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHAPTICFEEDBACK +}; |