summaryrefslogtreecommitdiffstats
path: root/js/xpconnect/loader
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /js/xpconnect/loader
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/xpconnect/loader')
-rw-r--r--js/xpconnect/loader/AutoMemMap.cpp158
-rw-r--r--js/xpconnect/loader/AutoMemMap.h100
-rw-r--r--js/xpconnect/loader/ChromeScriptLoader.cpp592
-rw-r--r--js/xpconnect/loader/ComponentUtils.sys.mjs33
-rw-r--r--js/xpconnect/loader/IOBuffers.h148
-rw-r--r--js/xpconnect/loader/JSMEnvironmentProxy.cpp259
-rw-r--r--js/xpconnect/loader/JSMEnvironmentProxy.h31
-rw-r--r--js/xpconnect/loader/ModuleEnvironmentProxy.cpp237
-rw-r--r--js/xpconnect/loader/ModuleEnvironmentProxy.h30
-rw-r--r--js/xpconnect/loader/PScriptCache.ipdl36
-rw-r--r--js/xpconnect/loader/PrecompiledScript.h63
-rw-r--r--js/xpconnect/loader/ScriptCacheActors.cpp92
-rw-r--r--js/xpconnect/loader/ScriptCacheActors.h59
-rw-r--r--js/xpconnect/loader/ScriptPreloader-inl.h167
-rw-r--r--js/xpconnect/loader/ScriptPreloader.cpp1375
-rw-r--r--js/xpconnect/loader/ScriptPreloader.h555
-rw-r--r--js/xpconnect/loader/SkipCheckForBrokenURLOrZeroSized.h22
-rw-r--r--js/xpconnect/loader/SyncModuleLoader.cpp259
-rw-r--r--js/xpconnect/loader/SyncModuleLoader.h108
-rw-r--r--js/xpconnect/loader/URLPreloader.cpp709
-rw-r--r--js/xpconnect/loader/URLPreloader.h318
-rw-r--r--js/xpconnect/loader/XPCOMUtils.sys.mjs334
-rw-r--r--js/xpconnect/loader/moz.build67
-rw-r--r--js/xpconnect/loader/mozJSLoaderUtils.cpp76
-rw-r--r--js/xpconnect/loader/mozJSLoaderUtils.h30
-rw-r--r--js/xpconnect/loader/mozJSModuleLoader.cpp2227
-rw-r--r--js/xpconnect/loader/mozJSModuleLoader.h350
-rw-r--r--js/xpconnect/loader/mozJSSubScriptLoader.cpp476
-rw-r--r--js/xpconnect/loader/mozJSSubScriptLoader.h50
-rw-r--r--js/xpconnect/loader/nsImportModule.cpp113
-rw-r--r--js/xpconnect/loader/nsImportModule.h240
-rwxr-xr-xjs/xpconnect/loader/script_cache.py92
32 files changed, 9406 insertions, 0 deletions
diff --git a/js/xpconnect/loader/AutoMemMap.cpp b/js/xpconnect/loader/AutoMemMap.cpp
new file mode 100644
index 0000000000..7ce26a9a33
--- /dev/null
+++ b/js/xpconnect/loader/AutoMemMap.cpp
@@ -0,0 +1,158 @@
+/* -*- 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 "AutoMemMap.h"
+#include "ScriptPreloader-inl.h"
+
+#include "mozilla/Unused.h"
+#include "mozilla/Try.h"
+#include "mozilla/ipc/FileDescriptor.h"
+#include "nsIFile.h"
+
+#include <private/pprio.h>
+
+namespace mozilla {
+namespace loader {
+
+using namespace mozilla::ipc;
+
+AutoMemMap::~AutoMemMap() { reset(); }
+
+FileDescriptor AutoMemMap::cloneFileDescriptor() const {
+ if (fd.get()) {
+ auto handle =
+ FileDescriptor::PlatformHandleType(PR_FileDesc2NativeHandle(fd.get()));
+ return FileDescriptor(handle);
+ }
+ return FileDescriptor();
+}
+
+Result<Ok, nsresult> AutoMemMap::init(nsIFile* file, int flags, int mode,
+ PRFileMapProtect prot) {
+ MOZ_ASSERT(!fd);
+
+ MOZ_TRY(file->OpenNSPRFileDesc(flags, mode, getter_Transfers(fd)));
+
+ return initInternal(prot);
+}
+
+Result<Ok, nsresult> AutoMemMap::init(const FileDescriptor& file,
+ PRFileMapProtect prot, size_t maybeSize) {
+ MOZ_ASSERT(!fd);
+ if (!file.IsValid()) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ auto handle = file.ClonePlatformHandle();
+
+ fd.reset(PR_ImportFile(PROsfd(handle.get())));
+ if (!fd) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ Unused << handle.release();
+
+ return initInternal(prot, maybeSize);
+}
+
+Result<Ok, nsresult> AutoMemMap::initInternal(PRFileMapProtect prot,
+ size_t maybeSize) {
+ MOZ_ASSERT(!fileMap);
+ MOZ_ASSERT(!addr);
+
+ if (maybeSize > 0) {
+ // Some OSes' shared memory objects can't be stat()ed, either at
+ // all (Android) or without loosening the sandbox (Mac) so just
+ // use the size.
+ size_ = maybeSize;
+ } else {
+ // But if we don't have the size, assume it's a regular file and
+ // ask for it.
+ PRFileInfo64 fileInfo;
+ MOZ_TRY(PR_GetOpenFileInfo64(fd.get(), &fileInfo));
+
+ if (fileInfo.size > UINT32_MAX) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+ size_ = fileInfo.size;
+ }
+
+ fileMap = PR_CreateFileMap(fd.get(), 0, prot);
+ if (!fileMap) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ addr = PR_MemMap(fileMap, 0, size_);
+ if (!addr) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ return Ok();
+}
+
+#ifdef XP_WIN
+
+Result<Ok, nsresult> AutoMemMap::initWithHandle(const FileDescriptor& file,
+ size_t size,
+ PRFileMapProtect prot) {
+ MOZ_ASSERT(!fd);
+ MOZ_ASSERT(!handle_);
+ if (!file.IsValid()) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ handle_ = file.ClonePlatformHandle().release();
+
+ MOZ_ASSERT(!addr);
+
+ size_ = size;
+
+ addr = MapViewOfFile(
+ handle_, prot == PR_PROT_READONLY ? FILE_MAP_READ : FILE_MAP_ALL_ACCESS,
+ 0, 0, size);
+ if (!addr) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ return Ok();
+}
+
+FileDescriptor AutoMemMap::cloneHandle() const {
+ return FileDescriptor(handle_);
+}
+
+#else
+
+Result<Ok, nsresult> AutoMemMap::initWithHandle(const FileDescriptor& file,
+ size_t size,
+ PRFileMapProtect prot) {
+ MOZ_DIAGNOSTIC_ASSERT(size > 0);
+ return init(file, prot, size);
+}
+
+FileDescriptor AutoMemMap::cloneHandle() const { return cloneFileDescriptor(); }
+
+#endif
+
+void AutoMemMap::reset() {
+ if (addr && !persistent_) {
+ Unused << NS_WARN_IF(PR_MemUnmap(addr, size()) != PR_SUCCESS);
+ addr = nullptr;
+ }
+ if (fileMap) {
+ Unused << NS_WARN_IF(PR_CloseFileMap(fileMap) != PR_SUCCESS);
+ fileMap = nullptr;
+ }
+#ifdef XP_WIN
+ if (handle_) {
+ CloseHandle(handle_);
+ handle_ = nullptr;
+ }
+#endif
+ fd = nullptr;
+}
+
+} // namespace loader
+} // namespace mozilla
diff --git a/js/xpconnect/loader/AutoMemMap.h b/js/xpconnect/loader/AutoMemMap.h
new file mode 100644
index 0000000000..54180d09e3
--- /dev/null
+++ b/js/xpconnect/loader/AutoMemMap.h
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; 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/. */
+
+#ifndef loader_AutoMemMap_h
+#define loader_AutoMemMap_h
+
+#include "mozilla/FileUtils.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/RangedPtr.h"
+#include "mozilla/Result.h"
+
+#include <prio.h>
+
+class nsIFile;
+
+namespace mozilla {
+namespace ipc {
+class FileDescriptor;
+}
+
+namespace loader {
+
+class AutoMemMap {
+ typedef mozilla::ipc::FileDescriptor FileDescriptor;
+
+ public:
+ AutoMemMap() = default;
+
+ ~AutoMemMap();
+
+ Result<Ok, nsresult> init(nsIFile* file, int flags = PR_RDONLY, int mode = 0,
+ PRFileMapProtect prot = PR_PROT_READONLY);
+
+ Result<Ok, nsresult> init(const FileDescriptor& file,
+ PRFileMapProtect prot = PR_PROT_READONLY,
+ size_t maybeSize = 0);
+
+ // Initializes the mapped memory with a shared memory handle. On
+ // Unix-like systems, this is identical to the above init() method. On
+ // Windows, the FileDescriptor must be a handle for a file mapping,
+ // rather than a file descriptor.
+ Result<Ok, nsresult> initWithHandle(const FileDescriptor& file, size_t size,
+ PRFileMapProtect prot = PR_PROT_READONLY);
+
+ void reset();
+
+ bool initialized() const { return addr; }
+
+ uint32_t size() const { return size_; }
+
+ template <typename T = void>
+ RangedPtr<T> get() {
+ MOZ_ASSERT(addr);
+ return {static_cast<T*>(addr), size_};
+ }
+
+ template <typename T = void>
+ const RangedPtr<T> get() const {
+ MOZ_ASSERT(addr);
+ return {static_cast<T*>(addr), size_};
+ }
+
+ size_t nonHeapSizeOfExcludingThis() { return size_; }
+
+ FileDescriptor cloneFileDescriptor() const;
+ FileDescriptor cloneHandle() const;
+
+ // Makes this mapping persistent. After calling this, the mapped memory
+ // will remained mapped, even after this instance is destroyed.
+ void setPersistent() { persistent_ = true; }
+
+ private:
+ Result<Ok, nsresult> initInternal(PRFileMapProtect prot,
+ size_t maybeSize = 0);
+
+ AutoFDClose fd;
+ PRFileMap* fileMap = nullptr;
+
+#ifdef XP_WIN
+ // We can't include windows.h in this header, since it gets included
+ // by some binding headers (which are explicitly incompatible with
+ // windows.h). So we can't use the HANDLE type here.
+ void* handle_ = nullptr;
+#endif
+
+ uint32_t size_ = 0;
+ void* addr = nullptr;
+
+ bool persistent_ = 0;
+
+ AutoMemMap(const AutoMemMap&) = delete;
+ void operator=(const AutoMemMap&) = delete;
+};
+
+} // namespace loader
+} // namespace mozilla
+
+#endif // loader_AutoMemMap_h
diff --git a/js/xpconnect/loader/ChromeScriptLoader.cpp b/js/xpconnect/loader/ChromeScriptLoader.cpp
new file mode 100644
index 0000000000..5c7115c997
--- /dev/null
+++ b/js/xpconnect/loader/ChromeScriptLoader.cpp
@@ -0,0 +1,592 @@
+/* -*- 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 "PrecompiledScript.h"
+
+#include "nsIIncrementalStreamLoader.h"
+#include "nsIURI.h"
+#include "nsIChannel.h"
+#include "nsNetUtil.h"
+#include "nsThreadUtils.h"
+
+#include "jsapi.h"
+#include "jsfriendapi.h"
+#include "js/CompileOptions.h" // JS::CompileOptions, JS::OwningCompileOptions
+#include "js/CompilationAndEvaluation.h"
+#include "js/experimental/CompileScript.h" // JS::CompileGlobalScriptToStencil, JS::NewFrontendContext, JS::DestroyFrontendContext, JS::SetNativeStackQuota, JS::ThreadStackQuotaForSize, JS::HadFrontendErrors, JS::ConvertFrontendErrorsToRuntimeErrors
+#include "js/experimental/JSStencil.h" // JS::Stencil, JS::CompileGlobalScriptToStencil, JS::InstantiateGlobalStencil, JS::CompilationStorage
+#include "js/SourceText.h" // JS::SourceText
+#include "js/Utility.h"
+
+#include "mozilla/AlreadyAddRefed.h" // already_AddRefed
+#include "mozilla/Assertions.h" // MOZ_ASSERT
+#include "mozilla/Attributes.h"
+#include "mozilla/ClearOnShutdown.h" // RunOnShutdown
+#include "mozilla/EventQueue.h" // EventQueuePriority
+#include "mozilla/Mutex.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPrefs_javascript.h"
+#include "mozilla/dom/ChromeUtils.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/ScriptLoader.h"
+#include "mozilla/HoldDropJSObjects.h"
+#include "mozilla/RefPtr.h" // RefPtr
+#include "mozilla/TaskController.h" // TaskController, Task
+#include "mozilla/ThreadSafety.h" // MOZ_GUARDED_BY
+#include "mozilla/Utf8.h" // Utf8Unit
+#include "mozilla/Vector.h"
+#include "nsCCUncollectableMarker.h"
+#include "nsCycleCollectionParticipant.h"
+
+using namespace JS;
+using namespace mozilla;
+using namespace mozilla::dom;
+
+class AsyncScriptCompileTask final : public Task {
+ static mozilla::StaticMutex sOngoingTasksMutex;
+ static Vector<AsyncScriptCompileTask*> sOngoingTasks
+ MOZ_GUARDED_BY(sOngoingTasksMutex);
+ static bool sIsShutdownRegistered;
+
+ // Compilation tasks should be cancelled before calling JS_ShutDown, in order
+ // to avoid keeping JS::Stencil and SharedImmutableString pointers alive
+ // beyond it.
+ //
+ // Cancel all ongoing tasks at ShutdownPhase::XPCOMShutdownFinal, which
+ // happens before calling JS_ShutDown.
+ static bool RegisterTask(AsyncScriptCompileTask* aTask) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!sIsShutdownRegistered) {
+ sIsShutdownRegistered = true;
+
+ RunOnShutdown([] {
+ StaticMutexAutoLock lock(sOngoingTasksMutex);
+ for (auto* task : sOngoingTasks) {
+ task->Cancel();
+ }
+ });
+ }
+
+ StaticMutexAutoLock lock(sOngoingTasksMutex);
+ return sOngoingTasks.append(aTask);
+ }
+
+ static void UnregisterTask(const AsyncScriptCompileTask* aTask) {
+ StaticMutexAutoLock lock(sOngoingTasksMutex);
+ sOngoingTasks.eraseIfEqual(aTask);
+ }
+
+ public:
+ explicit AsyncScriptCompileTask(JS::SourceText<Utf8Unit>&& aSrcBuf)
+ : Task(Kind::OffMainThreadOnly, EventQueuePriority::Normal),
+ mOptions(JS::OwningCompileOptions::ForFrontendContext()),
+ mSrcBuf(std::move(aSrcBuf)),
+ mMutex("AsyncScriptCompileTask") {}
+
+ ~AsyncScriptCompileTask() {
+ if (mFrontendContext) {
+ JS::DestroyFrontendContext(mFrontendContext);
+ }
+ UnregisterTask(this);
+ }
+
+ bool Init(const JS::OwningCompileOptions& aOptions) {
+ if (!RegisterTask(this)) {
+ return false;
+ }
+
+ mFrontendContext = JS::NewFrontendContext();
+ if (!mFrontendContext) {
+ return false;
+ }
+
+ if (!mOptions.copy(mFrontendContext, aOptions)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private:
+ void Compile() {
+ // NOTE: The stack limit must be set from the same thread that compiles.
+ size_t stackSize = TaskController::GetThreadStackSize();
+ JS::SetNativeStackQuota(mFrontendContext,
+ JS::ThreadStackQuotaForSize(stackSize));
+
+ JS::CompilationStorage compileStorage;
+ mStencil = JS::CompileGlobalScriptToStencil(mFrontendContext, mOptions,
+ mSrcBuf, compileStorage);
+ }
+
+ // Cancel the task.
+ // If the task is already running, this waits for the task to finish.
+ void Cancel() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MutexAutoLock lock(mMutex);
+
+ mIsCancelled = true;
+
+ mStencil = nullptr;
+ }
+
+ public:
+ TaskResult Run() override {
+ MutexAutoLock lock(mMutex);
+
+ if (mIsCancelled) {
+ return TaskResult::Complete;
+ }
+
+ Compile();
+ return TaskResult::Complete;
+ }
+
+ already_AddRefed<JS::Stencil> StealStencil(JSContext* aCx) {
+ JS::FrontendContext* fc = mFrontendContext;
+ mFrontendContext = nullptr;
+
+ MOZ_ASSERT(fc);
+
+ if (JS::HadFrontendErrors(fc)) {
+ (void)JS::ConvertFrontendErrorsToRuntimeErrors(aCx, fc, mOptions);
+ JS::DestroyFrontendContext(fc);
+ return nullptr;
+ }
+
+ // Report warnings.
+ if (!JS::ConvertFrontendErrorsToRuntimeErrors(aCx, fc, mOptions)) {
+ JS::DestroyFrontendContext(fc);
+ return nullptr;
+ }
+
+ JS::DestroyFrontendContext(fc);
+
+ return mStencil.forget();
+ }
+
+#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY
+ bool GetName(nsACString& aName) override {
+ aName.AssignLiteral("AsyncScriptCompileTask");
+ return true;
+ }
+#endif
+
+ private:
+ // Owning-pointer for the context associated with the script compilation.
+ //
+ // The context is allocated on main thread in Init method, and is freed on
+ // any thread in the destructor.
+ JS::FrontendContext* mFrontendContext = nullptr;
+
+ JS::OwningCompileOptions mOptions;
+
+ RefPtr<JS::Stencil> mStencil;
+
+ JS::SourceText<Utf8Unit> mSrcBuf;
+
+ // This mutex is locked during running the task or cancelling task.
+ mozilla::Mutex mMutex;
+
+ bool mIsCancelled MOZ_GUARDED_BY(mMutex) = false;
+};
+
+/* static */ mozilla::StaticMutex AsyncScriptCompileTask::sOngoingTasksMutex;
+/* static */ Vector<AsyncScriptCompileTask*>
+ AsyncScriptCompileTask::sOngoingTasks;
+/* static */ bool AsyncScriptCompileTask::sIsShutdownRegistered = false;
+
+class AsyncScriptCompiler;
+
+class AsyncScriptCompilationCompleteTask : public Task {
+ public:
+ AsyncScriptCompilationCompleteTask(AsyncScriptCompiler* aCompiler,
+ AsyncScriptCompileTask* aCompileTask)
+ : Task(Kind::MainThreadOnly, EventQueuePriority::Normal),
+ mCompiler(aCompiler),
+ mCompileTask(aCompileTask) {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY
+ bool GetName(nsACString& aName) override {
+ aName.AssignLiteral("AsyncScriptCompilationCompleteTask");
+ return true;
+ }
+#endif
+
+ TaskResult Run() override;
+
+ private:
+ // NOTE:
+ // This field is main-thread only, and this task shouldn't be freed off
+ // main thread.
+ //
+ // This is guaranteed by not having off-thread tasks which depends on this
+ // task, because otherwise the off-thread task's mDependencies can be the
+ // last reference, which results in freeing this task off main thread.
+ //
+ // If such task is added, this field must be moved to separate storage.
+ RefPtr<AsyncScriptCompiler> mCompiler;
+
+ RefPtr<AsyncScriptCompileTask> mCompileTask;
+};
+
+class AsyncScriptCompiler final : public nsIIncrementalStreamLoaderObserver {
+ public:
+ // Note: References to this class are never held by cycle-collected objects.
+ // If at any point a reference is returned to a caller, please update this
+ // class to implement cycle collection.
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIINCREMENTALSTREAMLOADEROBSERVER
+
+ AsyncScriptCompiler(JSContext* aCx, nsIGlobalObject* aGlobal,
+ const nsACString& aURL, Promise* aPromise)
+ : mOptions(aCx),
+ mURL(aURL),
+ mGlobalObject(aGlobal),
+ mPromise(aPromise),
+ mScriptLength(0) {}
+
+ [[nodiscard]] nsresult Start(JSContext* aCx,
+ const CompileScriptOptionsDictionary& aOptions,
+ nsIPrincipal* aPrincipal);
+
+ void OnCompilationComplete(AsyncScriptCompileTask* aCompileTask);
+
+ protected:
+ virtual ~AsyncScriptCompiler() {
+ if (mPromise->State() == Promise::PromiseState::Pending) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ }
+ }
+
+ private:
+ void Reject(JSContext* aCx);
+ void Reject(JSContext* aCx, const char* aMxg);
+
+ bool StartCompile(JSContext* aCx);
+ bool StartOffThreadCompile(JS::SourceText<Utf8Unit>&& aSrcBuf);
+ void FinishCompile(JSContext* aCx);
+ void Finish(JSContext* aCx, RefPtr<JS::Stencil>&& aStencil);
+
+ OwningCompileOptions mOptions;
+ nsCString mURL;
+ nsCOMPtr<nsIGlobalObject> mGlobalObject;
+ RefPtr<Promise> mPromise;
+ nsString mCharset;
+ UniquePtr<Utf8Unit[], JS::FreePolicy> mScriptText;
+ size_t mScriptLength;
+};
+
+NS_IMPL_ISUPPORTS(AsyncScriptCompiler, nsIIncrementalStreamLoaderObserver)
+
+nsresult AsyncScriptCompiler::Start(
+ JSContext* aCx, const CompileScriptOptionsDictionary& aOptions,
+ nsIPrincipal* aPrincipal) {
+ mCharset = aOptions.mCharset;
+
+ CompileOptions options(aCx);
+ options.setFile(mURL.get()).setNoScriptRval(!aOptions.mHasReturnValue);
+
+ if (!aOptions.mLazilyParse) {
+ options.setForceFullParse();
+ }
+
+ if (NS_WARN_IF(!mOptions.copy(aCx, options))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), mURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIChannel> channel;
+ rv = NS_NewChannel(
+ getter_AddRefs(channel), uri, aPrincipal,
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_INTERNAL_CHROMEUTILS_COMPILED_SCRIPT);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // allow deprecated HTTP request from SystemPrincipal
+ nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
+ loadInfo->SetAllowDeprecatedSystemRequests(true);
+ nsCOMPtr<nsIIncrementalStreamLoader> loader;
+ rv = NS_NewIncrementalStreamLoader(getter_AddRefs(loader), this);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return channel->AsyncOpen(loader);
+}
+
+bool AsyncScriptCompiler::StartCompile(JSContext* aCx) {
+ JS::SourceText<Utf8Unit> srcBuf;
+ if (!srcBuf.init(aCx, std::move(mScriptText), mScriptLength)) {
+ return false;
+ }
+
+ // TODO: This uses the same heuristics and the same threshold as the
+ // JS::CanCompileOffThread, but the heuristics needs to be updated to
+ // reflect the change regarding the Stencil API, and also the thread
+ // management on the consumer side (bug 1846388).
+ static constexpr size_t OffThreadMinimumTextLength = 5 * 1000;
+
+ if (StaticPrefs::javascript_options_parallel_parsing() &&
+ mScriptLength >= OffThreadMinimumTextLength) {
+ if (!StartOffThreadCompile(std::move(srcBuf))) {
+ return false;
+ }
+ return true;
+ }
+
+ RefPtr<Stencil> stencil =
+ JS::CompileGlobalScriptToStencil(aCx, mOptions, srcBuf);
+ if (!stencil) {
+ return false;
+ }
+
+ Finish(aCx, std::move(stencil));
+ return true;
+}
+
+bool AsyncScriptCompiler::StartOffThreadCompile(
+ JS::SourceText<Utf8Unit>&& aSrcBuf) {
+ RefPtr<AsyncScriptCompileTask> compileTask =
+ new AsyncScriptCompileTask(std::move(aSrcBuf));
+
+ RefPtr<AsyncScriptCompilationCompleteTask> complationCompleteTask =
+ new AsyncScriptCompilationCompleteTask(this, compileTask.get());
+
+ if (!compileTask->Init(mOptions)) {
+ return false;
+ }
+
+ complationCompleteTask->AddDependency(compileTask.get());
+
+ TaskController::Get()->AddTask(compileTask.forget());
+ TaskController::Get()->AddTask(complationCompleteTask.forget());
+ return true;
+}
+
+Task::TaskResult AsyncScriptCompilationCompleteTask::Run() {
+ mCompiler->OnCompilationComplete(mCompileTask.get());
+ mCompiler = nullptr;
+ mCompileTask = nullptr;
+ return TaskResult::Complete;
+}
+
+void AsyncScriptCompiler::OnCompilationComplete(
+ AsyncScriptCompileTask* aCompileTask) {
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(mGlobalObject)) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return;
+ }
+
+ JSContext* cx = jsapi.cx();
+ RefPtr<JS::Stencil> stencil = aCompileTask->StealStencil(cx);
+ if (!stencil) {
+ Reject(cx);
+ return;
+ }
+
+ Finish(cx, std::move(stencil));
+ return;
+}
+
+void AsyncScriptCompiler::Finish(JSContext* aCx,
+ RefPtr<JS::Stencil>&& aStencil) {
+ RefPtr<PrecompiledScript> result =
+ new PrecompiledScript(mGlobalObject, aStencil, mOptions);
+
+ mPromise->MaybeResolve(result);
+}
+
+void AsyncScriptCompiler::Reject(JSContext* aCx) {
+ RootedValue value(aCx, JS::UndefinedValue());
+ if (JS_GetPendingException(aCx, &value)) {
+ JS_ClearPendingException(aCx);
+ }
+ mPromise->MaybeReject(value);
+}
+
+void AsyncScriptCompiler::Reject(JSContext* aCx, const char* aMsg) {
+ nsAutoString msg;
+ msg.AppendASCII(aMsg);
+ msg.AppendLiteral(": ");
+ AppendUTF8toUTF16(mURL, msg);
+
+ RootedValue exn(aCx);
+ if (xpc::NonVoidStringToJsval(aCx, msg, &exn)) {
+ JS_SetPendingException(aCx, exn);
+ }
+
+ Reject(aCx);
+}
+
+NS_IMETHODIMP
+AsyncScriptCompiler::OnIncrementalData(nsIIncrementalStreamLoader* aLoader,
+ nsISupports* aContext,
+ uint32_t aDataLength,
+ const uint8_t* aData,
+ uint32_t* aConsumedData) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AsyncScriptCompiler::OnStreamComplete(nsIIncrementalStreamLoader* aLoader,
+ nsISupports* aContext, nsresult aStatus,
+ uint32_t aLength, const uint8_t* aBuf) {
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(mGlobalObject)) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+ JSContext* cx = jsapi.cx();
+
+ if (NS_FAILED(aStatus)) {
+ Reject(cx, "Unable to load script");
+ return NS_OK;
+ }
+
+ nsresult rv = ScriptLoader::ConvertToUTF8(
+ nullptr, aBuf, aLength, mCharset, nullptr, mScriptText, mScriptLength);
+ if (NS_FAILED(rv)) {
+ Reject(cx, "Unable to decode script");
+ return NS_OK;
+ }
+
+ if (!StartCompile(cx)) {
+ Reject(cx);
+ }
+
+ return NS_OK;
+}
+
+namespace mozilla {
+namespace dom {
+
+/* static */
+already_AddRefed<Promise> ChromeUtils::CompileScript(
+ GlobalObject& aGlobal, const nsAString& aURL,
+ const CompileScriptOptionsDictionary& aOptions, ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ MOZ_ASSERT(global);
+
+ RefPtr<Promise> promise = Promise::Create(global, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ NS_ConvertUTF16toUTF8 url(aURL);
+ RefPtr<AsyncScriptCompiler> compiler =
+ new AsyncScriptCompiler(aGlobal.Context(), global, url, promise);
+
+ nsresult rv = compiler->Start(aGlobal.Context(), aOptions,
+ aGlobal.GetSubjectPrincipal());
+ if (NS_FAILED(rv)) {
+ promise->MaybeReject(rv);
+ }
+
+ return promise.forget();
+}
+
+PrecompiledScript::PrecompiledScript(nsISupports* aParent,
+ RefPtr<JS::Stencil> aStencil,
+ JS::ReadOnlyCompileOptions& aOptions)
+ : mParent(aParent),
+ mStencil(aStencil),
+ mURL(aOptions.filename().c_str()),
+ mHasReturnValue(!aOptions.noScriptRval) {
+ MOZ_ASSERT(aParent);
+ MOZ_ASSERT(aStencil);
+#ifdef DEBUG
+ JS::InstantiateOptions options(aOptions);
+ options.assertDefault();
+#endif
+};
+
+void PrecompiledScript::ExecuteInGlobal(JSContext* aCx, HandleObject aGlobal,
+ const ExecuteInGlobalOptions& aOptions,
+ MutableHandleValue aRval,
+ ErrorResult& aRv) {
+ {
+ RootedObject targetObj(aCx, JS_FindCompilationScope(aCx, aGlobal));
+ // Use AutoEntryScript for its ReportException method call.
+ // This will ensure notified any exception happening in the content script
+ // directly to the console, so that exceptions are flagged with the right
+ // innerWindowID. It helps these exceptions to appear in the page's web
+ // console.
+ AutoEntryScript aes(targetObj, "pre-compiled-script execution");
+ JSContext* cx = aes.cx();
+
+ // See assertion in constructor.
+ JS::InstantiateOptions options;
+ Rooted<JSScript*> script(
+ cx, JS::InstantiateGlobalStencil(cx, options, mStencil));
+ if (!script) {
+ aRv.NoteJSContextException(aCx);
+ return;
+ }
+
+ if (!JS_ExecuteScript(cx, script, aRval)) {
+ JS::RootedValue exn(cx);
+ if (aOptions.mReportExceptions) {
+ // Note that ReportException will consume the exception.
+ aes.ReportException();
+ } else {
+ // Set the exception on our caller's cx.
+ aRv.MightThrowJSException();
+ aRv.StealExceptionFromJSContext(cx);
+ }
+ return;
+ }
+ }
+
+ JS_WrapValue(aCx, aRval);
+}
+
+void PrecompiledScript::GetUrl(nsAString& aUrl) { CopyUTF8toUTF16(mURL, aUrl); }
+
+bool PrecompiledScript::HasReturnValue() { return mHasReturnValue; }
+
+JSObject* PrecompiledScript::WrapObject(JSContext* aCx,
+ HandleObject aGivenProto) {
+ return PrecompiledScript_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+bool PrecompiledScript::IsBlackForCC(bool aTracingNeeded) {
+ return (nsCCUncollectableMarker::sGeneration && HasKnownLiveWrapper() &&
+ (!aTracingNeeded || HasNothingToTrace(this)));
+}
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PrecompiledScript, mParent)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PrecompiledScript)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(PrecompiledScript)
+ return tmp->IsBlackForCC(false);
+NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_END
+
+NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_BEGIN(PrecompiledScript)
+ return tmp->IsBlackForCC(true);
+NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END
+
+NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(PrecompiledScript)
+ return tmp->IsBlackForCC(false);
+NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(PrecompiledScript)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(PrecompiledScript)
+
+} // namespace dom
+} // namespace mozilla
diff --git a/js/xpconnect/loader/ComponentUtils.sys.mjs b/js/xpconnect/loader/ComponentUtils.sys.mjs
new file mode 100644
index 0000000000..cfffb96ef7
--- /dev/null
+++ b/js/xpconnect/loader/ComponentUtils.sys.mjs
@@ -0,0 +1,33 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 sts=2 et filetype=javascript
+ * 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/. */
+
+/**
+ * Deprecated utilities for JavaScript components loaded by the JS component
+ * loader.
+ */
+
+const nsIFactoryQI = ChromeUtils.generateQI(["nsIFactory"]);
+
+export var ComponentUtils = {
+ /**
+ * Generates a singleton nsIFactory implementation that can be used as
+ * an argument to nsIComponentRegistrar.registerFactory.
+ * @param aServiceConstructor
+ * Constructor function of the component.
+ */
+ generateSingletonFactory(aServiceConstructor) {
+ return {
+ _instance: null,
+ createInstance(aIID) {
+ if (this._instance === null) {
+ this._instance = new aServiceConstructor();
+ }
+ return this._instance.QueryInterface(aIID);
+ },
+ QueryInterface: nsIFactoryQI,
+ };
+ },
+};
diff --git a/js/xpconnect/loader/IOBuffers.h b/js/xpconnect/loader/IOBuffers.h
new file mode 100644
index 0000000000..e26b0c3bca
--- /dev/null
+++ b/js/xpconnect/loader/IOBuffers.h
@@ -0,0 +1,148 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef IOBuffers_h
+#define IOBuffers_h
+
+#include "mozilla/Assertions.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/EndianUtils.h"
+#include "mozilla/EnumSet.h"
+#include "mozilla/Range.h"
+#include "mozilla/Span.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace loader {
+
+class OutputBuffer {
+ public:
+ OutputBuffer() {}
+
+ uint8_t* write(size_t size) {
+ auto buf = data.AppendElements(size);
+ cursor_ += size;
+ return buf;
+ }
+
+ void codeUint8(const uint8_t& val) { *write(sizeof val) = val; }
+
+ template <typename T>
+ void codeUint8(const EnumSet<T>& val) {
+ // EnumSets are always represented as uint32_t values, so we need to
+ // assert that the value actually fits in a uint8 before writing it.
+ uint32_t value = val.serialize();
+ codeUint8(CheckedUint8(value).value());
+ }
+
+ void codeUint16(const uint16_t& val) {
+ LittleEndian::writeUint16(write(sizeof val), val);
+ }
+
+ void codeUint32(const uint32_t& val) {
+ LittleEndian::writeUint32(write(sizeof val), val);
+ }
+
+ void codeString(const nsCString& str) {
+ auto len = CheckedUint16(str.Length()).value();
+
+ codeUint16(len);
+ memcpy(write(len), str.BeginReading(), len);
+ }
+
+ size_t cursor() const { return cursor_; }
+
+ uint8_t* Get() { return data.Elements(); }
+
+ const uint8_t* Get() const { return data.Elements(); }
+
+ private:
+ nsTArray<uint8_t> data;
+ size_t cursor_ = 0;
+};
+
+class InputBuffer {
+ public:
+ explicit InputBuffer(const Range<uint8_t>& buffer) : data(buffer) {}
+
+ const uint8_t* read(size_t size) {
+ MOZ_ASSERT(checkCapacity(size));
+
+ auto buf = &data[cursor_];
+ cursor_ += size;
+ return buf;
+ }
+
+ bool codeUint8(uint8_t& val) {
+ if (checkCapacity(sizeof val)) {
+ val = *read(sizeof val);
+ }
+ return !error_;
+ }
+
+ template <typename T>
+ bool codeUint8(EnumSet<T>& val) {
+ uint8_t value;
+ if (codeUint8(value)) {
+ val.deserialize(value);
+ }
+ return !error_;
+ }
+
+ bool codeUint16(uint16_t& val) {
+ if (checkCapacity(sizeof val)) {
+ val = LittleEndian::readUint16(read(sizeof val));
+ }
+ return !error_;
+ }
+
+ bool codeUint32(uint32_t& val) {
+ if (checkCapacity(sizeof val)) {
+ val = LittleEndian::readUint32(read(sizeof val));
+ }
+ return !error_;
+ }
+
+ bool codeString(nsCString& str) {
+ uint16_t len;
+ if (codeUint16(len)) {
+ if (checkCapacity(len)) {
+ str.SetLength(len);
+ memcpy(str.BeginWriting(), read(len), len);
+ }
+ }
+ return !error_;
+ }
+
+ bool error() { return error_; }
+
+ bool finished() { return error_ || !remainingCapacity(); }
+
+ size_t remainingCapacity() { return data.length() - cursor_; }
+
+ size_t cursor() const { return cursor_; }
+
+ const uint8_t* Get() const { return data.begin().get(); }
+
+ private:
+ bool checkCapacity(size_t size) {
+ if (size > remainingCapacity()) {
+ error_ = true;
+ }
+ return !error_;
+ }
+
+ bool error_ = false;
+
+ public:
+ const Range<uint8_t>& data;
+ size_t cursor_ = 0;
+};
+
+} // namespace loader
+} // namespace mozilla
+
+#endif // IOBuffers_h
diff --git a/js/xpconnect/loader/JSMEnvironmentProxy.cpp b/js/xpconnect/loader/JSMEnvironmentProxy.cpp
new file mode 100644
index 0000000000..cb21151033
--- /dev/null
+++ b/js/xpconnect/loader/JSMEnvironmentProxy.cpp
@@ -0,0 +1,259 @@
+/* -*- 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 "JSMEnvironmentProxy.h"
+
+#include "mozilla/Assertions.h" // MOZ_ASSERT
+#include "mozilla/Maybe.h" // mozilla::Maybe
+
+#include <stddef.h> // size_t
+
+#include "jsapi.h" // JS_HasExtensibleLexicalEnvironment, JS_ExtensibleLexicalEnvironment
+#include "js/Class.h" // JS::ObjectOpResult
+#include "js/ErrorReport.h" // JS_ReportOutOfMemory
+#include "js/GCVector.h" // JS::RootedVector
+#include "js/Id.h" // JS::PropertyKey
+#include "js/PropertyAndElement.h" // JS::IdVector, JS_HasPropertyById, JS_HasOwnPropertyById, JS_GetPropertyById, JS_Enumerate
+#include "js/PropertyDescriptor.h" // JS::PropertyDescriptor, JS_GetOwnPropertyDescriptorById
+#include "js/Proxy.h" // js::ProxyOptions, js::NewProxyObject, js::GetProxyPrivate
+#include "js/RootingAPI.h" // JS::Rooted, JS::Handle, JS::MutableHandle
+#include "js/TypeDecls.h" // JSContext, JSObject, JS::MutableHandleVector
+#include "js/Value.h" // JS::Value, JS::UndefinedValue, JS_UNINITIALIZED_LEXICAL
+#include "js/friend/ErrorMessages.h" // JSMSG_*
+
+namespace mozilla {
+namespace loader {
+
+struct JSMEnvironmentProxyHandler : public js::BaseProxyHandler {
+ JSMEnvironmentProxyHandler() : BaseProxyHandler(&gFamily, false) {}
+
+ bool defineProperty(JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::Handle<JS::PropertyKey> aId,
+ JS::Handle<JS::PropertyDescriptor> aDesc,
+ JS::ObjectOpResult& aResult) const override {
+ return aResult.fail(JSMSG_CANT_DEFINE_PROP_OBJECT_NOT_EXTENSIBLE);
+ }
+
+ bool getPrototype(JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::MutableHandle<JSObject*> aProtop) const override {
+ aProtop.set(nullptr);
+ return true;
+ }
+
+ bool setPrototype(JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::Handle<JSObject*> aProto,
+ JS::ObjectOpResult& aResult) const override {
+ if (!aProto) {
+ return aResult.succeed();
+ }
+ return aResult.failCantSetProto();
+ }
+
+ bool getPrototypeIfOrdinary(
+ JSContext* aCx, JS::Handle<JSObject*> aProxy, bool* aIsOrdinary,
+ JS::MutableHandle<JSObject*> aProtop) const override {
+ *aIsOrdinary = false;
+ return true;
+ }
+
+ bool setImmutablePrototype(JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ bool* aSucceeded) const override {
+ *aSucceeded = true;
+ return true;
+ }
+
+ bool preventExtensions(JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::ObjectOpResult& aResult) const override {
+ aResult.succeed();
+ return true;
+ }
+
+ bool isExtensible(JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ bool* aExtensible) const override {
+ *aExtensible = false;
+ return true;
+ }
+
+ bool set(JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::Handle<JS::PropertyKey> aId, JS::Handle<JS::Value> aValue,
+ JS::Handle<JS::Value> aReceiver,
+ JS::ObjectOpResult& aResult) const override {
+ return aResult.failReadOnly();
+ }
+
+ bool delete_(JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::Handle<JS::PropertyKey> aId,
+ JS::ObjectOpResult& aResult) const override {
+ return aResult.failCantDelete();
+ }
+
+ bool getOwnPropertyDescriptor(
+ JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::Handle<JS::PropertyKey> aId,
+ JS::MutableHandle<mozilla::Maybe<JS::PropertyDescriptor>> aDesc)
+ const override;
+ bool has(JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::Handle<JS::PropertyKey> aId, bool* aBp) const override;
+ bool get(JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::Handle<JS::Value> aReceiver, JS::Handle<JS::PropertyKey> aId,
+ JS::MutableHandle<JS::Value> aVp) const override;
+ bool ownPropertyKeys(
+ JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::MutableHandleVector<JS::PropertyKey> aProps) const override;
+
+ private:
+ static JSObject* getGlobal(JSContext* aCx, JS::Handle<JSObject*> aProxy) {
+ JS::Rooted<JSObject*> globalObj(aCx,
+ &js::GetProxyPrivate(aProxy).toObject());
+ return globalObj;
+ }
+
+ public:
+ static const char gFamily;
+ static const JSMEnvironmentProxyHandler gHandler;
+};
+
+const JSMEnvironmentProxyHandler JSMEnvironmentProxyHandler::gHandler;
+const char JSMEnvironmentProxyHandler::gFamily = 0;
+
+JSObject* ResolveModuleObjectPropertyById(JSContext* aCx,
+ JS::Handle<JSObject*> aModObj,
+ JS::Handle<JS::PropertyKey> aId) {
+ if (JS_HasExtensibleLexicalEnvironment(aModObj)) {
+ JS::Rooted<JSObject*> lexical(aCx,
+ JS_ExtensibleLexicalEnvironment(aModObj));
+ bool found;
+ if (!JS_HasOwnPropertyById(aCx, lexical, aId, &found)) {
+ return nullptr;
+ }
+ if (found) {
+ return lexical;
+ }
+ }
+ return aModObj;
+}
+
+JSObject* ResolveModuleObjectProperty(JSContext* aCx,
+ JS::Handle<JSObject*> aModObj,
+ const char* aName) {
+ if (JS_HasExtensibleLexicalEnvironment(aModObj)) {
+ JS::RootedObject lexical(aCx, JS_ExtensibleLexicalEnvironment(aModObj));
+ bool found;
+ if (!JS_HasOwnProperty(aCx, lexical, aName, &found)) {
+ return nullptr;
+ }
+ if (found) {
+ return lexical;
+ }
+ }
+ return aModObj;
+}
+
+bool JSMEnvironmentProxyHandler::getOwnPropertyDescriptor(
+ JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::Handle<JS::PropertyKey> aId,
+ JS::MutableHandle<mozilla::Maybe<JS::PropertyDescriptor>> aDesc) const {
+ JS::Rooted<JSObject*> globalObj(aCx, getGlobal(aCx, aProxy));
+ JS::Rooted<JSObject*> holder(
+ aCx, ResolveModuleObjectPropertyById(aCx, globalObj, aId));
+ if (!JS_GetOwnPropertyDescriptorById(aCx, holder, aId, aDesc)) {
+ return false;
+ }
+
+ if (aDesc.get().isNothing()) {
+ return true;
+ }
+
+ JS::PropertyDescriptor& desc = *aDesc.get();
+
+ if (desc.hasValue()) {
+ if (desc.value().isMagic(JS_UNINITIALIZED_LEXICAL)) {
+ desc.setValue(JS::UndefinedValue());
+ }
+ }
+
+ desc.setConfigurable(false);
+ desc.setEnumerable(true);
+ if (!desc.isAccessorDescriptor()) {
+ desc.setWritable(false);
+ }
+
+ return true;
+}
+
+bool JSMEnvironmentProxyHandler::has(JSContext* aCx,
+ JS::Handle<JSObject*> aProxy,
+ JS::Handle<JS::PropertyKey> aId,
+ bool* aBp) const {
+ JS::Rooted<JSObject*> globalObj(aCx, getGlobal(aCx, aProxy));
+ JS::Rooted<JSObject*> holder(
+ aCx, ResolveModuleObjectPropertyById(aCx, globalObj, aId));
+ return JS_HasPropertyById(aCx, holder, aId, aBp);
+}
+
+bool JSMEnvironmentProxyHandler::get(JSContext* aCx,
+ JS::Handle<JSObject*> aProxy,
+ JS::Handle<JS::Value> aReceiver,
+ JS::Handle<JS::PropertyKey> aId,
+ JS::MutableHandle<JS::Value> aVp) const {
+ JS::Rooted<JSObject*> globalObj(aCx, getGlobal(aCx, aProxy));
+ JS::Rooted<JSObject*> holder(
+ aCx, ResolveModuleObjectPropertyById(aCx, globalObj, aId));
+ if (!JS_GetPropertyById(aCx, holder, aId, aVp)) {
+ return false;
+ }
+
+ if (aVp.isMagic(JS_UNINITIALIZED_LEXICAL)) {
+ aVp.setUndefined();
+ }
+
+ return true;
+}
+
+bool JSMEnvironmentProxyHandler::ownPropertyKeys(
+ JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::MutableHandleVector<JS::PropertyKey> aProps) const {
+ JS::Rooted<JSObject*> globalObj(aCx, getGlobal(aCx, aProxy));
+ JS::Rooted<JS::IdVector> globalIds(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, globalObj, &globalIds)) {
+ return false;
+ }
+
+ for (size_t i = 0; i < globalIds.length(); i++) {
+ if (!aProps.append(globalIds[i])) {
+ JS_ReportOutOfMemory(aCx);
+ return false;
+ }
+ }
+
+ JS::RootedObject lexicalEnv(aCx, JS_ExtensibleLexicalEnvironment(globalObj));
+ JS::Rooted<JS::IdVector> lexicalIds(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, lexicalEnv, &lexicalIds)) {
+ return false;
+ }
+
+ for (size_t i = 0; i < lexicalIds.length(); i++) {
+ if (!aProps.append(lexicalIds[i])) {
+ JS_ReportOutOfMemory(aCx);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+JSObject* CreateJSMEnvironmentProxy(JSContext* aCx,
+ JS::Handle<JSObject*> aGlobalObj) {
+ js::ProxyOptions options;
+ options.setLazyProto(true);
+
+ JS::Rooted<JS::Value> globalVal(aCx, JS::ObjectValue(*aGlobalObj));
+ return NewProxyObject(aCx, &JSMEnvironmentProxyHandler::gHandler, globalVal,
+ nullptr, options);
+}
+
+} // namespace loader
+} // namespace mozilla
diff --git a/js/xpconnect/loader/JSMEnvironmentProxy.h b/js/xpconnect/loader/JSMEnvironmentProxy.h
new file mode 100644
index 0000000000..f45d7e3801
--- /dev/null
+++ b/js/xpconnect/loader/JSMEnvironmentProxy.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 mozilla_loader_JSMEnvironmentProxy_h
+#define mozilla_loader_JSMEnvironmentProxy_h
+
+#include "js/Id.h" // JS::PropertyKey
+#include "js/TypeDecls.h" // JSContext, JSObject
+#include "js/RootingAPI.h" // JS::Handle
+
+namespace mozilla {
+namespace loader {
+
+JSObject* ResolveModuleObjectPropertyById(JSContext* aCx,
+ JS::Handle<JSObject*> aModObj,
+ JS::Handle<JS::PropertyKey> aId);
+
+JSObject* ResolveModuleObjectProperty(JSContext* aCx,
+ JS::Handle<JSObject*> aModObj,
+ const char* aName);
+
+JSObject* CreateJSMEnvironmentProxy(JSContext* aCx,
+ JS::Handle<JSObject*> aGlobalObj);
+
+} // namespace loader
+} // namespace mozilla
+
+#endif // mozilla_loader_JSMEnvironmentProxy_h
diff --git a/js/xpconnect/loader/ModuleEnvironmentProxy.cpp b/js/xpconnect/loader/ModuleEnvironmentProxy.cpp
new file mode 100644
index 0000000000..e70fcb3865
--- /dev/null
+++ b/js/xpconnect/loader/ModuleEnvironmentProxy.cpp
@@ -0,0 +1,237 @@
+/* -*- 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 "ModuleEnvironmentProxy.h"
+
+#include "mozilla/Assertions.h" // MOZ_ASSERT
+#include "mozilla/Maybe.h" // mozilla::Maybe
+
+#include <stddef.h> // size_t
+
+#include "js/Class.h" // JS::ObjectOpResult
+#include "js/ErrorReport.h" // JS_ReportOutOfMemory
+#include "js/GCVector.h" // JS::RootedVector
+#include "js/Id.h" // JS::PropertyKey
+#include "js/PropertyAndElement.h" // JS::IdVector, JS_HasPropertyById, JS_GetPropertyById, JS_Enumerate
+#include "js/PropertyDescriptor.h" // JS::PropertyDescriptor, JS_GetOwnPropertyDescriptorById
+#include "js/Proxy.h" // js::ProxyOptions, js::NewProxyObject, js::GetProxyPrivate
+#include "js/RootingAPI.h" // JS::Rooted, JS::Handle, JS::MutableHandle
+#include "js/TypeDecls.h" // JSContext, JSObject, JS::MutableHandleVector
+#include "js/Value.h" // JS::Value
+#include "js/friend/ErrorMessages.h" // JSMSG_*
+#include "js/String.h"
+#include "js/Modules.h"
+
+namespace mozilla {
+namespace loader {
+
+struct ModuleEnvironmentProxyHandler : public js::BaseProxyHandler {
+ ModuleEnvironmentProxyHandler() : js::BaseProxyHandler(&gFamily, false) {}
+
+ bool defineProperty(JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::Handle<JS::PropertyKey> aId,
+ JS::Handle<JS::PropertyDescriptor> aDesc,
+ JS::ObjectOpResult& aResult) const override {
+ return aResult.fail(JSMSG_CANT_DEFINE_PROP_OBJECT_NOT_EXTENSIBLE);
+ }
+
+ bool getPrototype(JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::MutableHandle<JSObject*> aProtop) const override {
+ aProtop.set(nullptr);
+ return true;
+ }
+
+ bool setPrototype(JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::Handle<JSObject*> aProto,
+ JS::ObjectOpResult& aResult) const override {
+ if (!aProto) {
+ return aResult.succeed();
+ }
+ return aResult.failCantSetProto();
+ }
+
+ bool getPrototypeIfOrdinary(
+ JSContext* aCx, JS::Handle<JSObject*> aProxy, bool* aIsOrdinary,
+ JS::MutableHandle<JSObject*> aProtop) const override {
+ *aIsOrdinary = false;
+ return true;
+ }
+
+ bool setImmutablePrototype(JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ bool* aSucceeded) const override {
+ *aSucceeded = true;
+ return true;
+ }
+
+ bool preventExtensions(JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::ObjectOpResult& aResult) const override {
+ aResult.succeed();
+ return true;
+ }
+
+ bool isExtensible(JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ bool* aExtensible) const override {
+ *aExtensible = false;
+ return true;
+ }
+
+ bool set(JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::Handle<JS::PropertyKey> aId, JS::Handle<JS::Value> aValue,
+ JS::Handle<JS::Value> aReceiver,
+ JS::ObjectOpResult& aResult) const override {
+ return aResult.failReadOnly();
+ }
+
+ bool delete_(JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::Handle<JS::PropertyKey> aId,
+ JS::ObjectOpResult& aResult) const override {
+ return aResult.failCantDelete();
+ }
+
+ bool getOwnPropertyDescriptor(
+ JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::Handle<JS::PropertyKey> aId,
+ JS::MutableHandle<mozilla::Maybe<JS::PropertyDescriptor>> aDesc)
+ const override;
+ bool has(JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::Handle<JS::PropertyKey> aId, bool* aBp) const override;
+ bool get(JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::Handle<JS::Value> receiver, JS::Handle<JS::PropertyKey> aId,
+ JS::MutableHandle<JS::Value> aVp) const override;
+ bool ownPropertyKeys(
+ JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::MutableHandleVector<JS::PropertyKey> aProps) const override;
+
+ private:
+ static JSObject* getEnvironment(JS::Handle<JSObject*> aProxy) {
+ return &js::GetProxyPrivate(aProxy).toObject();
+ }
+
+ static bool equalsNamespace(JSContext* aCx, JS::Handle<JS::PropertyKey> aId,
+ bool* aMatch) {
+ if (!aId.isString()) {
+ *aMatch = false;
+ return true;
+ }
+ return JS_StringEqualsLiteral(aCx, aId.toString(), "*namespace*", aMatch);
+ }
+
+ public:
+ static const char gFamily;
+ static const ModuleEnvironmentProxyHandler gHandler;
+};
+
+const ModuleEnvironmentProxyHandler ModuleEnvironmentProxyHandler::gHandler;
+const char ModuleEnvironmentProxyHandler::gFamily = 0;
+
+bool ModuleEnvironmentProxyHandler::getOwnPropertyDescriptor(
+ JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::Handle<JS::PropertyKey> aId,
+ JS::MutableHandle<mozilla::Maybe<JS::PropertyDescriptor>> aDesc) const {
+ bool isNamespace;
+ if (!equalsNamespace(aCx, aId, &isNamespace)) {
+ return false;
+ }
+ if (isNamespace) {
+ aDesc.reset();
+ return true;
+ }
+
+ JS::Rooted<JSObject*> envObj(aCx, getEnvironment(aProxy));
+ if (!JS_GetOwnPropertyDescriptorById(aCx, envObj, aId, aDesc)) {
+ return false;
+ }
+
+ if (aDesc.get().isNothing()) {
+ return true;
+ }
+
+ JS::PropertyDescriptor& desc = *aDesc.get();
+
+ desc.setConfigurable(false);
+ desc.setWritable(false);
+ desc.setEnumerable(true);
+
+ return true;
+}
+
+bool ModuleEnvironmentProxyHandler::has(JSContext* aCx,
+ JS::Handle<JSObject*> aProxy,
+ JS::Handle<JS::PropertyKey> aId,
+ bool* aBp) const {
+ bool isNamespace;
+ if (!equalsNamespace(aCx, aId, &isNamespace)) {
+ return false;
+ }
+ if (isNamespace) {
+ *aBp = false;
+ return true;
+ }
+
+ JS::Rooted<JSObject*> envObj(aCx, getEnvironment(aProxy));
+ return JS_HasOwnPropertyById(aCx, envObj, aId, aBp);
+}
+
+bool ModuleEnvironmentProxyHandler::get(
+ JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::Handle<JS::Value> aReceiver, JS::Handle<JS::PropertyKey> aId,
+ JS::MutableHandle<JS::Value> aVp) const {
+ bool isNamespace;
+ if (!equalsNamespace(aCx, aId, &isNamespace)) {
+ return false;
+ }
+ if (isNamespace) {
+ aVp.setUndefined();
+ return true;
+ }
+
+ JS::Rooted<JSObject*> envObj(aCx, getEnvironment(aProxy));
+ return JS_GetPropertyById(aCx, envObj, aId, aVp);
+}
+
+bool ModuleEnvironmentProxyHandler::ownPropertyKeys(
+ JSContext* aCx, JS::Handle<JSObject*> aProxy,
+ JS::MutableHandleVector<JS::PropertyKey> aProps) const {
+ JS::Rooted<JSObject*> envObj(aCx, getEnvironment(aProxy));
+ JS::Rooted<JS::IdVector> ids(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, envObj, &ids)) {
+ return false;
+ }
+
+ for (size_t i = 0; i < ids.length(); i++) {
+ bool isNamespace;
+ if (!equalsNamespace(aCx, ids[i], &isNamespace)) {
+ return false;
+ }
+ if (isNamespace) {
+ continue;
+ }
+ if (!aProps.append(ids[i])) {
+ JS_ReportOutOfMemory(aCx);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+JSObject* CreateModuleEnvironmentProxy(JSContext* aCx,
+ JS::Handle<JSObject*> aModuleObj) {
+ js::ProxyOptions options;
+ options.setLazyProto(true);
+
+ JS::Rooted<JSObject*> envObj(aCx, JS::GetModuleEnvironment(aCx, aModuleObj));
+ if (!envObj) {
+ return nullptr;
+ }
+
+ JS::Rooted<JS::Value> envVal(aCx, JS::ObjectValue(*envObj));
+ return NewProxyObject(aCx, &ModuleEnvironmentProxyHandler::gHandler, envVal,
+ nullptr, options);
+}
+
+} // namespace loader
+} // namespace mozilla
diff --git a/js/xpconnect/loader/ModuleEnvironmentProxy.h b/js/xpconnect/loader/ModuleEnvironmentProxy.h
new file mode 100644
index 0000000000..d59f6de5b3
--- /dev/null
+++ b/js/xpconnect/loader/ModuleEnvironmentProxy.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_loader_ModuleEnvironmentProxy_h
+#define mozilla_loader_ModuleEnvironmentProxy_h
+
+#include "js/TypeDecls.h" // JSContext, JSObject
+#include "js/RootingAPI.h" // JS::Handle
+
+namespace mozilla {
+namespace loader {
+
+// Create an object that works in the same way as global object returned by
+// `Cu.import`. This proxy exposes all global variables, including lexical
+// variables.
+//
+// This is a temporary workaround to support not-in-tree code that depends on
+// `Cu.import` return value.
+//
+// This will eventually be removed once ESM-ification finishes.
+JSObject* CreateModuleEnvironmentProxy(JSContext* aCx,
+ JS::Handle<JSObject*> aModuleObj);
+
+} // namespace loader
+} // namespace mozilla
+
+#endif // mozilla_loader_ModuleEnvironmentProxy_h
diff --git a/js/xpconnect/loader/PScriptCache.ipdl b/js/xpconnect/loader/PScriptCache.ipdl
new file mode 100644
index 0000000000..e22166c16d
--- /dev/null
+++ b/js/xpconnect/loader/PScriptCache.ipdl
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 8 -*- */
+/* vim: set sw=4 ts=8 et tw=80 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 PContent;
+
+include "mozilla/loader/ScriptCacheActors.h";
+
+using class mozilla::TimeStamp from "mozilla/TimeStamp.h";
+using struct mozilla::void_t from "mozilla/ipc/IPCCore.h";
+
+namespace mozilla {
+namespace loader {
+
+struct ScriptData {
+ nsCString url;
+ nsCString cachePath;
+ TimeStamp loadTime;
+ // This will be an empty array if script data is present in the previous
+ // session's cache.
+ uint8_t[] xdrData;
+};
+
+[ManualDealloc, ChildImpl="ScriptCacheChild", ParentImpl="ScriptCacheParent"]
+protocol PScriptCache
+{
+ manager PContent;
+
+parent:
+ async __delete__(ScriptData[] scripts);
+};
+
+} // namespace loader
+} // namespace mozilla
diff --git a/js/xpconnect/loader/PrecompiledScript.h b/js/xpconnect/loader/PrecompiledScript.h
new file mode 100644
index 0000000000..2b49c05373
--- /dev/null
+++ b/js/xpconnect/loader/PrecompiledScript.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; 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/. */
+
+#ifndef mozilla_dom_PrecompiledScript_h
+#define mozilla_dom_PrecompiledScript_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/PrecompiledScriptBinding.h"
+#include "mozilla/RefPtr.h"
+
+#include "js/experimental/JSStencil.h"
+#include "js/TypeDecls.h"
+
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+namespace JS {
+class ReadOnlyCompileOptions;
+}
+
+namespace mozilla {
+namespace dom {
+class PrecompiledScript : public nsISupports, public nsWrapperCache {
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_SKIPPABLE_WRAPPERCACHE_CLASS(PrecompiledScript)
+
+ explicit PrecompiledScript(nsISupports* aParent, RefPtr<JS::Stencil> aStencil,
+ JS::ReadOnlyCompileOptions& aOptions);
+
+ void ExecuteInGlobal(JSContext* aCx, JS::Handle<JSObject*> aGlobal,
+ const ExecuteInGlobalOptions& aOptions,
+ JS::MutableHandle<JS::Value> aRval, ErrorResult& aRv);
+
+ void GetUrl(nsAString& aUrl);
+
+ bool HasReturnValue();
+
+ nsISupports* GetParentObject() const { return mParent; }
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ protected:
+ virtual ~PrecompiledScript() = default;
+
+ private:
+ bool IsBlackForCC(bool aTracingNeeded);
+
+ nsCOMPtr<nsISupports> mParent;
+
+ RefPtr<JS::Stencil> mStencil;
+ nsCString mURL;
+ const bool mHasReturnValue;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PrecompiledScript_h
diff --git a/js/xpconnect/loader/ScriptCacheActors.cpp b/js/xpconnect/loader/ScriptCacheActors.cpp
new file mode 100644
index 0000000000..9b44f0ffe6
--- /dev/null
+++ b/js/xpconnect/loader/ScriptCacheActors.cpp
@@ -0,0 +1,92 @@
+/* -*- 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/ScriptPreloader.h"
+#include "ScriptPreloader-inl.h"
+#include "mozilla/loader/ScriptCacheActors.h"
+
+#include "mozilla/dom/ContentParent.h"
+
+namespace mozilla {
+namespace loader {
+
+void ScriptCacheChild::Init(const Maybe<FileDescriptor>& cacheFile,
+ bool wantCacheData) {
+ mWantCacheData = wantCacheData;
+
+ auto& cache = ScriptPreloader::GetChildSingleton();
+ Unused << cache.InitCache(cacheFile, this);
+
+ if (!wantCacheData) {
+ // If the parent process isn't expecting any cache data from us, we're
+ // done.
+ Send__delete__(this, AutoTArray<ScriptData, 0>());
+ }
+}
+
+// Finalize the script cache for the content process, and send back data about
+// any scripts executed up to this point.
+void ScriptCacheChild::SendScriptsAndFinalize(
+ ScriptPreloader::ScriptHash& scripts) {
+ MOZ_ASSERT(mWantCacheData);
+
+ AutoSafeJSAPI jsapi;
+
+ auto matcher = ScriptPreloader::Match<ScriptPreloader::ScriptStatus::Saved>();
+
+ nsTArray<ScriptData> dataArray;
+ for (auto& script : IterHash(scripts, matcher)) {
+ if (!script->mSize && !script->XDREncode(jsapi.cx())) {
+ continue;
+ }
+
+ auto data = dataArray.AppendElement();
+
+ data->url() = script->mURL;
+ data->cachePath() = script->mCachePath;
+ data->loadTime() = script->mLoadTime;
+
+ if (script->HasBuffer()) {
+ auto& xdrData = script->Buffer();
+ data->xdrData().AppendElements(xdrData.begin(), xdrData.length());
+ script->FreeData();
+ }
+ }
+
+ Send__delete__(this, dataArray);
+}
+
+void ScriptCacheChild::ActorDestroy(ActorDestroyReason aWhy) {
+ auto& cache = ScriptPreloader::GetChildSingleton();
+ cache.mChildActor = nullptr;
+}
+
+IPCResult ScriptCacheParent::Recv__delete__(nsTArray<ScriptData>&& scripts) {
+ if (!mWantCacheData && scripts.Length()) {
+ return IPC_FAIL(this, "UnexpectedScriptData");
+ }
+
+ // We don't want any more data from the process at this point.
+ mWantCacheData = false;
+
+ // Merge the child's script data with the parent's.
+ auto parent = static_cast<dom::ContentParent*>(Manager());
+ auto processType =
+ ScriptPreloader::GetChildProcessType(parent->GetRemoteType());
+
+ auto& cache = ScriptPreloader::GetChildSingleton();
+ for (auto& script : scripts) {
+ cache.NoteStencil(script.url(), script.cachePath(), processType,
+ std::move(script.xdrData()), script.loadTime());
+ }
+
+ return IPC_OK();
+}
+
+void ScriptCacheParent::ActorDestroy(ActorDestroyReason aWhy) {}
+
+} // namespace loader
+} // namespace mozilla
diff --git a/js/xpconnect/loader/ScriptCacheActors.h b/js/xpconnect/loader/ScriptCacheActors.h
new file mode 100644
index 0000000000..92148464ea
--- /dev/null
+++ b/js/xpconnect/loader/ScriptCacheActors.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef ScriptCache_h
+#define ScriptCache_h
+
+#include "mozilla/ScriptPreloader.h"
+#include "mozilla/loader/PScriptCacheChild.h"
+#include "mozilla/loader/PScriptCacheParent.h"
+
+namespace mozilla {
+namespace ipc {
+class FileDescriptor;
+}
+
+namespace loader {
+
+using mozilla::ipc::FileDescriptor;
+using mozilla::ipc::IPCResult;
+
+class ScriptCacheParent final : public PScriptCacheParent {
+ friend class PScriptCacheParent;
+
+ public:
+ explicit ScriptCacheParent(bool wantCacheData)
+ : mWantCacheData(wantCacheData) {}
+
+ protected:
+ IPCResult Recv__delete__(nsTArray<ScriptData>&& scripts);
+
+ virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ private:
+ bool mWantCacheData;
+};
+
+class ScriptCacheChild final : public PScriptCacheChild {
+ friend class mozilla::ScriptPreloader;
+
+ public:
+ ScriptCacheChild() = default;
+
+ void Init(const Maybe<FileDescriptor>& cacheFile, bool wantCacheData);
+
+ protected:
+ virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ void SendScriptsAndFinalize(ScriptPreloader::ScriptHash& scripts);
+
+ private:
+ bool mWantCacheData = false;
+};
+
+} // namespace loader
+} // namespace mozilla
+
+#endif // ScriptCache_h
diff --git a/js/xpconnect/loader/ScriptPreloader-inl.h b/js/xpconnect/loader/ScriptPreloader-inl.h
new file mode 100644
index 0000000000..5908600616
--- /dev/null
+++ b/js/xpconnect/loader/ScriptPreloader-inl.h
@@ -0,0 +1,167 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef ScriptPreloader_inl_h
+#define ScriptPreloader_inl_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/EndianUtils.h"
+#include "mozilla/EnumSet.h"
+#include "mozilla/Range.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+#include <prio.h>
+
+namespace mozilla {
+
+namespace loader {
+
+using mozilla::dom::AutoJSAPI;
+
+static inline Result<Ok, nsresult> Write(PRFileDesc* fd, const void* data,
+ int32_t len) {
+ if (PR_Write(fd, data, len) != len) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ return Ok();
+}
+
+static inline Result<Ok, nsresult> WritePadding(PRFileDesc* fd,
+ uint8_t padding) {
+ static const char paddingBytes[8] = "PADBYTE";
+ MOZ_DIAGNOSTIC_ASSERT(padding <= sizeof(paddingBytes));
+
+ if (padding == 0) {
+ return Ok();
+ }
+
+ if (PR_Write(fd, static_cast<const void*>(paddingBytes), padding) !=
+ padding) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ return Ok();
+}
+
+struct MOZ_RAII AutoSafeJSAPI : public AutoJSAPI {
+ AutoSafeJSAPI() { Init(); }
+};
+
+template <typename T>
+struct Matcher;
+
+// Wraps the iterator for a nsTHashTable so that it may be used as a range
+// iterator. Each iterator result acts as a smart pointer to the hash element,
+// and has a Remove() method which will remove the element from the hash.
+//
+// It also accepts an optional Matcher instance against which to filter the
+// elements which should be iterated over.
+//
+// Example:
+//
+// for (auto& elem : HashElemIter<HashType>(hash)) {
+// if (elem->IsDead()) {
+// elem.Remove();
+// }
+// }
+template <typename T>
+class HashElemIter {
+ using Iterator = typename T::Iterator;
+ using ElemType = typename T::UserDataType;
+
+ T& hash_;
+ Matcher<ElemType>* matcher_;
+ Iterator iter_;
+
+ public:
+ explicit HashElemIter(T& hash, Matcher<ElemType>* matcher = nullptr)
+ : hash_(hash), matcher_(matcher), iter_(hash.Iter()) {}
+
+ class Elem {
+ friend class HashElemIter<T>;
+
+ HashElemIter<T>& iter_;
+ bool done_;
+
+ Elem(HashElemIter& iter, bool done) : iter_(iter), done_(done) {
+ skipNonMatching();
+ }
+
+ Iterator& iter() { return iter_.iter_; }
+
+ void skipNonMatching() {
+ if (iter_.matcher_) {
+ while (!done_ && !iter_.matcher_->Matches(get())) {
+ iter().Next();
+ done_ = iter().Done();
+ }
+ }
+ }
+
+ public:
+ Elem& operator*() { return *this; }
+
+ ElemType get() {
+ if (done_) {
+ return nullptr;
+ }
+ return iter().UserData();
+ }
+
+ const ElemType get() const { return const_cast<Elem*>(this)->get(); }
+
+ ElemType operator->() { return get(); }
+
+ const ElemType operator->() const { return get(); }
+
+ operator ElemType() { return get(); }
+
+ void Remove() { iter().Remove(); }
+
+ Elem& operator++() {
+ MOZ_ASSERT(!done_);
+
+ iter().Next();
+ done_ = iter().Done();
+
+ skipNonMatching();
+ return *this;
+ }
+
+ bool operator!=(Elem& other) const {
+ return done_ != other.done_ || this->get() != other.get();
+ }
+ };
+
+ Elem begin() { return Elem(*this, iter_.Done()); }
+
+ Elem end() { return Elem(*this, true); }
+};
+
+template <typename T>
+HashElemIter<T> IterHash(T& hash,
+ Matcher<typename T::UserDataType>* matcher = nullptr) {
+ return HashElemIter<T>(hash, matcher);
+}
+
+template <typename T, typename F>
+bool Find(T&& iter, F&& match) {
+ for (auto& elem : iter) {
+ if (match(elem)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+}; // namespace loader
+}; // namespace mozilla
+
+#endif // ScriptPreloader_inl_h
diff --git a/js/xpconnect/loader/ScriptPreloader.cpp b/js/xpconnect/loader/ScriptPreloader.cpp
new file mode 100644
index 0000000000..fe90dfcf2d
--- /dev/null
+++ b/js/xpconnect/loader/ScriptPreloader.cpp
@@ -0,0 +1,1375 @@
+/* -*- 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 "ScriptPreloader-inl.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Monitor.h"
+
+#include "mozilla/ScriptPreloader.h"
+#include "mozilla/loader/ScriptCacheActors.h"
+
+#include "mozilla/URLPreloader.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Components.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/FileUtils.h"
+#include "mozilla/IOBuffers.h"
+#include "mozilla/Logging.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_javascript.h"
+#include "mozilla/TaskController.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Try.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/scache/StartupCache.h"
+
+#include "crc32c.h"
+#include "js/CompileOptions.h" // JS::ReadOnlyCompileOptions
+#include "js/experimental/JSStencil.h" // JS::Stencil, JS::DecodeStencil
+#include "js/experimental/CompileScript.h" // JS::NewFrontendContext, JS::DestroyFrontendContext, JS::SetNativeStackQuota, JS::ThreadStackQuotaForSize
+#include "js/Transcoding.h"
+#include "MainThreadUtils.h"
+#include "nsDebug.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsIFile.h"
+#include "nsIObserverService.h"
+#include "nsJSUtils.h"
+#include "nsMemoryReporterManager.h"
+#include "nsNetUtil.h"
+#include "nsProxyRelease.h"
+#include "nsThreadUtils.h"
+#include "nsXULAppAPI.h"
+#include "xpcpublic.h"
+
+#define STARTUP_COMPLETE_TOPIC "browser-delayed-startup-finished"
+#define DOC_ELEM_INSERTED_TOPIC "document-element-inserted"
+#define CONTENT_DOCUMENT_LOADED_TOPIC "content-document-loaded"
+#define CACHE_WRITE_TOPIC "browser-idle-startup-tasks-finished"
+#define XPCOM_SHUTDOWN_TOPIC "xpcom-shutdown"
+#define CACHE_INVALIDATE_TOPIC "startupcache-invalidate"
+
+// The maximum time we'll wait for a child process to finish starting up before
+// we send its script data back to the parent.
+constexpr uint32_t CHILD_STARTUP_TIMEOUT_MS = 8000;
+
+namespace mozilla {
+namespace {
+static LazyLogModule gLog("ScriptPreloader");
+
+#define LOG(level, ...) MOZ_LOG(gLog, LogLevel::level, (__VA_ARGS__))
+} // namespace
+
+using mozilla::dom::AutoJSAPI;
+using mozilla::dom::ContentChild;
+using mozilla::dom::ContentParent;
+using namespace mozilla::loader;
+using mozilla::scache::StartupCache;
+
+using namespace JS;
+
+ProcessType ScriptPreloader::sProcessType;
+
+nsresult ScriptPreloader::CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnonymize) {
+ MOZ_COLLECT_REPORT(
+ "explicit/script-preloader/heap/saved-scripts", KIND_HEAP, UNITS_BYTES,
+ SizeOfHashEntries<ScriptStatus::Saved>(mScripts, MallocSizeOf),
+ "Memory used to hold the scripts which have been executed in this "
+ "session, and will be written to the startup script cache file.");
+
+ MOZ_COLLECT_REPORT(
+ "explicit/script-preloader/heap/restored-scripts", KIND_HEAP, UNITS_BYTES,
+ SizeOfHashEntries<ScriptStatus::Restored>(mScripts, MallocSizeOf),
+ "Memory used to hold the scripts which have been restored from the "
+ "startup script cache file, but have not been executed in this session.");
+
+ MOZ_COLLECT_REPORT("explicit/script-preloader/heap/other", KIND_HEAP,
+ UNITS_BYTES, ShallowHeapSizeOfIncludingThis(MallocSizeOf),
+ "Memory used by the script cache service itself.");
+
+ // Since the mem-mapped cache file is mapped into memory, we want to report
+ // it as explicit memory somewhere. But since the child cache is shared
+ // between all processes, we don't want to report it as explicit memory for
+ // all of them. So we report it as explicit only in the parent process, and
+ // non-explicit everywhere else.
+ if (XRE_IsParentProcess()) {
+ MOZ_COLLECT_REPORT("explicit/script-preloader/non-heap/memmapped-cache",
+ KIND_NONHEAP, UNITS_BYTES,
+ mCacheData->nonHeapSizeOfExcludingThis(),
+ "The memory-mapped startup script cache file.");
+ } else {
+ MOZ_COLLECT_REPORT("script-preloader-memmapped-cache", KIND_NONHEAP,
+ UNITS_BYTES, mCacheData->nonHeapSizeOfExcludingThis(),
+ "The memory-mapped startup script cache file.");
+ }
+
+ return NS_OK;
+}
+
+StaticRefPtr<ScriptPreloader> ScriptPreloader::gScriptPreloader;
+StaticRefPtr<ScriptPreloader> ScriptPreloader::gChildScriptPreloader;
+StaticAutoPtr<AutoMemMap> ScriptPreloader::gCacheData;
+StaticAutoPtr<AutoMemMap> ScriptPreloader::gChildCacheData;
+
+ScriptPreloader& ScriptPreloader::GetSingleton() {
+ if (!gScriptPreloader) {
+ if (XRE_IsParentProcess()) {
+ gCacheData = new AutoMemMap();
+ gScriptPreloader = new ScriptPreloader(gCacheData.get());
+ gScriptPreloader->mChildCache = &GetChildSingleton();
+ Unused << gScriptPreloader->InitCache();
+ } else {
+ gScriptPreloader = &GetChildSingleton();
+ }
+ }
+
+ return *gScriptPreloader;
+}
+
+// The child singleton is available in all processes, including the parent, and
+// is used for scripts which are expected to be loaded into child processes
+// (such as process and frame scripts), or scripts that have already been loaded
+// into a child. The child caches are managed as follows:
+//
+// - Every startup, we open the cache file from the last session, move it to a
+// new location, and begin pre-loading the scripts that are stored in it. There
+// is a separate cache file for parent and content processes, but the parent
+// process opens both the parent and content cache files.
+//
+// - Once startup is complete, we write a new cache file for the next session,
+// containing only the scripts that were used during early startup, so we
+// don't waste pre-loading scripts that may not be needed.
+//
+// - For content processes, opening and writing the cache file is handled in the
+// parent process. The first content process of each type sends back the data
+// for scripts that were loaded in early startup, and the parent merges them
+// and writes them to a cache file.
+//
+// - Currently, content processes only benefit from the cache data written
+// during the *previous* session. Ideally, new content processes should
+// probably use the cache data written during this session if there was no
+// previous cache file, but I'd rather do that as a follow-up.
+ScriptPreloader& ScriptPreloader::GetChildSingleton() {
+ if (!gChildScriptPreloader) {
+ gChildCacheData = new AutoMemMap();
+ gChildScriptPreloader = new ScriptPreloader(gChildCacheData.get());
+ if (XRE_IsParentProcess()) {
+ Unused << gChildScriptPreloader->InitCache(u"scriptCache-child"_ns);
+ }
+ }
+
+ return *gChildScriptPreloader;
+}
+
+/* static */
+void ScriptPreloader::DeleteSingleton() {
+ gScriptPreloader = nullptr;
+ gChildScriptPreloader = nullptr;
+}
+
+/* static */
+void ScriptPreloader::DeleteCacheDataSingleton() {
+ MOZ_ASSERT(!gScriptPreloader);
+ MOZ_ASSERT(!gChildScriptPreloader);
+
+ gCacheData = nullptr;
+ gChildCacheData = nullptr;
+}
+
+void ScriptPreloader::InitContentChild(ContentParent& parent) {
+ auto& cache = GetChildSingleton();
+ cache.mSaveMonitor.AssertOnWritingThread();
+
+ // We want startup script data from the first process of a given type.
+ // That process sends back its script data before it executes any
+ // untrusted code, and then we never accept further script data for that
+ // type of process for the rest of the session.
+ //
+ // The script data from each process type is merged with the data from the
+ // parent process's frame and process scripts, and shared between all
+ // content process types in the next session.
+ //
+ // Note that if the first process of a given type crashes or shuts down
+ // before sending us its script data, we silently ignore it, and data for
+ // that process type is not included in the next session's cache. This
+ // should be a sufficiently rare occurrence that it's not worth trying to
+ // handle specially.
+ auto processType = GetChildProcessType(parent.GetRemoteType());
+ bool wantScriptData = !cache.mInitializedProcesses.contains(processType);
+ cache.mInitializedProcesses += processType;
+
+ auto fd = cache.mCacheData->cloneFileDescriptor();
+ // Don't send original cache data to new processes if the cache has been
+ // invalidated.
+ if (fd.IsValid() && !cache.mCacheInvalidated) {
+ Unused << parent.SendPScriptCacheConstructor(fd, wantScriptData);
+ } else {
+ Unused << parent.SendPScriptCacheConstructor(NS_ERROR_FILE_NOT_FOUND,
+ wantScriptData);
+ }
+}
+
+ProcessType ScriptPreloader::GetChildProcessType(const nsACString& remoteType) {
+ if (remoteType == EXTENSION_REMOTE_TYPE) {
+ return ProcessType::Extension;
+ }
+ if (remoteType == PRIVILEGEDABOUT_REMOTE_TYPE) {
+ return ProcessType::PrivilegedAbout;
+ }
+ return ProcessType::Web;
+}
+
+ScriptPreloader::ScriptPreloader(AutoMemMap* cacheData)
+ : mCacheData(cacheData),
+ mMonitor("[ScriptPreloader.mMonitor]"),
+ mSaveMonitor("[ScriptPreloader.mSaveMonitor]", this) {
+ // We do not set the process type for child processes here because the
+ // remoteType in ContentChild is not ready yet.
+ if (XRE_IsParentProcess()) {
+ sProcessType = ProcessType::Parent;
+ }
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ MOZ_RELEASE_ASSERT(obs);
+
+ if (XRE_IsParentProcess()) {
+ // In the parent process, we want to freeze the script cache as soon
+ // as idle tasks for the first browser window have completed.
+ obs->AddObserver(this, STARTUP_COMPLETE_TOPIC, false);
+ obs->AddObserver(this, CACHE_WRITE_TOPIC, false);
+ }
+
+ obs->AddObserver(this, XPCOM_SHUTDOWN_TOPIC, false);
+ obs->AddObserver(this, CACHE_INVALIDATE_TOPIC, false);
+}
+
+ScriptPreloader::~ScriptPreloader() { Cleanup(); }
+
+void ScriptPreloader::Cleanup() {
+ mScripts.Clear();
+ UnregisterWeakMemoryReporter(this);
+}
+
+void ScriptPreloader::StartCacheWrite() {
+ MOZ_DIAGNOSTIC_ASSERT(!mSaveThread);
+
+ Unused << NS_NewNamedThread("SaveScripts", getter_AddRefs(mSaveThread), this);
+
+ nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier();
+ barrier->AddBlocker(this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__,
+ u""_ns);
+}
+
+void ScriptPreloader::InvalidateCache() {
+ {
+ mMonitor.AssertNotCurrentThreadOwns();
+ MonitorAutoLock mal(mMonitor);
+
+ // Wait for pending off-thread parses to finish, since they depend on the
+ // memory allocated by our CachedStencil, and can't be canceled
+ // asynchronously.
+ FinishPendingParses(mal);
+
+ // Pending scripts should have been cleared by the above, and the queue
+ // should have been reset.
+ MOZ_ASSERT(mDecodingScripts.isEmpty());
+ MOZ_ASSERT(!mDecodedStencils);
+
+ mScripts.Clear();
+
+ // If we've already finished saving the cache at this point, start a new
+ // delayed save operation. This will write out an empty cache file in place
+ // of any cache file we've already written out this session, which will
+ // prevent us from falling back to the current session's cache file on the
+ // next startup.
+ if (mSaveComplete && !mSaveThread && mChildCache) {
+ mSaveComplete = false;
+
+ StartCacheWrite();
+ }
+ }
+
+ {
+ MonitorSingleWriterAutoLock saveMonitorAutoLock(mSaveMonitor);
+
+ mCacheInvalidated = true;
+ }
+
+ // If we're waiting on a timeout to finish saving, interrupt it and just save
+ // immediately.
+ mSaveMonitor.NotifyAll();
+}
+
+nsresult ScriptPreloader::Observe(nsISupports* subject, const char* topic,
+ const char16_t* data) {
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (!strcmp(topic, STARTUP_COMPLETE_TOPIC)) {
+ obs->RemoveObserver(this, STARTUP_COMPLETE_TOPIC);
+
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ mStartupFinished = true;
+ URLPreloader::GetSingleton().SetStartupFinished();
+ } else if (!strcmp(topic, CACHE_WRITE_TOPIC)) {
+ obs->RemoveObserver(this, CACHE_WRITE_TOPIC);
+
+ MOZ_ASSERT(mStartupFinished);
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ if (mChildCache && !mSaveComplete && !mSaveThread) {
+ StartCacheWrite();
+ }
+ } else if (mContentStartupFinishedTopic.Equals(topic)) {
+ // If this is an uninitialized about:blank viewer or a chrome: document
+ // (which should always be an XBL binding document), ignore it. We don't
+ // have to worry about it loading malicious content.
+ if (nsCOMPtr<dom::Document> doc = do_QueryInterface(subject)) {
+ nsCOMPtr<nsIURI> uri = doc->GetDocumentURI();
+
+ if ((NS_IsAboutBlank(uri) &&
+ doc->GetReadyStateEnum() == doc->READYSTATE_UNINITIALIZED) ||
+ uri->SchemeIs("chrome")) {
+ return NS_OK;
+ }
+ }
+ FinishContentStartup();
+ } else if (!strcmp(topic, "timer-callback")) {
+ FinishContentStartup();
+ } else if (!strcmp(topic, XPCOM_SHUTDOWN_TOPIC)) {
+ // Wait for any pending parses to finish at this point, to avoid creating
+ // new stencils during destroying the JS runtime.
+ MonitorAutoLock mal(mMonitor);
+ FinishPendingParses(mal);
+ } else if (!strcmp(topic, CACHE_INVALIDATE_TOPIC)) {
+ InvalidateCache();
+ }
+
+ return NS_OK;
+}
+
+void ScriptPreloader::FinishContentStartup() {
+ MOZ_ASSERT(XRE_IsContentProcess());
+
+#ifdef DEBUG
+ if (mContentStartupFinishedTopic.Equals(CONTENT_DOCUMENT_LOADED_TOPIC)) {
+ MOZ_ASSERT(sProcessType == ProcessType::PrivilegedAbout);
+ } else {
+ MOZ_ASSERT(sProcessType != ProcessType::PrivilegedAbout);
+ }
+#endif /* DEBUG */
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ obs->RemoveObserver(this, mContentStartupFinishedTopic.get());
+
+ mSaveTimer = nullptr;
+
+ mStartupFinished = true;
+
+ if (mChildActor) {
+ mChildActor->SendScriptsAndFinalize(mScripts);
+ }
+
+#ifdef XP_WIN
+ // Record the amount of USS at startup. This is Windows-only for now,
+ // we could turn it on for Linux relatively cheaply. On macOS it can have
+ // a perf impact. Only record this for non-privileged processes because
+ // privileged processes record this value at a different time, leading to
+ // a higher value which skews the telemetry.
+ if (sProcessType != ProcessType::PrivilegedAbout) {
+ mozilla::Telemetry::Accumulate(
+ mozilla::Telemetry::MEMORY_UNIQUE_CONTENT_STARTUP,
+ nsMemoryReporterManager::ResidentUnique() / 1024);
+ }
+#endif
+}
+
+bool ScriptPreloader::WillWriteScripts() {
+ return !mDataPrepared && (XRE_IsParentProcess() || mChildActor);
+}
+
+Result<nsCOMPtr<nsIFile>, nsresult> ScriptPreloader::GetCacheFile(
+ const nsAString& suffix) {
+ NS_ENSURE_TRUE(mProfD, Err(NS_ERROR_NOT_INITIALIZED));
+
+ nsCOMPtr<nsIFile> cacheFile;
+ MOZ_TRY(mProfD->Clone(getter_AddRefs(cacheFile)));
+
+ MOZ_TRY(cacheFile->AppendNative("startupCache"_ns));
+ Unused << cacheFile->Create(nsIFile::DIRECTORY_TYPE, 0777);
+
+ MOZ_TRY(cacheFile->Append(mBaseName + suffix));
+
+ return std::move(cacheFile);
+}
+
+static const uint8_t MAGIC[] = "mozXDRcachev003";
+
+Result<Ok, nsresult> ScriptPreloader::OpenCache() {
+ if (StartupCache::GetIgnoreDiskCache()) {
+ return Err(NS_ERROR_ABORT);
+ }
+
+ MOZ_TRY(NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(mProfD)));
+
+ nsCOMPtr<nsIFile> cacheFile;
+ MOZ_TRY_VAR(cacheFile, GetCacheFile(u".bin"_ns));
+
+ bool exists;
+ MOZ_TRY(cacheFile->Exists(&exists));
+ if (exists) {
+ MOZ_TRY(cacheFile->MoveTo(nullptr, mBaseName + u"-current.bin"_ns));
+ } else {
+ MOZ_TRY(cacheFile->SetLeafName(mBaseName + u"-current.bin"_ns));
+ MOZ_TRY(cacheFile->Exists(&exists));
+ if (!exists) {
+ return Err(NS_ERROR_FILE_NOT_FOUND);
+ }
+ }
+
+ MOZ_TRY(mCacheData->init(cacheFile));
+
+ return Ok();
+}
+
+// Opens the script cache file for this session, and initializes the script
+// cache based on its contents. See WriteCache for details of the cache file.
+Result<Ok, nsresult> ScriptPreloader::InitCache(const nsAString& basePath) {
+ mSaveMonitor.AssertOnWritingThread();
+ mCacheInitialized = true;
+ mBaseName = basePath;
+
+ RegisterWeakMemoryReporter(this);
+
+ if (!XRE_IsParentProcess()) {
+ return Ok();
+ }
+
+ // Grab the compilation scope before initializing the URLPreloader, since
+ // it's not safe to run component loader code during its critical section.
+ AutoSafeJSAPI jsapi;
+ JS::RootedObject scope(jsapi.cx(), xpc::CompilationScope());
+
+ // Note: Code on the main thread *must not access Omnijar in any way* until
+ // this AutoBeginReading guard is destroyed.
+ URLPreloader::AutoBeginReading abr;
+
+ MOZ_TRY(OpenCache());
+
+ return InitCacheInternal(scope);
+}
+
+Result<Ok, nsresult> ScriptPreloader::InitCache(
+ const Maybe<ipc::FileDescriptor>& cacheFile, ScriptCacheChild* cacheChild) {
+ mSaveMonitor.AssertOnWritingThread();
+ MOZ_ASSERT(XRE_IsContentProcess());
+
+ mCacheInitialized = true;
+ mChildActor = cacheChild;
+ sProcessType =
+ GetChildProcessType(dom::ContentChild::GetSingleton()->GetRemoteType());
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ MOZ_RELEASE_ASSERT(obs);
+
+ if (sProcessType == ProcessType::PrivilegedAbout) {
+ // Since we control all of the documents loaded in the privileged
+ // content process, we can increase the window of active time for the
+ // ScriptPreloader to include the scripts that are loaded until the
+ // first document finishes loading.
+ mContentStartupFinishedTopic.AssignLiteral(CONTENT_DOCUMENT_LOADED_TOPIC);
+ } else {
+ // In the child process, we need to freeze the script cache before any
+ // untrusted code has been executed. The insertion of the first DOM
+ // document element may sometimes be earlier than is ideal, but at
+ // least it should always be safe.
+ mContentStartupFinishedTopic.AssignLiteral(DOC_ELEM_INSERTED_TOPIC);
+ }
+ obs->AddObserver(this, mContentStartupFinishedTopic.get(), false);
+
+ RegisterWeakMemoryReporter(this);
+
+ auto cleanup = MakeScopeExit([&] {
+ // If the parent is expecting cache data from us, make sure we send it
+ // before it writes out its cache file. For normal proceses, this isn't
+ // a concern, since they begin loading documents quite early. For the
+ // preloaded process, we may end up waiting a long time (or, indeed,
+ // never loading a document), so we need an additional timeout.
+ if (cacheChild) {
+ NS_NewTimerWithObserver(getter_AddRefs(mSaveTimer), this,
+ CHILD_STARTUP_TIMEOUT_MS,
+ nsITimer::TYPE_ONE_SHOT);
+ }
+ });
+
+ if (cacheFile.isNothing()) {
+ return Ok();
+ }
+
+ MOZ_TRY(mCacheData->init(cacheFile.ref()));
+
+ return InitCacheInternal();
+}
+
+Result<Ok, nsresult> ScriptPreloader::InitCacheInternal(
+ JS::HandleObject scope) {
+ auto size = mCacheData->size();
+
+ uint32_t headerSize;
+ uint32_t crc;
+ if (size < sizeof(MAGIC) + sizeof(headerSize) + sizeof(crc)) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ auto data = mCacheData->get<uint8_t>();
+ MOZ_RELEASE_ASSERT(JS::IsTranscodingBytecodeAligned(data.get()));
+
+ auto end = data + size;
+
+ if (memcmp(MAGIC, data.get(), sizeof(MAGIC))) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+ data += sizeof(MAGIC);
+
+ headerSize = LittleEndian::readUint32(data.get());
+ data += sizeof(headerSize);
+
+ crc = LittleEndian::readUint32(data.get());
+ data += sizeof(crc);
+
+ if (data + headerSize > end) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ if (crc != ComputeCrc32c(~0, data.get(), headerSize)) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ {
+ auto cleanup = MakeScopeExit([&]() { mScripts.Clear(); });
+
+ LinkedList<CachedStencil> scripts;
+
+ Range<uint8_t> header(data, data + headerSize);
+ data += headerSize;
+
+ // Reconstruct alignment padding if required.
+ size_t currentOffset = data - mCacheData->get<uint8_t>();
+ data += JS::AlignTranscodingBytecodeOffset(currentOffset) - currentOffset;
+
+ InputBuffer buf(header);
+
+ size_t offset = 0;
+ while (!buf.finished()) {
+ auto script = MakeUnique<CachedStencil>(*this, buf);
+ MOZ_RELEASE_ASSERT(script);
+
+ auto scriptData = data + script->mOffset;
+ if (!JS::IsTranscodingBytecodeAligned(scriptData.get())) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ if (scriptData + script->mSize > end) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ // Make sure offsets match what we'd expect based on script ordering and
+ // size, as a basic sanity check.
+ if (script->mOffset != offset) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+ offset += script->mSize;
+
+ script->mXDRRange.emplace(scriptData, scriptData + script->mSize);
+
+ // Don't pre-decode the script unless it was used in this process type
+ // during the previous session.
+ if (script->mOriginalProcessTypes.contains(CurrentProcessType())) {
+ scripts.insertBack(script.get());
+ } else {
+ script->mReadyToExecute = true;
+ }
+
+ const auto& cachePath = script->mCachePath;
+ mScripts.InsertOrUpdate(cachePath, std::move(script));
+ }
+
+ if (buf.error()) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ mDecodingScripts = std::move(scripts);
+ cleanup.release();
+ }
+
+ StartDecodeTask(scope);
+ return Ok();
+}
+
+void ScriptPreloader::PrepareCacheWriteInternal() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mMonitor.AssertCurrentThreadOwns();
+
+ auto cleanup = MakeScopeExit([&]() {
+ if (mChildCache) {
+ mChildCache->PrepareCacheWrite();
+ }
+ });
+
+ if (mDataPrepared) {
+ return;
+ }
+
+ AutoSafeJSAPI jsapi;
+ JSAutoRealm ar(jsapi.cx(), xpc::PrivilegedJunkScope());
+ bool found = false;
+ for (auto& script : IterHash(mScripts, Match<ScriptStatus::Saved>())) {
+ // Don't write any scripts that are also in the child cache. They'll be
+ // loaded from the child cache in that case, so there's no need to write
+ // them twice.
+ CachedStencil* childScript =
+ mChildCache ? mChildCache->mScripts.Get(script->mCachePath) : nullptr;
+ if (childScript && !childScript->mProcessTypes.isEmpty()) {
+ childScript->UpdateLoadTime(script->mLoadTime);
+ childScript->mProcessTypes += script->mProcessTypes;
+ script.Remove();
+ continue;
+ }
+
+ if (!(script->mProcessTypes == script->mOriginalProcessTypes)) {
+ // Note: EnumSet doesn't support operator!=, hence the weird form above.
+ found = true;
+ }
+
+ if (!script->mSize && !script->XDREncode(jsapi.cx())) {
+ script.Remove();
+ }
+ }
+
+ if (!found) {
+ mSaveComplete = true;
+ return;
+ }
+
+ mDataPrepared = true;
+}
+
+void ScriptPreloader::PrepareCacheWrite() {
+ MonitorAutoLock mal(mMonitor);
+
+ PrepareCacheWriteInternal();
+}
+
+// Writes out a script cache file for the scripts accessed during early
+// startup in this session. The cache file is a little-endian binary file with
+// the following format:
+//
+// - A uint32 containing the size of the header block.
+//
+// - A header entry for each file stored in the cache containing:
+// - The URL that the script was originally read from.
+// - Its cache key.
+// - The offset of its XDR data within the XDR data block.
+// - The size of its XDR data in the XDR data block.
+// - A bit field describing which process types the script is used in.
+//
+// - A block of XDR data for the encoded scripts, with each script's data at
+// an offset from the start of the block, as specified above.
+Result<Ok, nsresult> ScriptPreloader::WriteCache() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ mSaveMonitor.AssertCurrentThreadOwns();
+
+ if (!mDataPrepared && !mSaveComplete) {
+ MonitorSingleWriterAutoUnlock mau(mSaveMonitor);
+
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "ScriptPreloader::PrepareCacheWrite"_ns,
+ GetMainThreadSerialEventTarget(),
+ NewRunnableMethod("ScriptPreloader::PrepareCacheWrite", this,
+ &ScriptPreloader::PrepareCacheWrite));
+ }
+
+ if (mSaveComplete) {
+ // If we don't have anything we need to save, we're done.
+ return Ok();
+ }
+
+ nsCOMPtr<nsIFile> cacheFile;
+ MOZ_TRY_VAR(cacheFile, GetCacheFile(u"-new.bin"_ns));
+
+ bool exists;
+ MOZ_TRY(cacheFile->Exists(&exists));
+ if (exists) {
+ MOZ_TRY(cacheFile->Remove(false));
+ }
+
+ {
+ AutoFDClose raiiFd;
+ MOZ_TRY(cacheFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE, 0644,
+ getter_Transfers(raiiFd)));
+ const auto fd = raiiFd.get();
+
+ // We also need to hold mMonitor while we're touching scripts in
+ // mScripts, or they may be freed before we're done with them.
+ mMonitor.AssertNotCurrentThreadOwns();
+ MonitorAutoLock mal(mMonitor);
+
+ nsTArray<CachedStencil*> scripts;
+ for (auto& script : IterHash(mScripts, Match<ScriptStatus::Saved>())) {
+ scripts.AppendElement(script);
+ }
+
+ // Sort scripts by load time, with async loaded scripts before sync scripts.
+ // Since async scripts are always loaded immediately at startup, it helps to
+ // have them stored contiguously.
+ scripts.Sort(CachedStencil::Comparator());
+
+ OutputBuffer buf;
+ size_t offset = 0;
+ for (auto script : scripts) {
+ script->mOffset = offset;
+ MOZ_DIAGNOSTIC_ASSERT(
+ JS::IsTranscodingBytecodeOffsetAligned(script->mOffset));
+ script->Code(buf);
+
+ offset += script->mSize;
+ MOZ_DIAGNOSTIC_ASSERT(
+ JS::IsTranscodingBytecodeOffsetAligned(script->mSize));
+ }
+
+ uint8_t headerSize[4];
+ LittleEndian::writeUint32(headerSize, buf.cursor());
+
+ uint8_t crc[4];
+ LittleEndian::writeUint32(crc, ComputeCrc32c(~0, buf.Get(), buf.cursor()));
+
+ MOZ_TRY(Write(fd, MAGIC, sizeof(MAGIC)));
+ MOZ_TRY(Write(fd, headerSize, sizeof(headerSize)));
+ MOZ_TRY(Write(fd, crc, sizeof(crc)));
+ MOZ_TRY(Write(fd, buf.Get(), buf.cursor()));
+
+ // Align the start of the scripts section to the transcode alignment.
+ size_t written = sizeof(MAGIC) + sizeof(headerSize) + buf.cursor();
+ size_t padding = JS::AlignTranscodingBytecodeOffset(written) - written;
+ if (padding) {
+ MOZ_TRY(WritePadding(fd, padding));
+ written += padding;
+ }
+
+ for (auto script : scripts) {
+ MOZ_DIAGNOSTIC_ASSERT(JS::IsTranscodingBytecodeOffsetAligned(written));
+ MOZ_TRY(Write(fd, script->Range().begin().get(), script->mSize));
+
+ written += script->mSize;
+ // We can only free the XDR data if the stencil isn't borrowing data from
+ // it.
+ if (script->mStencil && !JS::StencilIsBorrowed(script->mStencil)) {
+ script->FreeData();
+ }
+ }
+ }
+
+ MOZ_TRY(cacheFile->MoveTo(nullptr, mBaseName + u".bin"_ns));
+
+ return Ok();
+}
+
+nsresult ScriptPreloader::GetName(nsACString& aName) {
+ aName.AssignLiteral("ScriptPreloader");
+ return NS_OK;
+}
+
+// Runs in the mSaveThread thread, and writes out the cache file for the next
+// session after a reasonable delay.
+nsresult ScriptPreloader::Run() {
+ MonitorSingleWriterAutoLock mal(mSaveMonitor);
+
+ // Ideally wait about 10 seconds before saving, to avoid unnecessary IO
+ // during early startup. But only if the cache hasn't been invalidated,
+ // since that can trigger a new write during shutdown, and we don't want to
+ // cause shutdown hangs.
+ if (!mCacheInvalidated) {
+ mal.Wait(TimeDuration::FromSeconds(10));
+ }
+
+ auto result = URLPreloader::GetSingleton().WriteCache();
+ Unused << NS_WARN_IF(result.isErr());
+
+ result = WriteCache();
+ Unused << NS_WARN_IF(result.isErr());
+
+ {
+ MonitorSingleWriterAutoLock lock(mChildCache->mSaveMonitor);
+ result = mChildCache->WriteCache();
+ }
+ Unused << NS_WARN_IF(result.isErr());
+
+ NS_DispatchToMainThread(
+ NewRunnableMethod("ScriptPreloader::CacheWriteComplete", this,
+ &ScriptPreloader::CacheWriteComplete),
+ NS_DISPATCH_NORMAL);
+ return NS_OK;
+}
+
+void ScriptPreloader::CacheWriteComplete() {
+ mSaveThread->AsyncShutdown();
+ mSaveThread = nullptr;
+ mSaveComplete = true;
+
+ nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier();
+ barrier->RemoveBlocker(this);
+}
+
+void ScriptPreloader::NoteStencil(const nsCString& url,
+ const nsCString& cachePath,
+ JS::Stencil* stencil, bool isRunOnce) {
+ if (!Active()) {
+ if (isRunOnce) {
+ if (auto script = mScripts.Get(cachePath)) {
+ script->mIsRunOnce = true;
+ script->MaybeDropStencil();
+ }
+ }
+ return;
+ }
+
+ // Don't bother trying to cache any URLs with cache-busting query
+ // parameters.
+ if (cachePath.FindChar('?') >= 0) {
+ return;
+ }
+
+ // Don't bother caching files that belong to the mochitest harness.
+ constexpr auto mochikitPrefix = "chrome://mochikit/"_ns;
+ if (StringHead(url, mochikitPrefix.Length()) == mochikitPrefix) {
+ return;
+ }
+
+ auto* script =
+ mScripts.GetOrInsertNew(cachePath, *this, url, cachePath, stencil);
+ if (isRunOnce) {
+ script->mIsRunOnce = true;
+ }
+
+ if (!script->MaybeDropStencil() && !script->mStencil) {
+ MOZ_ASSERT(stencil);
+ script->mStencil = stencil;
+ script->mReadyToExecute = true;
+ }
+
+ script->UpdateLoadTime(TimeStamp::Now());
+ script->mProcessTypes += CurrentProcessType();
+}
+
+void ScriptPreloader::NoteStencil(const nsCString& url,
+ const nsCString& cachePath,
+ ProcessType processType,
+ nsTArray<uint8_t>&& xdrData,
+ TimeStamp loadTime) {
+ // After data has been prepared, there's no point in noting further scripts,
+ // since the cache either has already been written, or is about to be
+ // written. Any time prior to the data being prepared, we can safely mutate
+ // mScripts without locking. After that point, the save thread is free to
+ // access it, and we can't alter it without locking.
+ if (mDataPrepared) {
+ return;
+ }
+
+ auto* script =
+ mScripts.GetOrInsertNew(cachePath, *this, url, cachePath, nullptr);
+
+ if (!script->HasRange()) {
+ MOZ_ASSERT(!script->HasArray());
+
+ script->mSize = xdrData.Length();
+ script->mXDRData.construct<nsTArray<uint8_t>>(
+ std::forward<nsTArray<uint8_t>>(xdrData));
+
+ auto& data = script->Array();
+ script->mXDRRange.emplace(data.Elements(), data.Length());
+ }
+
+ if (!script->mSize && !script->mStencil) {
+ // If the content process is sending us an entry for a stencil
+ // which was in the cache at startup, it expects us to already have this
+ // script data, so it doesn't send it.
+ //
+ // However, the cache may have been invalidated at this point (usually
+ // due to the add-on manager installing or uninstalling a legacy
+ // extension during very early startup), which means we may no longer
+ // have an entry for this script. Since that means we have no data to
+ // write to the new cache, and no JSScript to generate it from, we need
+ // to discard this entry.
+ mScripts.Remove(cachePath);
+ return;
+ }
+
+ script->UpdateLoadTime(loadTime);
+ script->mProcessTypes += processType;
+}
+
+/* static */
+void ScriptPreloader::FillCompileOptionsForCachedStencil(
+ JS::CompileOptions& options) {
+ // Users of the cache do not require return values, so inform the JS parser in
+ // order for it to generate simpler bytecode.
+ options.setNoScriptRval(true);
+
+ // The ScriptPreloader trades off having bytecode available but not source
+ // text. This means the JS syntax-only parser is not used. If `toString` is
+ // called on functions in these scripts, the source-hook will fetch it over,
+ // so using `toString` of functions should be avoided in chrome js.
+ options.setSourceIsLazy(true);
+}
+
+/* static */
+void ScriptPreloader::FillDecodeOptionsForCachedStencil(
+ JS::DecodeOptions& options) {
+ // ScriptPreloader's XDR buffer is alive during the Stencil is alive.
+ // The decoded stencil can borrow from it.
+ //
+ // NOTE: The XDR buffer is alive during the entire browser lifetime only
+ // when it's mmapped.
+ options.borrowBuffer = true;
+}
+
+already_AddRefed<JS::Stencil> ScriptPreloader::GetCachedStencil(
+ JSContext* cx, const JS::ReadOnlyDecodeOptions& options,
+ const nsCString& path) {
+ MOZ_RELEASE_ASSERT(
+ !(XRE_IsContentProcess() && !mCacheInitialized),
+ "ScriptPreloader must be initialized before getting cached "
+ "scripts in the content process.");
+
+ // If a script is used by both the parent and the child, it's stored only
+ // in the child cache.
+ if (mChildCache) {
+ RefPtr<JS::Stencil> stencil =
+ mChildCache->GetCachedStencilInternal(cx, options, path);
+ if (stencil) {
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_SCRIPT_PRELOADER_REQUESTS::HitChild);
+ return stencil.forget();
+ }
+ }
+
+ RefPtr<JS::Stencil> stencil = GetCachedStencilInternal(cx, options, path);
+ Telemetry::AccumulateCategorical(
+ stencil ? Telemetry::LABELS_SCRIPT_PRELOADER_REQUESTS::Hit
+ : Telemetry::LABELS_SCRIPT_PRELOADER_REQUESTS::Miss);
+ return stencil.forget();
+}
+
+already_AddRefed<JS::Stencil> ScriptPreloader::GetCachedStencilInternal(
+ JSContext* cx, const JS::ReadOnlyDecodeOptions& options,
+ const nsCString& path) {
+ auto* cachedScript = mScripts.Get(path);
+ if (cachedScript) {
+ return WaitForCachedStencil(cx, options, cachedScript);
+ }
+ return nullptr;
+}
+
+already_AddRefed<JS::Stencil> ScriptPreloader::WaitForCachedStencil(
+ JSContext* cx, const JS::ReadOnlyDecodeOptions& options,
+ CachedStencil* script) {
+ if (!script->mReadyToExecute) {
+ // mReadyToExecute is kept as false only when off-thread decode task was
+ // available (pref is set to true) and the task was successfully created.
+ // See ScriptPreloader::StartDecodeTask methods.
+ MOZ_ASSERT(mDecodedStencils);
+
+ // Check for the finished operations that can contain our target.
+ if (mDecodedStencils->AvailableRead() > 0) {
+ FinishOffThreadDecode();
+ }
+
+ if (!script->mReadyToExecute) {
+ // Our target is not yet decoded.
+
+ // If script is small enough, we'd rather decode on main-thread than wait
+ // for a decode task to complete.
+ if (script->mSize < MAX_MAINTHREAD_DECODE_SIZE) {
+ LOG(Info, "Script is small enough to recompile on main thread\n");
+
+ script->mReadyToExecute = true;
+ Telemetry::ScalarAdd(
+ Telemetry::ScalarID::SCRIPT_PRELOADER_MAINTHREAD_RECOMPILE, 1);
+ } else {
+ LOG(Info, "Must wait for async script load: %s\n", script->mURL.get());
+ auto start = TimeStamp::Now();
+
+ MonitorAutoLock mal(mMonitor);
+
+ // Process finished tasks until our target is found.
+ while (!script->mReadyToExecute) {
+ if (mDecodedStencils->AvailableRead() > 0) {
+ FinishOffThreadDecode();
+ } else {
+ MOZ_ASSERT(!mDecodingScripts.isEmpty());
+ mWaitingForDecode = true;
+ mal.Wait();
+ mWaitingForDecode = false;
+ }
+ }
+
+ double waitedMS = (TimeStamp::Now() - start).ToMilliseconds();
+ Telemetry::Accumulate(Telemetry::SCRIPT_PRELOADER_WAIT_TIME,
+ int(waitedMS));
+ LOG(Debug, "Waited %fms\n", waitedMS);
+ }
+ }
+ }
+
+ return script->GetStencil(cx, options);
+}
+
+void ScriptPreloader::onDecodedStencilQueued() {
+ mMonitor.AssertNotCurrentThreadOwns();
+ MonitorAutoLock mal(mMonitor);
+
+ if (mWaitingForDecode) {
+ // Wake up the blocked main thread.
+ mal.Notify();
+ }
+
+ // NOTE: Do not perform DoFinishOffThreadDecode for partial data.
+}
+
+void ScriptPreloader::OnDecodeTaskFinished() {
+ mMonitor.AssertNotCurrentThreadOwns();
+ MonitorAutoLock mal(mMonitor);
+
+ if (mWaitingForDecode) {
+ // Wake up the blocked main thread.
+ mal.Notify();
+ } else {
+ // Issue a Runnable to handle all decoded stencils, even if the next
+ // WaitForCachedStencil call has not happened yet.
+ NS_DispatchToMainThread(
+ NewRunnableMethod("ScriptPreloader::DoFinishOffThreadDecode", this,
+ &ScriptPreloader::DoFinishOffThreadDecode));
+ }
+}
+
+void ScriptPreloader::OnDecodeTaskFailed() {
+ // NOTE: nullptr is enqueued to mDecodedStencils, and FinishOffThreadDecode
+ // handles it as failure.
+ OnDecodeTaskFinished();
+}
+
+void ScriptPreloader::FinishPendingParses(MonitorAutoLock& aMal) {
+ mMonitor.AssertCurrentThreadOwns();
+
+ // If off-thread decoding task hasn't been started, nothing to do.
+ // This can happen if the javascript.options.parallel_parsing pref was false,
+ // or the decode task fails to start.
+ if (!mDecodedStencils) {
+ return;
+ }
+
+ // Process any pending decodes that are in flight.
+ while (!mDecodingScripts.isEmpty()) {
+ if (mDecodedStencils->AvailableRead() > 0) {
+ FinishOffThreadDecode();
+ } else {
+ mWaitingForDecode = true;
+ aMal.Wait();
+ mWaitingForDecode = false;
+ }
+ }
+}
+
+void ScriptPreloader::DoFinishOffThreadDecode() {
+ // NOTE: mDecodedStencils could already be reset.
+ if (mDecodedStencils && mDecodedStencils->AvailableRead() > 0) {
+ FinishOffThreadDecode();
+ }
+}
+
+void ScriptPreloader::FinishOffThreadDecode() {
+ MOZ_ASSERT(mDecodedStencils);
+
+ while (mDecodedStencils->AvailableRead() > 0) {
+ RefPtr<JS::Stencil> stencil;
+ DebugOnly<int> reads = mDecodedStencils->Dequeue(&stencil, 1);
+ MOZ_ASSERT(reads == 1);
+
+ if (!stencil) {
+ // DecodeTask failed.
+ // Mark all remaining scripts to be decoded on the main thread.
+ for (CachedStencil* next = mDecodingScripts.getFirst(); next;) {
+ auto* script = next;
+ next = script->getNext();
+
+ script->mReadyToExecute = true;
+ script->remove();
+ }
+
+ break;
+ }
+
+ CachedStencil* script = mDecodingScripts.getFirst();
+ MOZ_ASSERT(script);
+
+ LOG(Debug, "Finished off-thread decode of %s\n", script->mURL.get());
+ script->mStencil = stencil.forget();
+ script->mReadyToExecute = true;
+ script->remove();
+ }
+
+ if (mDecodingScripts.isEmpty()) {
+ mDecodedStencils.reset();
+ }
+}
+
+void ScriptPreloader::StartDecodeTask(JS::HandleObject scope) {
+ auto start = TimeStamp::Now();
+ LOG(Debug, "Off-thread decoding scripts...\n");
+
+ Vector<JS::TranscodeSource> decodingSources;
+
+ size_t size = 0;
+ for (CachedStencil* next = mDecodingScripts.getFirst(); next;) {
+ auto* script = next;
+ next = script->getNext();
+
+ MOZ_ASSERT(script->IsMemMapped());
+
+ // Skip any scripts that we decoded on the main thread rather than
+ // waiting for an off-thread operation to complete.
+ if (script->mReadyToExecute) {
+ script->remove();
+ continue;
+ }
+ if (!decodingSources.emplaceBack(script->Range(), script->mURL.get(), 0)) {
+ break;
+ }
+
+ LOG(Debug, "Beginning off-thread decode of script %s (%u bytes)\n",
+ script->mURL.get(), script->mSize);
+
+ size += script->mSize;
+ }
+
+ MOZ_ASSERT(decodingSources.length() == mDecodingScripts.length());
+
+ if (size == 0 && mDecodingScripts.isEmpty()) {
+ return;
+ }
+
+ AutoSafeJSAPI jsapi;
+ JSContext* cx = jsapi.cx();
+ JSAutoRealm ar(cx, scope ? scope : xpc::CompilationScope());
+
+ JS::CompileOptions options(cx);
+ FillCompileOptionsForCachedStencil(options);
+
+ // All XDR buffers are mmapped and live longer than JS runtime.
+ // The bytecode can be borrowed from the buffer.
+ options.borrowBuffer = true;
+ options.usePinnedBytecode = true;
+
+ JS::DecodeOptions decodeOptions(options);
+
+ size_t decodingSourcesLength = decodingSources.length();
+
+ if (!StaticPrefs::javascript_options_parallel_parsing() ||
+ !StartDecodeTask(decodeOptions, std::move(decodingSources))) {
+ LOG(Info, "Can't decode %lu bytes of scripts off-thread",
+ (unsigned long)size);
+ for (auto* script : mDecodingScripts) {
+ script->mReadyToExecute = true;
+ }
+ return;
+ }
+
+ LOG(Debug, "Initialized decoding of %u scripts (%u bytes) in %fms\n",
+ (unsigned)decodingSourcesLength, (unsigned)size,
+ (TimeStamp::Now() - start).ToMilliseconds());
+}
+
+bool ScriptPreloader::StartDecodeTask(
+ const JS::ReadOnlyDecodeOptions& decodeOptions,
+ Vector<JS::TranscodeSource>&& decodingSources) {
+ mDecodedStencils.emplace(decodingSources.length());
+ MOZ_ASSERT(mDecodedStencils);
+
+ nsCOMPtr<nsIRunnable> task =
+ new DecodeTask(this, decodeOptions, std::move(decodingSources));
+
+ nsresult rv = NS_DispatchBackgroundTask(task.forget());
+
+ return NS_SUCCEEDED(rv);
+}
+
+NS_IMETHODIMP ScriptPreloader::DecodeTask::Run() {
+ auto failure = [&]() {
+ RefPtr<JS::Stencil> stencil;
+ DebugOnly<int> writes = mPreloader->mDecodedStencils->Enqueue(stencil);
+ MOZ_ASSERT(writes == 1);
+ mPreloader->OnDecodeTaskFailed();
+ };
+
+ JS::FrontendContext* fc = JS::NewFrontendContext();
+ if (!fc) {
+ failure();
+ return NS_OK;
+ }
+
+ auto cleanup = MakeScopeExit([&]() { JS::DestroyFrontendContext(fc); });
+
+ size_t stackSize = TaskController::GetThreadStackSize();
+ JS::SetNativeStackQuota(fc, JS::ThreadStackQuotaForSize(stackSize));
+
+ size_t remaining = mDecodingSources.length();
+ for (auto& source : mDecodingSources) {
+ RefPtr<JS::Stencil> stencil;
+ auto result = JS::DecodeStencil(fc, mDecodeOptions, source.range,
+ getter_AddRefs(stencil));
+ if (result != JS::TranscodeResult::Ok) {
+ failure();
+ return NS_OK;
+ }
+
+ DebugOnly<int> writes = mPreloader->mDecodedStencils->Enqueue(stencil);
+ MOZ_ASSERT(writes == 1);
+
+ remaining--;
+ if (remaining) {
+ mPreloader->onDecodedStencilQueued();
+ }
+ }
+
+ mPreloader->OnDecodeTaskFinished();
+ return NS_OK;
+}
+
+ScriptPreloader::CachedStencil::CachedStencil(ScriptPreloader& cache,
+ InputBuffer& buf)
+ : mCache(cache) {
+ Code(buf);
+
+ // Swap the mProcessTypes and mOriginalProcessTypes values, since we want to
+ // start with an empty set of processes loaded into for this session, and
+ // compare against last session's values later.
+ mOriginalProcessTypes = mProcessTypes;
+ mProcessTypes = {};
+}
+
+bool ScriptPreloader::CachedStencil::XDREncode(JSContext* cx) {
+ auto cleanup = MakeScopeExit([&]() { MaybeDropStencil(); });
+
+ mXDRData.construct<JS::TranscodeBuffer>();
+
+ JS::TranscodeResult code = JS::EncodeStencil(cx, mStencil, Buffer());
+ if (code == JS::TranscodeResult::Ok) {
+ mXDRRange.emplace(Buffer().begin(), Buffer().length());
+ mSize = Range().length();
+ return true;
+ }
+ mXDRData.destroy();
+ JS_ClearPendingException(cx);
+ return false;
+}
+
+already_AddRefed<JS::Stencil> ScriptPreloader::CachedStencil::GetStencil(
+ JSContext* cx, const JS::ReadOnlyDecodeOptions& options) {
+ MOZ_ASSERT(mReadyToExecute);
+ if (mStencil) {
+ return do_AddRef(mStencil);
+ }
+
+ if (!HasRange()) {
+ // We've already executed the script, and thrown it away. But it wasn't
+ // in the cache at startup, so we don't have any data to decode. Give
+ // up.
+ return nullptr;
+ }
+
+ // If we have no script at this point, the script was too small to decode
+ // off-thread, or it was needed before the off-thread compilation was
+ // finished, and is small enough to decode on the main thread rather than
+ // wait for the off-thread decoding to finish. In either case, we decode
+ // it synchronously the first time it's needed.
+
+ auto start = TimeStamp::Now();
+ LOG(Info, "Decoding stencil %s on main thread...\n", mURL.get());
+
+ RefPtr<JS::Stencil> stencil;
+ if (JS::DecodeStencil(cx, options, Range(), getter_AddRefs(stencil)) ==
+ JS::TranscodeResult::Ok) {
+ // Lock the monitor here to avoid data races on mScript
+ // from other threads like the cache writing thread.
+ //
+ // It is possible that we could end up decoding the same
+ // script twice, because DecodeScript isn't being guarded
+ // by the monitor; however, to encourage off-thread decode
+ // to proceed for other scripts we don't hold the monitor
+ // while doing main thread decode, merely while updating
+ // mScript.
+ mCache.mMonitor.AssertNotCurrentThreadOwns();
+ MonitorAutoLock mal(mCache.mMonitor);
+
+ mStencil = stencil.forget();
+
+ if (mCache.mSaveComplete) {
+ // We can only free XDR data if the stencil isn't borrowing data out of
+ // it.
+ if (!JS::StencilIsBorrowed(mStencil)) {
+ FreeData();
+ }
+ }
+ }
+
+ LOG(Debug, "Finished decoding in %fms",
+ (TimeStamp::Now() - start).ToMilliseconds());
+
+ return do_AddRef(mStencil);
+}
+
+// nsIAsyncShutdownBlocker
+
+nsresult ScriptPreloader::GetName(nsAString& aName) {
+ aName.AssignLiteral(u"ScriptPreloader: Saving bytecode cache");
+ return NS_OK;
+}
+
+nsresult ScriptPreloader::GetState(nsIPropertyBag** aState) {
+ *aState = nullptr;
+ return NS_OK;
+}
+
+nsresult ScriptPreloader::BlockShutdown(
+ nsIAsyncShutdownClient* aBarrierClient) {
+ // If we're waiting on a timeout to finish saving, interrupt it and just save
+ // immediately.
+ mSaveMonitor.NotifyAll();
+ return NS_OK;
+}
+
+already_AddRefed<nsIAsyncShutdownClient> ScriptPreloader::GetShutdownBarrier() {
+ nsCOMPtr<nsIAsyncShutdownService> svc = components::AsyncShutdown::Service();
+ MOZ_RELEASE_ASSERT(svc);
+
+ nsCOMPtr<nsIAsyncShutdownClient> barrier;
+ Unused << svc->GetXpcomWillShutdown(getter_AddRefs(barrier));
+ MOZ_RELEASE_ASSERT(barrier);
+
+ return barrier.forget();
+}
+
+NS_IMPL_ISUPPORTS(ScriptPreloader, nsIObserver, nsIRunnable, nsIMemoryReporter,
+ nsINamed, nsIAsyncShutdownBlocker)
+
+#undef LOG
+
+} // namespace mozilla
diff --git a/js/xpconnect/loader/ScriptPreloader.h b/js/xpconnect/loader/ScriptPreloader.h
new file mode 100644
index 0000000000..bccaf150b9
--- /dev/null
+++ b/js/xpconnect/loader/ScriptPreloader.h
@@ -0,0 +1,555 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef ScriptPreloader_h
+#define ScriptPreloader_h
+
+#include "mozilla/Atomics.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/EnumSet.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/MaybeOneOf.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/Range.h"
+#include "mozilla/Result.h"
+#include "mozilla/SPSCQueue.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Vector.h"
+#include "mozilla/loader/AutoMemMap.h"
+#include "MainThreadUtils.h"
+#include "nsClassHashtable.h"
+#include "nsThreadUtils.h"
+#include "nsIAsyncShutdown.h"
+#include "nsIFile.h"
+#include "nsIMemoryReporter.h"
+#include "nsIObserver.h"
+#include "nsIThread.h"
+#include "nsITimer.h"
+
+#include "js/CompileOptions.h" // JS::DecodeOptions, JS::ReadOnlyDecodeOptions
+#include "js/experimental/JSStencil.h" // JS::Stencil
+#include "js/GCAnnotations.h" // for JS_HAZ_NON_GC_POINTER
+#include "js/RootingAPI.h" // for Handle, Heap
+#include "js/Transcoding.h" // for TranscodeBuffer, TranscodeRange, TranscodeSource
+#include "js/TypeDecls.h" // for HandleObject, HandleScript
+
+#include <prio.h>
+
+namespace mozilla {
+namespace dom {
+class ContentParent;
+}
+namespace ipc {
+class FileDescriptor;
+}
+namespace loader {
+class InputBuffer;
+class ScriptCacheChild;
+
+enum class ProcessType : uint8_t {
+ Uninitialized,
+ Parent,
+ Web,
+ Extension,
+ PrivilegedAbout,
+};
+
+template <typename T>
+struct Matcher {
+ virtual bool Matches(T) = 0;
+};
+} // namespace loader
+
+using namespace mozilla::loader;
+
+class ScriptPreloader : public nsIObserver,
+ public nsIMemoryReporter,
+ public nsIRunnable,
+ public nsINamed,
+ public nsIAsyncShutdownBlocker,
+ public SingleWriterLockOwner {
+ MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf)
+
+ friend class mozilla::loader::ScriptCacheChild;
+
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIMEMORYREPORTER
+ NS_DECL_NSIRUNNABLE
+ NS_DECL_NSINAMED
+ NS_DECL_NSIASYNCSHUTDOWNBLOCKER
+
+ private:
+ static StaticRefPtr<ScriptPreloader> gScriptPreloader;
+ static StaticRefPtr<ScriptPreloader> gChildScriptPreloader;
+ static StaticAutoPtr<AutoMemMap> gCacheData;
+ static StaticAutoPtr<AutoMemMap> gChildCacheData;
+
+ public:
+ static ScriptPreloader& GetSingleton();
+ static ScriptPreloader& GetChildSingleton();
+
+ static void DeleteSingleton();
+ static void DeleteCacheDataSingleton();
+
+ static ProcessType GetChildProcessType(const nsACString& remoteType);
+
+ // Fill some options that should be consistent across all scripts stored
+ // into preloader cache.
+ static void FillCompileOptionsForCachedStencil(JS::CompileOptions& options);
+ static void FillDecodeOptionsForCachedStencil(JS::DecodeOptions& options);
+
+ bool OnWritingThread() const override { return NS_IsMainThread(); }
+
+ // Retrieves the stencil with the given cache key from the cache.
+ // Returns null if the stencil is not cached.
+ already_AddRefed<JS::Stencil> GetCachedStencil(
+ JSContext* cx, const JS::ReadOnlyDecodeOptions& options,
+ const nsCString& path);
+
+ // Notes the execution of a script with the given URL and cache key.
+ // Depending on the stage of startup, the script may be serialized and
+ // stored to the startup script cache.
+ //
+ // If isRunOnce is true, this script is expected to run only once per
+ // process per browser session. A cached instance will not be kept alive
+ // for repeated execution.
+ void NoteStencil(const nsCString& url, const nsCString& cachePath,
+ JS::Stencil* stencil, bool isRunOnce = false);
+
+ // Notes the IPC arrival of the XDR data of a stencil compiled by some
+ // child process. See ScriptCacheChild::SendScriptsAndFinalize.
+ void NoteStencil(const nsCString& url, const nsCString& cachePath,
+ ProcessType processType, nsTArray<uint8_t>&& xdrData,
+ TimeStamp loadTime);
+
+ // Initializes the script cache from the startup script cache file.
+ Result<Ok, nsresult> InitCache(const nsAString& = u"scriptCache"_ns);
+
+ Result<Ok, nsresult> InitCache(const Maybe<ipc::FileDescriptor>& cacheFile,
+ ScriptCacheChild* cacheChild);
+
+ bool Active() const { return mCacheInitialized && !mStartupFinished; }
+
+ private:
+ Result<Ok, nsresult> InitCacheInternal(JS::Handle<JSObject*> scope = nullptr);
+ already_AddRefed<JS::Stencil> GetCachedStencilInternal(
+ JSContext* cx, const JS::ReadOnlyDecodeOptions& options,
+ const nsCString& path);
+
+ public:
+ static ProcessType CurrentProcessType() {
+ MOZ_ASSERT(sProcessType != ProcessType::Uninitialized);
+ return sProcessType;
+ }
+
+ static void InitContentChild(dom::ContentParent& parent);
+
+ protected:
+ virtual ~ScriptPreloader();
+
+ private:
+ enum class ScriptStatus {
+ Restored,
+ Saved,
+ };
+
+ // Represents a cached script stencil, either initially read from the
+ // cache file, to be added to the next session's stencil cache file, or
+ // both.
+ //
+ // - Read from the cache, and being decoded off thread. In this case:
+ // - mReadyToExecute is false
+ // - mDecodingScripts contains the CachedStencil
+ // - mDecodedStencils have never contained the stencil
+ // - mStencil is null
+ //
+ // - Off-thread decode for the stencil has finished, but the stencil has not
+ // yet been dequeued nor executed. In this case:
+ // - mReadyToExecute is true
+ // - mDecodingScripts contains the CachedStencil
+ // - mDecodedStencils contains the decoded stencil
+ // - mStencil is null
+ //
+ // - Off-thread decode for the stencil has finished, and the stencil has
+ // been dequeued, but has not yet been executed. In this case:
+ // - mReadyToExecute is true
+ // - mDecodingScripts no longer contains the CachedStencil
+ // - mDecodedStencils no longer contains the decoded stencil
+ // - mStencil is non-null
+ //
+ // - Fully decoded, and ready to be added to the next session's cache
+ // file. In this case:
+ // - mReadyToExecute is true
+ // - mStencil is non-null
+ //
+ // A stencil to be added to the next session's cache file always has a
+ // non-null mStencil value. If it was read from the last session's cache
+ // file, it also has a non-empty mXDRRange range, which will be stored in
+ // the next session's cache file. If it was compiled in this session, its
+ // mXDRRange will initially be empty, and its mXDRData buffer will be
+ // populated just before it is written to the cache file.
+ class CachedStencil : public LinkedListElement<CachedStencil> {
+ public:
+ CachedStencil(CachedStencil&&) = delete;
+
+ CachedStencil(ScriptPreloader& cache, const nsCString& url,
+ const nsCString& cachePath, JS::Stencil* stencil)
+ : mCache(cache),
+ mURL(url),
+ mCachePath(cachePath),
+ mStencil(stencil),
+ mReadyToExecute(true),
+ mIsRunOnce(false) {}
+
+ inline CachedStencil(ScriptPreloader& cache, InputBuffer& buf);
+
+ ~CachedStencil() = default;
+
+ ScriptStatus Status() const {
+ return mProcessTypes.isEmpty() ? ScriptStatus::Restored
+ : ScriptStatus::Saved;
+ }
+
+ // For use with nsTArray::Sort.
+ //
+ // Orders scripts by script load time, so that scripts which are needed
+ // earlier are stored earlier, and scripts needed at approximately the
+ // same time are stored approximately contiguously.
+ struct Comparator {
+ bool Equals(const CachedStencil* a, const CachedStencil* b) const {
+ return a->mLoadTime == b->mLoadTime;
+ }
+
+ bool LessThan(const CachedStencil* a, const CachedStencil* b) const {
+ return a->mLoadTime < b->mLoadTime;
+ }
+ };
+
+ struct StatusMatcher final : public Matcher<CachedStencil*> {
+ explicit StatusMatcher(ScriptStatus status) : mStatus(status) {}
+
+ virtual bool Matches(CachedStencil* script) override {
+ return script->Status() == mStatus;
+ }
+
+ const ScriptStatus mStatus;
+ };
+
+ void FreeData() {
+ // If the script data isn't mmapped, we need to release both it
+ // and the Range that points to it at the same time.
+ if (!IsMemMapped()) {
+ mXDRRange.reset();
+ mXDRData.destroy();
+ }
+ }
+
+ void UpdateLoadTime(const TimeStamp& loadTime) {
+ if (mLoadTime.IsNull() || loadTime < mLoadTime) {
+ mLoadTime = loadTime;
+ }
+ }
+
+ // Checks whether the cached JSScript for this entry will be needed
+ // again and, if not, drops it and returns true. This is the case for
+ // run-once scripts that do not still need to be encoded into the
+ // cache.
+ //
+ // If this method returns false, callers may set mScript to a cached
+ // JSScript instance for this entry. If it returns true, they should
+ // not.
+ bool MaybeDropStencil() {
+ if (mIsRunOnce && (HasRange() || !mCache.WillWriteScripts())) {
+ mStencil = nullptr;
+ return true;
+ }
+ return false;
+ }
+
+ // Encodes this script into XDR data, and stores the result in mXDRData.
+ // Returns true on success, false on failure.
+ bool XDREncode(JSContext* cx);
+
+ // Encodes or decodes this script, in the storage format required by the
+ // script cache file.
+ template <typename Buffer>
+ void Code(Buffer& buffer) {
+ buffer.codeString(mURL);
+ buffer.codeString(mCachePath);
+ buffer.codeUint32(mOffset);
+ buffer.codeUint32(mSize);
+ buffer.codeUint8(mProcessTypes);
+ }
+
+ // Returns the XDR data generated for this script during this session. See
+ // mXDRData.
+ JS::TranscodeBuffer& Buffer() {
+ MOZ_ASSERT(HasBuffer());
+ return mXDRData.ref<JS::TranscodeBuffer>();
+ }
+
+ bool HasBuffer() { return mXDRData.constructed<JS::TranscodeBuffer>(); }
+
+ // Returns the read-only XDR data for this script. See mXDRRange.
+ const JS::TranscodeRange& Range() {
+ MOZ_ASSERT(HasRange());
+ return mXDRRange.ref();
+ }
+
+ bool HasRange() { return mXDRRange.isSome(); }
+
+ bool IsMemMapped() const { return mXDRData.empty(); }
+
+ nsTArray<uint8_t>& Array() {
+ MOZ_ASSERT(HasArray());
+ return mXDRData.ref<nsTArray<uint8_t>>();
+ }
+
+ bool HasArray() { return mXDRData.constructed<nsTArray<uint8_t>>(); }
+
+ already_AddRefed<JS::Stencil> GetStencil(
+ JSContext* cx, const JS::ReadOnlyDecodeOptions& options);
+
+ size_t HeapSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) {
+ auto size = mallocSizeOf(this);
+
+ if (HasArray()) {
+ size += Array().ShallowSizeOfExcludingThis(mallocSizeOf);
+ } else if (HasBuffer()) {
+ size += Buffer().sizeOfExcludingThis(mallocSizeOf);
+ }
+
+ if (mStencil) {
+ size += JS::SizeOfStencil(mStencil, mallocSizeOf);
+ }
+
+ // Note: mURL and mCachePath use the same string for scripts loaded
+ // by the message manager. The following statement avoids
+ // double-measuring in that case.
+ size += (mURL.SizeOfExcludingThisIfUnshared(mallocSizeOf) +
+ mCachePath.SizeOfExcludingThisEvenIfShared(mallocSizeOf));
+
+ return size;
+ }
+
+ ScriptPreloader& mCache;
+
+ // The URL from which this script was initially read and compiled.
+ nsCString mURL;
+ // A unique identifier for this script's filesystem location, used as a
+ // primary cache lookup value.
+ nsCString mCachePath;
+
+ // The offset of this script in the cache file, from the start of the XDR
+ // data block.
+ uint32_t mOffset = 0;
+ // The size of this script's encoded XDR data.
+ uint32_t mSize = 0;
+
+ TimeStamp mLoadTime{};
+
+ RefPtr<JS::Stencil> mStencil;
+
+ // True if this script is ready to be executed. This means that either the
+ // off-thread portion of an off-thread decode has finished, or the
+ // off-thread decode failed, and may be immediately decoded
+ // whenever it is first executed.
+ bool mReadyToExecute = false;
+
+ // True if this script is expected to run once per process. If so, its
+ // JSScript instance will be dropped as soon as the script has
+ // executed and been encoded into the cache.
+ bool mIsRunOnce = false;
+
+ // The set of processes in which this script has been used.
+ EnumSet<ProcessType> mProcessTypes{};
+
+ // The set of processes which the script was loaded into during the
+ // last session, as read from the cache file.
+ EnumSet<ProcessType> mOriginalProcessTypes{};
+
+ // The read-only XDR data for this script, which was either read from an
+ // existing cache file, or generated by encoding a script which was
+ // compiled during this session.
+ Maybe<JS::TranscodeRange> mXDRRange;
+
+ // XDR data which was generated from a script compiled during this
+ // session, and will be written to the cache file.
+ //
+ // The format is JS::TranscodeBuffer if the script was XDR'd as part
+ // of this process, or nsTArray<> if the script was transfered by IPC
+ // from a child process.
+ MaybeOneOf<JS::TranscodeBuffer, nsTArray<uint8_t>> mXDRData;
+ } JS_HAZ_NON_GC_POINTER;
+
+ template <ScriptStatus status>
+ static Matcher<CachedStencil*>* Match() {
+ static CachedStencil::StatusMatcher matcher{status};
+ return &matcher;
+ }
+
+ // The maximum size of scripts to re-decode on the main thread if off-thread
+ // decoding hasn't finished yet. In practice, we don't hit this very often,
+ // but when we do, re-decoding some smaller scripts on the main thread gives
+ // the background decoding a chance to catch up without blocking the main
+ // thread for quite as long.
+ static constexpr int MAX_MAINTHREAD_DECODE_SIZE = 50 * 1024;
+
+ explicit ScriptPreloader(AutoMemMap* cacheData);
+
+ void Cleanup();
+
+ void FinishPendingParses(MonitorAutoLock& aMal);
+ void InvalidateCache();
+
+ // Opens the cache file for reading.
+ Result<Ok, nsresult> OpenCache();
+
+ // Writes a new cache file to disk. Must not be called on the main thread.
+ Result<Ok, nsresult> WriteCache() MOZ_REQUIRES(mSaveMonitor);
+
+ void StartCacheWrite();
+
+ // Prepares scripts for writing to the cache, serializing new scripts to
+ // XDR, and calculating their size-based offsets.
+ void PrepareCacheWrite();
+
+ void PrepareCacheWriteInternal();
+
+ void CacheWriteComplete();
+
+ void FinishContentStartup();
+
+ // Returns true if scripts added to the cache now will be encoded and
+ // written to the cache. If we've already encoded scripts for the cache
+ // write, or this is a content process which hasn't been asked to return
+ // script bytecode, this will return false.
+ bool WillWriteScripts();
+
+ // Returns a file pointer for the cache file with the given name in the
+ // current profile.
+ Result<nsCOMPtr<nsIFile>, nsresult> GetCacheFile(const nsAString& suffix);
+
+ // Waits for the given cached script to finish compiling off-thread, or
+ // decodes it synchronously on the main thread, as appropriate.
+ already_AddRefed<JS::Stencil> WaitForCachedStencil(
+ JSContext* cx, const JS::ReadOnlyDecodeOptions& options,
+ CachedStencil* script);
+
+ void StartDecodeTask(JS::Handle<JSObject*> scope);
+
+ private:
+ bool StartDecodeTask(const JS::ReadOnlyDecodeOptions& decodeOptions,
+ Vector<JS::TranscodeSource>&& decodingSources);
+
+ class DecodeTask : public Runnable {
+ ScriptPreloader* mPreloader;
+ JS::OwningDecodeOptions mDecodeOptions;
+ Vector<JS::TranscodeSource> mDecodingSources;
+
+ public:
+ DecodeTask(ScriptPreloader* preloader,
+ const JS::ReadOnlyDecodeOptions& decodeOptions,
+ Vector<JS::TranscodeSource>&& decodingSources)
+ : Runnable("ScriptPreloaderDecodeTask"),
+ mPreloader(preloader),
+ mDecodingSources(std::move(decodingSources)) {
+ mDecodeOptions.infallibleCopy(decodeOptions);
+ }
+
+ NS_IMETHOD Run() override;
+ };
+
+ friend class DecodeTask;
+
+ void onDecodedStencilQueued();
+ void OnDecodeTaskFinished();
+ void OnDecodeTaskFailed();
+
+ public:
+ void FinishOffThreadDecode();
+ void DoFinishOffThreadDecode();
+
+ already_AddRefed<nsIAsyncShutdownClient> GetShutdownBarrier();
+
+ size_t ShallowHeapSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) {
+ return (mallocSizeOf(this) +
+ mScripts.ShallowSizeOfExcludingThis(mallocSizeOf) +
+ mallocSizeOf(mSaveThread.get()) + mallocSizeOf(mProfD.get()));
+ }
+
+ using ScriptHash = nsClassHashtable<nsCStringHashKey, CachedStencil>;
+
+ template <ScriptStatus status>
+ static size_t SizeOfHashEntries(ScriptHash& scripts,
+ mozilla::MallocSizeOf mallocSizeOf) {
+ size_t size = 0;
+ for (auto elem : IterHash(scripts, Match<status>())) {
+ size += elem->HeapSizeOfIncludingThis(mallocSizeOf);
+ }
+ return size;
+ }
+
+ ScriptHash mScripts;
+
+ // True after we've shown the first window, and are no longer adding new
+ // scripts to the cache.
+ bool mStartupFinished = false;
+
+ bool mCacheInitialized = false;
+ bool mSaveComplete = false;
+ bool mDataPrepared = false;
+ // May only be changed on the main thread, while `mSaveMonitor` is held.
+ bool mCacheInvalidated MOZ_GUARDED_BY(mSaveMonitor) = false;
+
+ // The list of scripts currently being decoded in a background thread.
+ LinkedList<CachedStencil> mDecodingScripts;
+
+ // The result of the decode task.
+ //
+ // This is emplaced when starting the decode task, with the capacity equal
+ // to the number of sources.
+ //
+ // If the decode task failed, nullptr is enqueued.
+ Maybe<SPSCQueue<RefPtr<JS::Stencil>>> mDecodedStencils;
+
+ // True is main-thread is blocked and we should notify with Monitor. Access
+ // only while `mMonitor` is held.
+ bool mWaitingForDecode MOZ_GUARDED_BY(mMonitor) = false;
+
+ // The process type of the current process.
+ static ProcessType sProcessType;
+
+ // The process types for which remote processes have been initialized, and
+ // are expected to send back script data.
+ EnumSet<ProcessType> mInitializedProcesses{};
+
+ RefPtr<ScriptPreloader> mChildCache;
+ ScriptCacheChild* mChildActor = nullptr;
+
+ nsString mBaseName;
+ nsCString mContentStartupFinishedTopic;
+
+ nsCOMPtr<nsIFile> mProfD;
+ nsCOMPtr<nsIThread> mSaveThread;
+ nsCOMPtr<nsITimer> mSaveTimer;
+
+ // The mmapped cache data from this session's cache file.
+ // The instance is held by either `gCacheData` or `gChildCacheData` static
+ // fields, and its lifetime is guaranteed to be longer than ScriptPreloader
+ // instance.
+ AutoMemMap* mCacheData;
+
+ Monitor mMonitor;
+ MonitorSingleWriter mSaveMonitor MOZ_ACQUIRED_BEFORE(mMonitor);
+};
+
+} // namespace mozilla
+
+#endif // ScriptPreloader_h
diff --git a/js/xpconnect/loader/SkipCheckForBrokenURLOrZeroSized.h b/js/xpconnect/loader/SkipCheckForBrokenURLOrZeroSized.h
new file mode 100644
index 0000000000..3a413b898f
--- /dev/null
+++ b/js/xpconnect/loader/SkipCheckForBrokenURLOrZeroSized.h
@@ -0,0 +1,22 @@
+/* -*- 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_loader_SkipCheckForBrokenURLOrZeroSized_h
+#define mozilla_loader_SkipCheckForBrokenURLOrZeroSized_h
+
+#include <stdint.h> // uint8_t
+
+namespace mozilla {
+namespace loader {
+
+// Represents the `aSkipCheckForBrokenURLOrZeroSized` parameter for
+// `NS_NewChannel` function.
+enum class SkipCheckForBrokenURLOrZeroSized : uint8_t { No, Yes };
+
+} // namespace loader
+} // namespace mozilla
+
+#endif // mozilla_loader_SkipCheckForBrokenURLOrZeroSized_h
diff --git a/js/xpconnect/loader/SyncModuleLoader.cpp b/js/xpconnect/loader/SyncModuleLoader.cpp
new file mode 100644
index 0000000000..0cff7ae6ac
--- /dev/null
+++ b/js/xpconnect/loader/SyncModuleLoader.cpp
@@ -0,0 +1,259 @@
+/* -*- 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 "SyncModuleLoader.h"
+
+#include "nsISupportsImpl.h"
+
+#include "js/loader/ModuleLoadRequest.h"
+#include "js/RootingAPI.h" // JS::Rooted
+#include "js/PropertyAndElement.h" // JS_SetProperty
+#include "js/Value.h" // JS::Value, JS::NumberValue
+#include "mozJSModuleLoader.h"
+
+using namespace JS::loader;
+
+namespace mozilla {
+namespace loader {
+
+//////////////////////////////////////////////////////////////
+// SyncScriptLoader
+//////////////////////////////////////////////////////////////
+
+NS_IMPL_ISUPPORTS0(SyncScriptLoader)
+
+nsIURI* SyncScriptLoader::GetBaseURI() const { return nullptr; }
+
+void SyncScriptLoader::ReportErrorToConsole(ScriptLoadRequest* aRequest,
+ nsresult aResult) const {}
+
+void SyncScriptLoader::ReportWarningToConsole(
+ ScriptLoadRequest* aRequest, const char* aMessageName,
+ const nsTArray<nsString>& aParams) const {}
+
+nsresult SyncScriptLoader::FillCompileOptionsForRequest(
+ JSContext* cx, ScriptLoadRequest* aRequest, JS::CompileOptions* aOptions,
+ JS::MutableHandle<JSScript*> aIntroductionScript) {
+ return NS_OK;
+}
+
+//////////////////////////////////////////////////////////////
+// SyncModuleLoader
+//////////////////////////////////////////////////////////////
+
+NS_IMPL_ADDREF_INHERITED(SyncModuleLoader, JS::loader::ModuleLoaderBase)
+NS_IMPL_RELEASE_INHERITED(SyncModuleLoader, JS::loader::ModuleLoaderBase)
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(SyncModuleLoader,
+ JS::loader::ModuleLoaderBase, mLoadRequests)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SyncModuleLoader)
+NS_INTERFACE_MAP_END_INHERITING(JS::loader::ModuleLoaderBase)
+
+SyncModuleLoader::SyncModuleLoader(SyncScriptLoader* aScriptLoader,
+ nsIGlobalObject* aGlobalObject)
+ : ModuleLoaderBase(aScriptLoader, aGlobalObject) {}
+
+SyncModuleLoader::~SyncModuleLoader() { MOZ_ASSERT(mLoadRequests.isEmpty()); }
+
+already_AddRefed<ModuleLoadRequest> SyncModuleLoader::CreateStaticImport(
+ nsIURI* aURI, ModuleLoadRequest* aParent) {
+ RefPtr<SyncLoadContext> context = new SyncLoadContext();
+ RefPtr<ModuleLoadRequest> request = new ModuleLoadRequest(
+ aURI, aParent->ReferrerPolicy(), aParent->mFetchOptions,
+ dom::SRIMetadata(), aParent->mURI, context, false, /* is top level */
+ false, /* is dynamic import */
+ this, aParent->mVisitedSet, aParent->GetRootModule());
+ request->NoCacheEntryFound();
+ return request.forget();
+}
+
+already_AddRefed<ModuleLoadRequest> SyncModuleLoader::CreateDynamicImport(
+ JSContext* aCx, nsIURI* aURI, LoadedScript* aMaybeActiveScript,
+ JS::Handle<JSString*> aSpecifier, JS::Handle<JSObject*> aPromise) {
+ RefPtr<SyncLoadContext> context = new SyncLoadContext();
+ RefPtr<ModuleLoadRequest> request = new ModuleLoadRequest(
+ aURI, aMaybeActiveScript->ReferrerPolicy(),
+ aMaybeActiveScript->GetFetchOptions(), dom::SRIMetadata(),
+ aMaybeActiveScript->BaseURL(), context,
+ /* aIsTopLevel = */ true, /* aIsDynamicImport = */ true, this,
+ ModuleLoadRequest::NewVisitedSetForTopLevelImport(aURI), nullptr);
+
+ request->SetDynamicImport(aMaybeActiveScript, aSpecifier, aPromise);
+ request->NoCacheEntryFound();
+
+ return request.forget();
+}
+
+void SyncModuleLoader::OnDynamicImportStarted(ModuleLoadRequest* aRequest) {
+ MOZ_ASSERT(aRequest->IsDynamicImport());
+ MOZ_ASSERT(!mLoadRequests.Contains(aRequest));
+
+ if (aRequest->IsFetching()) {
+ // This module is newly imported.
+ //
+ // DynamicImportRequests() can contain multiple requests when a dynamic
+ // import is performed while evaluating the top-level script of other
+ // dynamic imports.
+ //
+ // mLoadRequests should be empty given evaluation is performed after
+ // handling all fetching requests.
+ MOZ_ASSERT(DynamicImportRequests().Contains(aRequest));
+ MOZ_ASSERT(mLoadRequests.isEmpty());
+
+ nsresult rv = OnFetchComplete(aRequest, NS_OK);
+ if (NS_FAILED(rv)) {
+ mLoadRequests.CancelRequestsAndClear();
+ CancelDynamicImport(aRequest, rv);
+ return;
+ }
+
+ rv = ProcessRequests();
+ if (NS_FAILED(rv)) {
+ CancelDynamicImport(aRequest, rv);
+ return;
+ }
+ } else {
+ // This module had already been imported.
+ MOZ_ASSERT(DynamicImportRequests().isEmpty());
+ MOZ_ASSERT(mLoadRequests.isEmpty());
+ }
+
+ ProcessDynamicImport(aRequest);
+}
+
+bool SyncModuleLoader::CanStartLoad(ModuleLoadRequest* aRequest,
+ nsresult* aRvOut) {
+ return mozJSModuleLoader::IsTrustedScheme(aRequest->mURI);
+}
+
+nsresult SyncModuleLoader::StartFetch(ModuleLoadRequest* aRequest) {
+ MOZ_ASSERT(aRequest->HasLoadContext());
+
+ aRequest->mBaseURL = aRequest->mURI;
+
+ // Loading script source and compilation are intertwined in
+ // mozJSModuleLoader. Perform both operations here but only report load
+ // failures. Compilation failure is reported in CompileFetchedModule.
+
+ dom::AutoJSAPI jsapi;
+ if (!jsapi.Init(GetGlobalObject())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ JSContext* cx = jsapi.cx();
+ JS::RootedScript script(cx);
+ nsresult rv =
+ mozJSModuleLoader::LoadSingleModuleScript(this, cx, aRequest, &script);
+ MOZ_ASSERT_IF(jsapi.HasException(), NS_FAILED(rv));
+ MOZ_ASSERT(bool(script) == NS_SUCCEEDED(rv));
+
+ // Check for failure to load script source and abort.
+ bool threwException = jsapi.HasException();
+ if (NS_FAILED(rv) && !threwException) {
+ nsAutoCString uri;
+ nsresult rv2 = aRequest->mURI->GetSpec(uri);
+ NS_ENSURE_SUCCESS(rv2, rv2);
+
+ JS_ReportErrorUTF8(cx, "Failed to load %s", PromiseFlatCString(uri).get());
+
+ // Remember the error for MaybeReportLoadError.
+ if (!mLoadException.initialized()) {
+ mLoadException.init(cx);
+ }
+ if (!jsapi.StealException(&mLoadException)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ if (mLoadException.isObject()) {
+ // Expose `nsresult`.
+ JS::Rooted<JS::Value> resultVal(cx, JS::NumberValue(uint32_t(rv)));
+ JS::Rooted<JSObject*> exceptionObj(cx, &mLoadException.toObject());
+ if (!JS_SetProperty(cx, exceptionObj, "result", resultVal)) {
+ // Ignore the error and keep reporting the exception without the result
+ // property.
+ JS_ClearPendingException(cx);
+ }
+ }
+
+ return rv;
+ }
+
+ // Otherwise remember the results in this context so we can report them later.
+ SyncLoadContext* context = aRequest->GetSyncLoadContext();
+ context->mRv = rv;
+ if (threwException) {
+ context->mExceptionValue.init(cx);
+ if (!jsapi.StealException(&context->mExceptionValue)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+ if (script) {
+ context->mScript.init(cx);
+ context->mScript = script;
+ }
+
+ if (!aRequest->IsDynamicImport()) {
+ // NOTE: Dynamic import is stored into mDynamicImportRequests.
+ mLoadRequests.AppendElement(aRequest);
+ }
+
+ return NS_OK;
+}
+
+nsresult SyncModuleLoader::CompileFetchedModule(
+ JSContext* aCx, JS::Handle<JSObject*> aGlobal, JS::CompileOptions& aOptions,
+ ModuleLoadRequest* aRequest, JS::MutableHandle<JSObject*> aModuleOut) {
+ // Compilation already happened in StartFetch. Report the result here.
+ SyncLoadContext* context = aRequest->GetSyncLoadContext();
+ nsresult rv = context->mRv;
+ if (context->mScript) {
+ aModuleOut.set(JS::GetModuleObject(context->mScript));
+ context->mScript = nullptr;
+ }
+ if (NS_FAILED(rv)) {
+ JS_SetPendingException(aCx, context->mExceptionValue);
+ context->mExceptionValue = JS::UndefinedValue();
+ }
+
+ MOZ_ASSERT(JS_IsExceptionPending(aCx) == NS_FAILED(rv));
+ MOZ_ASSERT(bool(aModuleOut) == NS_SUCCEEDED(rv));
+
+ return rv;
+}
+
+void SyncModuleLoader::MaybeReportLoadError(JSContext* aCx) {
+ if (JS_IsExceptionPending(aCx)) {
+ // Do not override.
+ return;
+ }
+
+ if (mLoadException.isUndefined()) {
+ return;
+ }
+
+ JS_SetPendingException(aCx, mLoadException);
+ mLoadException = JS::UndefinedValue();
+}
+
+void SyncModuleLoader::OnModuleLoadComplete(ModuleLoadRequest* aRequest) {}
+
+nsresult SyncModuleLoader::ProcessRequests() {
+ // Work list to drive module loader since this is all synchronous.
+ while (!mLoadRequests.isEmpty()) {
+ RefPtr<ScriptLoadRequest> request = mLoadRequests.StealFirst();
+ nsresult rv = OnFetchComplete(request->AsModuleRequest(), NS_OK);
+ if (NS_FAILED(rv)) {
+ mLoadRequests.CancelRequestsAndClear();
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+} // namespace loader
+} // namespace mozilla
diff --git a/js/xpconnect/loader/SyncModuleLoader.h b/js/xpconnect/loader/SyncModuleLoader.h
new file mode 100644
index 0000000000..93b7f58d39
--- /dev/null
+++ b/js/xpconnect/loader/SyncModuleLoader.h
@@ -0,0 +1,108 @@
+/* -*- 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_loader_SyncModuleLoader_h
+#define mozilla_loader_SyncModuleLoader_h
+
+#include "js/loader/LoadContextBase.h"
+#include "js/loader/ModuleLoaderBase.h"
+
+#include "SkipCheckForBrokenURLOrZeroSized.h"
+
+class mozJSModuleLoader;
+
+namespace mozilla {
+namespace loader {
+
+class SyncScriptLoader : public JS::loader::ScriptLoaderInterface {
+ public:
+ NS_DECL_ISUPPORTS
+
+ private:
+ ~SyncScriptLoader() = default;
+
+ nsIURI* GetBaseURI() const override;
+
+ void ReportErrorToConsole(ScriptLoadRequest* aRequest,
+ nsresult aResult) const override;
+
+ void ReportWarningToConsole(ScriptLoadRequest* aRequest,
+ const char* aMessageName,
+ const nsTArray<nsString>& aParams) const override;
+
+ nsresult FillCompileOptionsForRequest(
+ JSContext* cx, ScriptLoadRequest* aRequest, JS::CompileOptions* aOptions,
+ JS::MutableHandle<JSScript*> aIntroductionScript) override;
+};
+
+class SyncModuleLoader : public JS::loader::ModuleLoaderBase {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(SyncModuleLoader,
+ JS::loader::ModuleLoaderBase)
+
+ SyncModuleLoader(SyncScriptLoader* aScriptLoader,
+ nsIGlobalObject* aGlobalObject);
+
+ [[nodiscard]] nsresult ProcessRequests();
+
+ void MaybeReportLoadError(JSContext* aCx);
+
+ private:
+ ~SyncModuleLoader();
+
+ already_AddRefed<ModuleLoadRequest> CreateStaticImport(
+ nsIURI* aURI, ModuleLoadRequest* aParent) override;
+
+ already_AddRefed<ModuleLoadRequest> CreateDynamicImport(
+ JSContext* aCx, nsIURI* aURI, LoadedScript* aMaybeActiveScript,
+ JS::Handle<JSString*> aSpecifier,
+ JS::Handle<JSObject*> aPromise) override;
+
+ void OnDynamicImportStarted(ModuleLoadRequest* aRequest) override;
+
+ bool CanStartLoad(ModuleLoadRequest* aRequest, nsresult* aRvOut) override;
+
+ nsresult StartFetch(ModuleLoadRequest* aRequest) override;
+
+ nsresult CompileFetchedModule(
+ JSContext* aCx, JS::Handle<JSObject*> aGlobal,
+ JS::CompileOptions& aOptions, ModuleLoadRequest* aRequest,
+ JS::MutableHandle<JSObject*> aModuleScript) override;
+
+ void OnModuleLoadComplete(ModuleLoadRequest* aRequest) override;
+
+ JS::loader::ScriptLoadRequestList mLoadRequests;
+
+ // If any of module scripts failed to load, exception is set here until it's
+ // reported by MaybeReportLoadError.
+ JS::PersistentRooted<JS::Value> mLoadException;
+};
+
+// Data specific to SyncModuleLoader that is associated with each load
+// request.
+class SyncLoadContext : public JS::loader::LoadContextBase {
+ public:
+ SyncLoadContext() : LoadContextBase(JS::loader::ContextKind::Sync) {}
+
+ public:
+ // The result of compiling a module script. These fields are used temporarily
+ // before being passed to the module loader.
+ nsresult mRv;
+
+ SkipCheckForBrokenURLOrZeroSized mSkipCheck;
+
+ // The exception thrown during compiling a module script. These fields are
+ // used temporarily before being passed to the module loader.
+ JS::PersistentRooted<JS::Value> mExceptionValue;
+
+ JS::PersistentRooted<JSScript*> mScript;
+};
+
+} // namespace loader
+} // namespace mozilla
+
+#endif // mozilla_loader_SyncModuleLoader_h
diff --git a/js/xpconnect/loader/URLPreloader.cpp b/js/xpconnect/loader/URLPreloader.cpp
new file mode 100644
index 0000000000..ad7354e5a0
--- /dev/null
+++ b/js/xpconnect/loader/URLPreloader.cpp
@@ -0,0 +1,709 @@
+/* -*- 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 "ScriptPreloader-inl.h"
+#include "mozilla/URLPreloader.h"
+#include "mozilla/loader/AutoMemMap.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/FileUtils.h"
+#include "mozilla/IOBuffers.h"
+#include "mozilla/Logging.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Services.h"
+#include "mozilla/Try.h"
+#include "mozilla/Unused.h"
+#include "mozilla/Vector.h"
+#include "mozilla/scache/StartupCache.h"
+
+#include "crc32c.h"
+#include "MainThreadUtils.h"
+#include "nsPrintfCString.h"
+#include "nsDebug.h"
+#include "nsIFile.h"
+#include "nsIFileURL.h"
+#include "nsNetUtil.h"
+#include "nsPromiseFlatString.h"
+#include "nsProxyRelease.h"
+#include "nsThreadUtils.h"
+#include "nsXULAppAPI.h"
+#include "nsZipArchive.h"
+#include "xpcpublic.h"
+
+namespace mozilla {
+namespace {
+static LazyLogModule gURLLog("URLPreloader");
+
+#define LOG(level, ...) MOZ_LOG(gURLLog, LogLevel::level, (__VA_ARGS__))
+
+template <typename T>
+bool StartsWith(const T& haystack, const T& needle) {
+ return StringHead(haystack, needle.Length()) == needle;
+}
+} // anonymous namespace
+
+using namespace mozilla::loader;
+using mozilla::scache::StartupCache;
+
+nsresult URLPreloader::CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnonymize) {
+ MOZ_COLLECT_REPORT("explicit/url-preloader/other", KIND_HEAP, UNITS_BYTES,
+ ShallowSizeOfIncludingThis(MallocSizeOf),
+ "Memory used by the URL preloader service itself.");
+
+ for (const auto& elem : mCachedURLs.Values()) {
+ nsAutoCString pathName;
+ pathName.Append(elem->mPath);
+ // The backslashes will automatically be replaced with slashes in
+ // about:memory, without splitting each path component into a separate
+ // branch in the memory report tree.
+ pathName.ReplaceChar('/', '\\');
+
+ nsPrintfCString path("explicit/url-preloader/cached-urls/%s/[%s]",
+ elem->TypeString(), pathName.get());
+
+ aHandleReport->Callback(
+ ""_ns, path, KIND_HEAP, UNITS_BYTES,
+ elem->SizeOfIncludingThis(MallocSizeOf),
+ nsLiteralCString("Memory used to hold cache data for files which "
+ "have been read or pre-loaded during this session."),
+ aData);
+ }
+
+ return NS_OK;
+}
+
+// static
+already_AddRefed<URLPreloader> URLPreloader::Create(bool* aInitialized) {
+ // The static APIs like URLPreloader::Read work in the child process because
+ // they fall back to a synchronous read. The actual preloader must be
+ // explicitly initialized, and this should only be done in the parent.
+ MOZ_RELEASE_ASSERT(XRE_IsParentProcess());
+
+ RefPtr<URLPreloader> preloader = new URLPreloader();
+ if (preloader->InitInternal().isOk()) {
+ *aInitialized = true;
+ RegisterWeakMemoryReporter(preloader);
+ } else {
+ *aInitialized = false;
+ }
+
+ return preloader.forget();
+}
+
+URLPreloader& URLPreloader::GetSingleton() {
+ if (!sSingleton) {
+ sSingleton = Create(&sInitialized);
+ ClearOnShutdown(&sSingleton);
+ }
+
+ return *sSingleton;
+}
+
+bool URLPreloader::sInitialized = false;
+
+StaticRefPtr<URLPreloader> URLPreloader::sSingleton;
+
+URLPreloader::~URLPreloader() {
+ if (sInitialized) {
+ UnregisterWeakMemoryReporter(this);
+ sInitialized = false;
+ }
+}
+
+Result<Ok, nsresult> URLPreloader::InitInternal() {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+
+ if (Omnijar::HasOmnijar(Omnijar::GRE)) {
+ MOZ_TRY(Omnijar::GetURIString(Omnijar::GRE, mGREPrefix));
+ }
+ if (Omnijar::HasOmnijar(Omnijar::APP)) {
+ MOZ_TRY(Omnijar::GetURIString(Omnijar::APP, mAppPrefix));
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIIOService> ios = do_GetIOService(&rv);
+ MOZ_TRY(rv);
+
+ nsCOMPtr<nsIProtocolHandler> ph;
+ MOZ_TRY(ios->GetProtocolHandler("resource", getter_AddRefs(ph)));
+
+ mResProto = do_QueryInterface(ph, &rv);
+ MOZ_TRY(rv);
+
+ mChromeReg = services::GetChromeRegistry();
+ if (!mChromeReg) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ MOZ_TRY(NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(mProfD)));
+
+ return Ok();
+}
+
+URLPreloader& URLPreloader::ReInitialize() {
+ MOZ_ASSERT(sSingleton);
+ sSingleton = nullptr;
+ sSingleton = Create(&sInitialized);
+ return *sSingleton;
+}
+
+Result<nsCOMPtr<nsIFile>, nsresult> URLPreloader::GetCacheFile(
+ const nsAString& suffix) {
+ if (!mProfD) {
+ return Err(NS_ERROR_NOT_INITIALIZED);
+ }
+
+ nsCOMPtr<nsIFile> cacheFile;
+ MOZ_TRY(mProfD->Clone(getter_AddRefs(cacheFile)));
+
+ MOZ_TRY(cacheFile->AppendNative("startupCache"_ns));
+ Unused << cacheFile->Create(nsIFile::DIRECTORY_TYPE, 0777);
+
+ MOZ_TRY(cacheFile->Append(u"urlCache"_ns + suffix));
+
+ return std::move(cacheFile);
+}
+
+static const uint8_t URL_MAGIC[] = "mozURLcachev003";
+
+Result<nsCOMPtr<nsIFile>, nsresult> URLPreloader::FindCacheFile() {
+ if (StartupCache::GetIgnoreDiskCache()) {
+ return Err(NS_ERROR_ABORT);
+ }
+
+ nsCOMPtr<nsIFile> cacheFile;
+ MOZ_TRY_VAR(cacheFile, GetCacheFile(u".bin"_ns));
+
+ bool exists;
+ MOZ_TRY(cacheFile->Exists(&exists));
+ if (exists) {
+ MOZ_TRY(cacheFile->MoveTo(nullptr, u"urlCache-current.bin"_ns));
+ } else {
+ MOZ_TRY(cacheFile->SetLeafName(u"urlCache-current.bin"_ns));
+ MOZ_TRY(cacheFile->Exists(&exists));
+ if (!exists) {
+ return Err(NS_ERROR_FILE_NOT_FOUND);
+ }
+ }
+
+ return std::move(cacheFile);
+}
+
+Result<Ok, nsresult> URLPreloader::WriteCache() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(mStartupFinished);
+
+ // The script preloader might call us a second time, if it has to re-write
+ // its cache after a cache flush. We don't care about cache flushes, since
+ // our cache doesn't store any file data, only paths. And we currently clear
+ // our cached file list after the first write, which means that a second
+ // write would (aside from breaking the invariant that we never touch
+ // mCachedURLs off-main-thread after the first write, and trigger a data
+ // race) mean we get no pre-loading on the next startup.
+ if (mCacheWritten) {
+ return Ok();
+ }
+ mCacheWritten = true;
+
+ LOG(Debug, "Writing cache...");
+
+ nsCOMPtr<nsIFile> cacheFile;
+ MOZ_TRY_VAR(cacheFile, GetCacheFile(u"-new.bin"_ns));
+
+ bool exists;
+ MOZ_TRY(cacheFile->Exists(&exists));
+ if (exists) {
+ MOZ_TRY(cacheFile->Remove(false));
+ }
+
+ {
+ AutoFDClose raiiFd;
+ MOZ_TRY(cacheFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE, 0644,
+ getter_Transfers(raiiFd)));
+ const auto fd = raiiFd.get();
+
+ nsTArray<URLEntry*> entries;
+ for (const auto& entry : mCachedURLs.Values()) {
+ if (entry->mReadTime) {
+ entries.AppendElement(entry.get());
+ }
+ }
+
+ entries.Sort(URLEntry::Comparator());
+
+ OutputBuffer buf;
+ for (auto entry : entries) {
+ entry->Code(buf);
+ }
+
+ uint8_t headerSize[4];
+ LittleEndian::writeUint32(headerSize, buf.cursor());
+
+ uint8_t crc[4];
+ LittleEndian::writeUint32(crc, ComputeCrc32c(~0, buf.Get(), buf.cursor()));
+
+ MOZ_TRY(Write(fd, URL_MAGIC, sizeof(URL_MAGIC)));
+ MOZ_TRY(Write(fd, headerSize, sizeof(headerSize)));
+ MOZ_TRY(Write(fd, crc, sizeof(crc)));
+ MOZ_TRY(Write(fd, buf.Get(), buf.cursor()));
+ }
+
+ MOZ_TRY(cacheFile->MoveTo(nullptr, u"urlCache.bin"_ns));
+
+ NS_DispatchToMainThread(
+ NewRunnableMethod("URLPreloader::Cleanup", this, &URLPreloader::Cleanup));
+
+ return Ok();
+}
+
+void URLPreloader::Cleanup() { mCachedURLs.Clear(); }
+
+Result<Ok, nsresult> URLPreloader::ReadCache(
+ LinkedList<URLEntry>& pendingURLs) {
+ LOG(Debug, "Reading cache...");
+
+ nsCOMPtr<nsIFile> cacheFile;
+ MOZ_TRY_VAR(cacheFile, FindCacheFile());
+
+ AutoMemMap cache;
+ MOZ_TRY(cache.init(cacheFile));
+
+ auto size = cache.size();
+
+ uint32_t headerSize;
+ uint32_t crc;
+ if (size < sizeof(URL_MAGIC) + sizeof(headerSize) + sizeof(crc)) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ auto data = cache.get<uint8_t>();
+ auto end = data + size;
+
+ if (memcmp(URL_MAGIC, data.get(), sizeof(URL_MAGIC))) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+ data += sizeof(URL_MAGIC);
+
+ headerSize = LittleEndian::readUint32(data.get());
+ data += sizeof(headerSize);
+
+ crc = LittleEndian::readUint32(data.get());
+ data += sizeof(crc);
+
+ if (data + headerSize > end) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ if (crc != ComputeCrc32c(~0, data.get(), headerSize)) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ {
+ mMonitor.AssertCurrentThreadOwns();
+
+ auto cleanup = MakeScopeExit([&]() {
+ while (auto* elem = pendingURLs.getFirst()) {
+ elem->remove();
+ }
+ mCachedURLs.Clear();
+ });
+
+ Range<uint8_t> header(data, data + headerSize);
+ data += headerSize;
+
+ InputBuffer buf(header);
+ while (!buf.finished()) {
+ CacheKey key(buf);
+
+ LOG(Debug, "Cached file: %s %s", key.TypeString(), key.mPath.get());
+
+ // Don't bother doing anything else if the key didn't load correctly.
+ // We're going to throw it out right away, and it is possible that this
+ // leads to pendingURLs getting into a weird state.
+ if (buf.error()) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ auto entry = mCachedURLs.GetOrInsertNew(key, key);
+ entry->mResultCode = NS_ERROR_NOT_INITIALIZED;
+
+ if (entry->isInList()) {
+#ifdef NIGHTLY_BUILD
+ MOZ_DIAGNOSTIC_ASSERT(pendingURLs.contains(entry),
+ "Entry should be in pendingURLs");
+ MOZ_DIAGNOSTIC_ASSERT(key.mPath.Length() > 0,
+ "Path should be non-empty");
+ MOZ_DIAGNOSTIC_ASSERT(false, "Entry should be new and not in any list");
+#endif
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ pendingURLs.insertBack(entry);
+ }
+
+ MOZ_RELEASE_ASSERT(!buf.error(),
+ "We should have already bailed on an error");
+
+ cleanup.release();
+ }
+
+ return Ok();
+}
+
+void URLPreloader::BackgroundReadFiles() {
+ auto cleanup = MakeScopeExit([&]() {
+ auto lock = mReaderThread.Lock();
+ auto& readerThread = lock.ref();
+ NS_DispatchToMainThread(NewRunnableMethod(
+ "nsIThread::AsyncShutdown", readerThread, &nsIThread::AsyncShutdown));
+
+ readerThread = nullptr;
+ });
+
+ Vector<nsZipCursor> cursors;
+ LinkedList<URLEntry> pendingURLs;
+ {
+ MonitorAutoLock mal(mMonitor);
+
+ if (ReadCache(pendingURLs).isErr()) {
+ mReaderInitialized = true;
+ mal.NotifyAll();
+ return;
+ }
+
+ int numZipEntries = 0;
+ for (auto entry : pendingURLs) {
+ if (entry->mType != entry->TypeFile) {
+ numZipEntries++;
+ }
+ }
+ MOZ_RELEASE_ASSERT(cursors.reserve(numZipEntries));
+
+ // Initialize the zip cursors for all files in Omnijar while the monitor
+ // is locked. Omnijar is not threadsafe, so the caller of
+ // AutoBeginReading guard must ensure that no code accesses Omnijar
+ // until this segment is done. Once the cursors have been initialized,
+ // the actual reading and decompression can safely be done off-thread,
+ // as is the case for thread-retargeted jar: channels.
+ for (auto entry : pendingURLs) {
+ if (entry->mType == entry->TypeFile) {
+ continue;
+ }
+
+ RefPtr<nsZipArchive> zip = entry->Archive();
+ if (!zip) {
+ MOZ_CRASH_UNSAFE_PRINTF(
+ "Failed to get Omnijar %s archive for entry (path: \"%s\")",
+ entry->TypeString(), entry->mPath.get());
+ }
+
+ auto item = zip->GetItem(entry->mPath.get());
+ if (!item) {
+ entry->mResultCode = NS_ERROR_FILE_NOT_FOUND;
+ continue;
+ }
+
+ size_t size = item->RealSize();
+
+ entry->mData.SetLength(size);
+ auto data = entry->mData.BeginWriting();
+
+ cursors.infallibleEmplaceBack(item, zip, reinterpret_cast<uint8_t*>(data),
+ size, true);
+ }
+
+ mReaderInitialized = true;
+ mal.NotifyAll();
+ }
+
+ // Loop over the entries, read the file's contents, store them in the
+ // entry's mData pointer, and notify any waiting threads to check for
+ // completion.
+ uint32_t i = 0;
+ for (auto entry : pendingURLs) {
+ // If there is any other error code, the entry has already failed at
+ // this point, so don't bother trying to read it again.
+ if (entry->mResultCode != NS_ERROR_NOT_INITIALIZED) {
+ continue;
+ }
+
+ nsresult rv = NS_OK;
+
+ LOG(Debug, "Background reading %s file %s", entry->TypeString(),
+ entry->mPath.get());
+
+ if (entry->mType == entry->TypeFile) {
+ auto result = entry->Read();
+ if (result.isErr()) {
+ rv = result.unwrapErr();
+ }
+ } else {
+ auto& cursor = cursors[i++];
+
+ uint32_t len;
+ cursor.Copy(&len);
+ if (len != entry->mData.Length()) {
+ entry->mData.Truncate();
+ rv = NS_ERROR_FAILURE;
+ }
+ }
+
+ entry->mResultCode = rv;
+ mMonitor.NotifyAll();
+ }
+
+ // We're done reading pending entries, so clear the list.
+ pendingURLs.clear();
+}
+
+void URLPreloader::BeginBackgroundRead() {
+ auto lock = mReaderThread.Lock();
+ auto& readerThread = lock.ref();
+ if (!readerThread && !mReaderInitialized && sInitialized) {
+ nsresult rv;
+ rv = NS_NewNamedThread("BGReadURLs", getter_AddRefs(readerThread));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ nsCOMPtr<nsIRunnable> runnable =
+ NewRunnableMethod("URLPreloader::BackgroundReadFiles", this,
+ &URLPreloader::BackgroundReadFiles);
+ rv = readerThread->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // If we can't launch the task, just destroy the thread
+ readerThread = nullptr;
+ return;
+ }
+ }
+}
+
+Result<nsCString, nsresult> URLPreloader::ReadInternal(const CacheKey& key,
+ ReadType readType) {
+ if (mStartupFinished || !mReaderInitialized) {
+ URLEntry entry(key);
+
+ return entry.Read();
+ }
+
+ auto entry = mCachedURLs.GetOrInsertNew(key, key);
+
+ entry->UpdateUsedTime();
+
+ return entry->ReadOrWait(readType);
+}
+
+Result<nsCString, nsresult> URLPreloader::ReadURIInternal(nsIURI* uri,
+ ReadType readType) {
+ CacheKey key;
+ MOZ_TRY_VAR(key, ResolveURI(uri));
+
+ return ReadInternal(key, readType);
+}
+
+/* static */ Result<nsCString, nsresult> URLPreloader::Read(const CacheKey& key,
+ ReadType readType) {
+ // If we're being called before the preloader has been initialized (i.e.,
+ // before the profile has been initialized), just fall back to a synchronous
+ // read. This happens when we're reading .ini and preference files that are
+ // needed to locate and initialize the profile.
+ if (!sInitialized) {
+ return URLEntry(key).Read();
+ }
+
+ return GetSingleton().ReadInternal(key, readType);
+}
+
+/* static */ Result<nsCString, nsresult> URLPreloader::ReadURI(
+ nsIURI* uri, ReadType readType) {
+ if (!sInitialized) {
+ return Err(NS_ERROR_NOT_INITIALIZED);
+ }
+
+ return GetSingleton().ReadURIInternal(uri, readType);
+}
+
+/* static */ Result<nsCString, nsresult> URLPreloader::ReadFile(
+ nsIFile* file, ReadType readType) {
+ return Read(CacheKey(file), readType);
+}
+
+/* static */ Result<nsCString, nsresult> URLPreloader::Read(
+ FileLocation& location, ReadType readType) {
+ if (location.IsZip()) {
+ if (location.GetBaseZip()) {
+ nsCString path;
+ location.GetPath(path);
+ return ReadZip(location.GetBaseZip(), path);
+ }
+ return URLEntry::ReadLocation(location);
+ }
+
+ nsCOMPtr<nsIFile> file = location.GetBaseFile();
+ return ReadFile(file, readType);
+}
+
+/* static */ Result<nsCString, nsresult> URLPreloader::ReadZip(
+ nsZipArchive* zip, const nsACString& path, ReadType readType) {
+ // If the zip archive belongs to an Omnijar location, map it to a cache
+ // entry, and cache it as normal. Otherwise, simply read the entry
+ // synchronously, since other JAR archives are currently unsupported by the
+ // cache.
+ RefPtr<nsZipArchive> reader = Omnijar::GetReader(Omnijar::GRE);
+ if (zip == reader) {
+ CacheKey key(CacheKey::TypeGREJar, path);
+ return Read(key, readType);
+ }
+
+ reader = Omnijar::GetReader(Omnijar::APP);
+ if (zip == reader) {
+ CacheKey key(CacheKey::TypeAppJar, path);
+ return Read(key, readType);
+ }
+
+ // Not an Omnijar archive, so just read it directly.
+ FileLocation location(zip, PromiseFlatCString(path).BeginReading());
+ return URLEntry::ReadLocation(location);
+}
+
+Result<URLPreloader::CacheKey, nsresult> URLPreloader::ResolveURI(nsIURI* uri) {
+ nsCString spec;
+ nsCString scheme;
+ MOZ_TRY(uri->GetSpec(spec));
+ MOZ_TRY(uri->GetScheme(scheme));
+
+ nsCOMPtr<nsIURI> resolved;
+
+ // If the URI is a resource: or chrome: URI, first resolve it to the
+ // underlying URI that it wraps.
+ if (scheme.EqualsLiteral("resource")) {
+ MOZ_TRY(mResProto->ResolveURI(uri, spec));
+ MOZ_TRY(NS_NewURI(getter_AddRefs(resolved), spec));
+ } else if (scheme.EqualsLiteral("chrome")) {
+ MOZ_TRY(mChromeReg->ConvertChromeURL(uri, getter_AddRefs(resolved)));
+ MOZ_TRY(resolved->GetSpec(spec));
+ } else {
+ resolved = uri;
+ }
+ MOZ_TRY(resolved->GetScheme(scheme));
+
+ // Try the GRE and App Omnijar prefixes.
+ if (mGREPrefix.Length() && StartsWith(spec, mGREPrefix)) {
+ return CacheKey(CacheKey::TypeGREJar, Substring(spec, mGREPrefix.Length()));
+ }
+
+ if (mAppPrefix.Length() && StartsWith(spec, mAppPrefix)) {
+ return CacheKey(CacheKey::TypeAppJar, Substring(spec, mAppPrefix.Length()));
+ }
+
+ // Try for a file URI.
+ if (scheme.EqualsLiteral("file")) {
+ nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(resolved);
+ MOZ_ASSERT(fileURL);
+
+ nsCOMPtr<nsIFile> file;
+ MOZ_TRY(fileURL->GetFile(getter_AddRefs(file)));
+
+ nsString path;
+ MOZ_TRY(file->GetPath(path));
+
+ return CacheKey(CacheKey::TypeFile, NS_ConvertUTF16toUTF8(path));
+ }
+
+ // Not a file or Omnijar URI, so currently unsupported.
+ return Err(NS_ERROR_INVALID_ARG);
+}
+
+size_t URLPreloader::ShallowSizeOfIncludingThis(
+ mozilla::MallocSizeOf mallocSizeOf) {
+ return (mallocSizeOf(this) +
+ mAppPrefix.SizeOfExcludingThisEvenIfShared(mallocSizeOf) +
+ mGREPrefix.SizeOfExcludingThisEvenIfShared(mallocSizeOf) +
+ mCachedURLs.ShallowSizeOfExcludingThis(mallocSizeOf));
+}
+
+Result<FileLocation, nsresult> URLPreloader::CacheKey::ToFileLocation() {
+ if (mType == TypeFile) {
+ nsCOMPtr<nsIFile> file;
+ MOZ_TRY(NS_NewLocalFile(NS_ConvertUTF8toUTF16(mPath), false,
+ getter_AddRefs(file)));
+ return FileLocation(file);
+ }
+
+ RefPtr<nsZipArchive> zip = Archive();
+ return FileLocation(zip, mPath.get());
+}
+
+Result<nsCString, nsresult> URLPreloader::URLEntry::Read() {
+ FileLocation location;
+ MOZ_TRY_VAR(location, ToFileLocation());
+
+ MOZ_TRY_VAR(mData, ReadLocation(location));
+ return mData;
+}
+
+/* static */ Result<nsCString, nsresult> URLPreloader::URLEntry::ReadLocation(
+ FileLocation& location) {
+ FileLocation::Data data;
+ MOZ_TRY(location.GetData(data));
+
+ uint32_t size;
+ MOZ_TRY(data.GetSize(&size));
+
+ nsCString result;
+ result.SetLength(size);
+ MOZ_TRY(data.Copy(result.BeginWriting(), size));
+
+ return std::move(result);
+}
+
+Result<nsCString, nsresult> URLPreloader::URLEntry::ReadOrWait(
+ ReadType readType) {
+ auto now = TimeStamp::Now();
+ LOG(Info, "Reading %s\n", mPath.get());
+ auto cleanup = MakeScopeExit([&]() {
+ LOG(Info, "Read in %fms\n", (TimeStamp::Now() - now).ToMilliseconds());
+ });
+
+ if (mResultCode == NS_ERROR_NOT_INITIALIZED) {
+ MonitorAutoLock mal(GetSingleton().mMonitor);
+
+ while (mResultCode == NS_ERROR_NOT_INITIALIZED) {
+ mal.Wait();
+ }
+ }
+
+ if (mResultCode == NS_OK && mData.IsVoid()) {
+ LOG(Info, "Reading synchronously...\n");
+ return Read();
+ }
+
+ if (NS_FAILED(mResultCode)) {
+ return Err(mResultCode);
+ }
+
+ nsCString res = mData;
+
+ if (readType == Forget) {
+ mData.SetIsVoid(true);
+ }
+ return res;
+}
+
+inline URLPreloader::CacheKey::CacheKey(InputBuffer& buffer) {
+ Code(buffer);
+ MOZ_DIAGNOSTIC_ASSERT(
+ mType == TypeAppJar || mType == TypeGREJar || mType == TypeFile,
+ "mType should be valid");
+}
+
+NS_IMPL_ISUPPORTS(URLPreloader, nsIMemoryReporter)
+
+#undef LOG
+
+} // namespace mozilla
diff --git a/js/xpconnect/loader/URLPreloader.h b/js/xpconnect/loader/URLPreloader.h
new file mode 100644
index 0000000000..2573fc89a2
--- /dev/null
+++ b/js/xpconnect/loader/URLPreloader.h
@@ -0,0 +1,318 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef URLPreloader_h
+#define URLPreloader_h
+
+#include "mozilla/DataMutex.h"
+#include "mozilla/FileLocation.h"
+#include "mozilla/HashFunctions.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/Omnijar.h"
+#include "mozilla/Range.h"
+#include "mozilla/Vector.h"
+#include "mozilla/Result.h"
+#include "nsClassHashtable.h"
+#include "nsHashKeys.h"
+#include "nsIChromeRegistry.h"
+#include "nsIFile.h"
+#include "nsIURI.h"
+#include "nsIMemoryReporter.h"
+#include "nsIResProtocolHandler.h"
+#include "nsIThread.h"
+#include "nsReadableUtils.h"
+
+class nsZipArchive;
+
+namespace mozilla {
+namespace loader {
+class InputBuffer;
+}
+
+using namespace mozilla::loader;
+
+class ScriptPreloader;
+
+/**
+ * A singleton class to manage loading local URLs during startup, recording
+ * them, and pre-loading them during early startup in the next session. URLs
+ * that are not already loaded (or already being pre-loaded) when required are
+ * read synchronously from disk, and (if startup is not already complete)
+ * added to the pre-load list for the next session.
+ */
+class URLPreloader final : public nsIMemoryReporter {
+ MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf)
+
+ URLPreloader() = default;
+
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMEMORYREPORTER
+
+ static URLPreloader& GetSingleton();
+
+ // The type of read operation to perform.
+ enum ReadType {
+ // Read the file and then immediately forget its data.
+ Forget,
+ // Read the file and retain its data for the next caller.
+ Retain,
+ };
+
+ // Helpers to read the contents of files or JAR archive entries with various
+ // representations. If the preloader has not yet been initialized, or the
+ // given location is not supported by the cache, the entries will be read
+ // synchronously, and not stored in the cache.
+ static Result<nsCString, nsresult> Read(FileLocation& location,
+ ReadType readType = Forget);
+
+ static Result<nsCString, nsresult> ReadURI(nsIURI* uri,
+ ReadType readType = Forget);
+
+ static Result<nsCString, nsresult> ReadFile(nsIFile* file,
+ ReadType readType = Forget);
+
+ static Result<nsCString, nsresult> ReadZip(nsZipArchive* archive,
+ const nsACString& path,
+ ReadType readType = Forget);
+
+ void SetStartupFinished() { mStartupFinished = true; }
+
+ private:
+ struct CacheKey;
+
+ Result<nsCString, nsresult> ReadInternal(const CacheKey& key,
+ ReadType readType);
+
+ Result<nsCString, nsresult> ReadURIInternal(nsIURI* uri, ReadType readType);
+
+ Result<nsCString, nsresult> ReadFileInternal(nsIFile* file,
+ ReadType readType);
+
+ static Result<nsCString, nsresult> Read(const CacheKey& key,
+ ReadType readType);
+
+ static bool sInitialized;
+
+ static mozilla::StaticRefPtr<URLPreloader> sSingleton;
+
+ protected:
+ friend class AddonManagerStartup;
+ friend class ScriptPreloader;
+
+ virtual ~URLPreloader();
+
+ Result<Ok, nsresult> WriteCache();
+
+ static URLPreloader& ReInitialize();
+
+ // Clear leftover entries after the cache has been written.
+ void Cleanup();
+
+ // Begins reading files off-thread, and ensures that initialization has
+ // completed before leaving the current scope. The caller *must* ensure that
+ // no code on the main thread access Omnijar, either directly or indirectly,
+ // for the lifetime of this guard object.
+ struct MOZ_RAII AutoBeginReading final {
+ AutoBeginReading() { GetSingleton().BeginBackgroundRead(); }
+
+ ~AutoBeginReading() {
+ auto& reader = GetSingleton();
+
+ MonitorAutoLock mal(reader.mMonitor);
+
+ while (!reader.mReaderInitialized && URLPreloader::sInitialized) {
+ mal.Wait();
+ }
+ }
+ };
+
+ private:
+ // Represents a key for an entry in the URI cache, based on its file or JAR
+ // location.
+ struct CacheKey {
+ // The type of the entry. TypeAppJar and TypeGREJar entries are in the
+ // app-specific or toolkit Omnijar files, and are handled specially.
+ // TypeFile entries are plain files in the filesystem.
+ enum EntryType : uint8_t {
+ TypeAppJar,
+ TypeGREJar,
+ TypeFile,
+ };
+
+ CacheKey() = default;
+ CacheKey(const CacheKey& other) = default;
+
+ CacheKey(EntryType type, const nsACString& path)
+ : mType(type), mPath(path) {}
+
+ explicit CacheKey(nsIFile* file) : mType(TypeFile) {
+ nsString path;
+ MOZ_ALWAYS_SUCCEEDS(file->GetPath(path));
+ MOZ_DIAGNOSTIC_ASSERT(path.Length() > 0);
+ CopyUTF16toUTF8(path, mPath);
+ }
+
+ explicit inline CacheKey(InputBuffer& buffer);
+
+ // Encodes or decodes the cache key for storage in a session cache file.
+ template <typename Buffer>
+ void Code(Buffer& buffer) {
+ buffer.codeUint8(*reinterpret_cast<uint8_t*>(&mType));
+ buffer.codeString(mPath);
+ MOZ_DIAGNOSTIC_ASSERT(mPath.Length() > 0);
+ }
+
+ uint32_t Hash() const { return HashGeneric(mType, HashString(mPath)); }
+
+ bool operator==(const CacheKey& other) const {
+ return mType == other.mType && mPath == other.mPath;
+ }
+
+ // Returns the Omnijar type for this entry. This may *only* be called
+ // for Omnijar entries.
+ Omnijar::Type OmnijarType() {
+ switch (mType) {
+ case TypeAppJar:
+ return Omnijar::APP;
+ case TypeGREJar:
+ return Omnijar::GRE;
+ default:
+ MOZ_CRASH("Unexpected entry type");
+ return Omnijar::GRE;
+ }
+ }
+
+ const char* TypeString() const {
+ switch (mType) {
+ case TypeAppJar:
+ return "AppJar";
+ case TypeGREJar:
+ return "GREJar";
+ case TypeFile:
+ return "File";
+ }
+ MOZ_ASSERT_UNREACHABLE("no such type");
+ return "";
+ }
+
+ already_AddRefed<nsZipArchive> Archive() {
+ return Omnijar::GetReader(OmnijarType());
+ }
+
+ Result<FileLocation, nsresult> ToFileLocation();
+
+ EntryType mType = TypeFile;
+
+ // The path of the entry. For Type*Jar entries, this is the path within
+ // the Omnijar archive. For TypeFile entries, this is the full path to
+ // the file.
+ nsCString mPath{};
+ };
+
+ // Represents an entry in the URI cache.
+ struct URLEntry final : public CacheKey, public LinkedListElement<URLEntry> {
+ MOZ_IMPLICIT URLEntry(const CacheKey& key)
+ : CacheKey(key), mData(VoidCString()) {}
+
+ explicit URLEntry(nsIFile* file) : CacheKey(file) {}
+
+ // For use with nsTArray::Sort.
+ //
+ // Sorts entries by the time they were initially read during this
+ // session.
+ struct Comparator final {
+ bool Equals(const URLEntry* a, const URLEntry* b) const {
+ return a->mReadTime == b->mReadTime;
+ }
+
+ bool LessThan(const URLEntry* a, const URLEntry* b) const {
+ return a->mReadTime < b->mReadTime;
+ }
+ };
+
+ // Sets the first-used time of this file to the earlier of its current
+ // first-use time or the given timestamp.
+ void UpdateUsedTime(const TimeStamp& time = TimeStamp::Now()) {
+ if (!mReadTime || time < mReadTime) {
+ mReadTime = time;
+ }
+ }
+
+ Result<nsCString, nsresult> Read();
+ static Result<nsCString, nsresult> ReadLocation(FileLocation& location);
+
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const {
+ return (mallocSizeOf(this) +
+ mPath.SizeOfExcludingThisEvenIfShared(mallocSizeOf) +
+ mData.SizeOfExcludingThisEvenIfShared(mallocSizeOf));
+ }
+
+ // Reads the contents of the file referenced by this entry, or wait for
+ // an off-thread read operation to finish if it is currently pending,
+ // and return the file's contents.
+ Result<nsCString, nsresult> ReadOrWait(ReadType readType);
+
+ nsCString mData;
+
+ TimeStamp mReadTime{};
+
+ nsresult mResultCode = NS_OK;
+ };
+
+ // Resolves the given URI to a CacheKey, if the URI is cacheable.
+ Result<CacheKey, nsresult> ResolveURI(nsIURI* uri);
+
+ static already_AddRefed<URLPreloader> Create(bool* aInitialized);
+
+ Result<Ok, nsresult> InitInternal();
+
+ // Returns a file pointer to the (possibly nonexistent) cache file with the
+ // given suffix.
+ Result<nsCOMPtr<nsIFile>, nsresult> GetCacheFile(const nsAString& suffix);
+ // Finds the correct cache file to use for this session.
+ Result<nsCOMPtr<nsIFile>, nsresult> FindCacheFile();
+
+ Result<Ok, nsresult> ReadCache(LinkedList<URLEntry>& pendingURLs);
+
+ void BackgroundReadFiles();
+ void BeginBackgroundRead();
+
+ using HashType = nsClassHashtable<nsGenericHashKey<CacheKey>, URLEntry>;
+
+ size_t ShallowSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf);
+
+ bool mStartupFinished = false;
+ bool mReaderInitialized = false;
+
+ // Only to be accessed from the cache write thread.
+ bool mCacheWritten = false;
+
+ // The prefix URLs for files in the GRE and App omni jar archives.
+ nsCString mGREPrefix;
+ nsCString mAppPrefix;
+
+ nsCOMPtr<nsIResProtocolHandler> mResProto;
+ nsCOMPtr<nsIChromeRegistry> mChromeReg;
+ nsCOMPtr<nsIFile> mProfD;
+
+ // Note: We use a RefPtr rather than an nsCOMPtr here because the
+ // AssertNoQueryNeeded checks done by getter_AddRefs happen at a time that
+ // violate data access invariants. It's wrapped in a mutex because
+ // the reader thread needs to be able to null this out to terminate itself.
+ DataMutex<RefPtr<nsIThread>> mReaderThread{"ReaderThread"};
+
+ // A map of URL entries which have were either read this session, or read
+ // from the last session's cache file.
+ HashType mCachedURLs;
+
+ Monitor mMonitor MOZ_UNANNOTATED{"[URLPreloader::mMutex]"};
+};
+
+} // namespace mozilla
+
+#endif // URLPreloader_h
diff --git a/js/xpconnect/loader/XPCOMUtils.sys.mjs b/js/xpconnect/loader/XPCOMUtils.sys.mjs
new file mode 100644
index 0000000000..be60e222a1
--- /dev/null
+++ b/js/xpconnect/loader/XPCOMUtils.sys.mjs
@@ -0,0 +1,334 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 sts=2 et filetype=javascript
+ * 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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+let global = Cu.getGlobalForObject({});
+
+// Some global imports expose additional symbols; for example,
+// `Cu.importGlobalProperties(["MessageChannel"])` imports `MessageChannel`
+// and `MessagePort`. This table maps those extra symbols to the main
+// import name.
+const EXTRA_GLOBAL_NAME_TO_IMPORT_NAME = {
+ MessagePort: "MessageChannel",
+};
+
+/**
+ * Redefines the given property on the given object with the given
+ * value. This can be used to redefine getter properties which do not
+ * implement setters.
+ */
+function redefine(object, prop, value) {
+ Object.defineProperty(object, prop, {
+ configurable: true,
+ enumerable: true,
+ value,
+ writable: true,
+ });
+ return value;
+}
+
+export var XPCOMUtils = {
+ /**
+ * Defines a getter on a specified object that will be created upon first use.
+ *
+ * @param aObject
+ * The object to define the lazy getter on.
+ * @param aName
+ * The name of the getter to define on aObject.
+ * @param aLambda
+ * A function that returns what the getter should return. This will
+ * only ever be called once.
+ */
+ defineLazyGetter(aObject, aName, aLambda) {
+ console.warn(
+ "Please use ChromeUtils.defineLazyGetter instead of XPCOMUtils.defineLazyGetter. XPCOMUtils.defineLazyGetter will be removed soon."
+ );
+ ChromeUtils.defineLazyGetter(aObject, aName, aLambda);
+ },
+
+ /**
+ * Defines a getter on a specified object for a script. The script will not
+ * be loaded until first use.
+ *
+ * @param aObject
+ * The object to define the lazy getter on.
+ * @param aNames
+ * The name of the getter to define on aObject for the script.
+ * This can be a string if the script exports only one symbol,
+ * or an array of strings if the script can be first accessed
+ * from several different symbols.
+ * @param aResource
+ * The URL used to obtain the script.
+ */
+ defineLazyScriptGetter(aObject, aNames, aResource) {
+ if (!Array.isArray(aNames)) {
+ aNames = [aNames];
+ }
+ for (let name of aNames) {
+ Object.defineProperty(aObject, name, {
+ get() {
+ XPCOMUtils._scriptloader.loadSubScript(aResource, aObject);
+ return aObject[name];
+ },
+ set(value) {
+ redefine(aObject, name, value);
+ },
+ configurable: true,
+ enumerable: true,
+ });
+ }
+ },
+
+ /**
+ * Overrides the scriptloader definition for tests to help with globals
+ * tracking. Should only be used for tests.
+ *
+ * @param {object} aObject
+ * The alternative script loader object to use.
+ */
+ overrideScriptLoaderForTests(aObject) {
+ Cu.crashIfNotInAutomation();
+ delete this._scriptloader;
+ this._scriptloader = aObject;
+ },
+
+ /**
+ * Defines a getter property on the given object for each of the given
+ * global names as accepted by Cu.importGlobalProperties. These
+ * properties are imported into the shared JSM module global, and then
+ * copied onto the given object, no matter which global the object
+ * belongs to.
+ *
+ * @param {object} aObject
+ * The object on which to define the properties.
+ * @param {string[]} aNames
+ * The list of global properties to define.
+ */
+ defineLazyGlobalGetters(aObject, aNames) {
+ for (let name of aNames) {
+ ChromeUtils.defineLazyGetter(aObject, name, () => {
+ if (!(name in global)) {
+ let importName = EXTRA_GLOBAL_NAME_TO_IMPORT_NAME[name] || name;
+ // eslint-disable-next-line mozilla/reject-importGlobalProperties, no-unused-vars
+ Cu.importGlobalProperties([importName]);
+ }
+ return global[name];
+ });
+ }
+ },
+
+ /**
+ * Defines a getter on a specified object for a service. The service will not
+ * be obtained until first use.
+ *
+ * @param aObject
+ * The object to define the lazy getter on.
+ * @param aName
+ * The name of the getter to define on aObject for the service.
+ * @param aContract
+ * The contract used to obtain the service.
+ * @param aInterfaceName
+ * The name of the interface to query the service to.
+ */
+ defineLazyServiceGetter(aObject, aName, aContract, aInterfaceName) {
+ ChromeUtils.defineLazyGetter(aObject, aName, () => {
+ if (aInterfaceName) {
+ return Cc[aContract].getService(Ci[aInterfaceName]);
+ }
+ return Cc[aContract].getService().wrappedJSObject;
+ });
+ },
+
+ /**
+ * Defines a lazy service getter on a specified object for each
+ * property in the given object.
+ *
+ * @param aObject
+ * The object to define the lazy getter on.
+ * @param aServices
+ * An object with a property for each service to be
+ * imported, where the property name is the name of the
+ * symbol to define, and the value is a 1 or 2 element array
+ * containing the contract ID and, optionally, the interface
+ * name of the service, as passed to defineLazyServiceGetter.
+ */
+ defineLazyServiceGetters(aObject, aServices) {
+ for (let [name, service] of Object.entries(aServices)) {
+ // Note: This is hot code, and cross-compartment array wrappers
+ // are not JIT-friendly to destructuring or spread operators, so
+ // we need to use indexed access instead.
+ this.defineLazyServiceGetter(
+ aObject,
+ name,
+ service[0],
+ service[1] || null
+ );
+ }
+ },
+
+ /**
+ * Defines a lazy module getter on a specified object for each
+ * property in the given object.
+ *
+ * @param aObject
+ * The object to define the lazy getter on.
+ * @param aModules
+ * An object with a property for each module property to be
+ * imported, where the property name is the name of the
+ * imported symbol and the value is the module URI.
+ */
+ defineLazyModuleGetters(aObject, aModules) {
+ for (let [name, module] of Object.entries(aModules)) {
+ ChromeUtils.defineModuleGetter(aObject, name, module);
+ }
+ },
+
+ /**
+ * Defines a getter on a specified object for preference value. The
+ * preference is read the first time that the property is accessed,
+ * and is thereafter kept up-to-date using a preference observer.
+ *
+ * @param aObject
+ * The object to define the lazy getter on.
+ * @param aName
+ * The name of the getter property to define on aObject.
+ * @param aPreference
+ * The name of the preference to read.
+ * @param aDefaultPrefValue
+ * The default value to use, if the preference is not defined.
+ * This is the default value of the pref, before applying aTransform.
+ * @param aOnUpdate
+ * A function to call upon update. Receives as arguments
+ * `(aPreference, previousValue, newValue)`
+ * @param aTransform
+ * An optional function to transform the value. If provided,
+ * this function receives the new preference value as an argument
+ * and its return value is used by the getter.
+ */
+ defineLazyPreferenceGetter(
+ aObject,
+ aName,
+ aPreference,
+ aDefaultPrefValue = null,
+ aOnUpdate = null,
+ aTransform = val => val
+ ) {
+ if (AppConstants.DEBUG && aDefaultPrefValue !== null) {
+ let prefType = Services.prefs.getPrefType(aPreference);
+ if (prefType != Ci.nsIPrefBranch.PREF_INVALID) {
+ // The pref may get defined after the lazy getter is called
+ // at which point the code here won't know the expected type.
+ let prefTypeForDefaultValue = {
+ boolean: Ci.nsIPrefBranch.PREF_BOOL,
+ number: Ci.nsIPrefBranch.PREF_INT,
+ string: Ci.nsIPrefBranch.PREF_STRING,
+ }[typeof aDefaultPrefValue];
+ if (prefTypeForDefaultValue != prefType) {
+ throw new Error(
+ `Default value does not match preference type (Got ${prefTypeForDefaultValue}, expected ${prefType}) for ${aPreference}`
+ );
+ }
+ }
+ }
+
+ // Note: We need to keep a reference to this observer alive as long
+ // as aObject is alive. This means that all of our getters need to
+ // explicitly close over the variable that holds the object, and we
+ // cannot define a value in place of a getter after we read the
+ // preference.
+ let observer = {
+ QueryInterface: XPCU_lazyPreferenceObserverQI,
+
+ value: undefined,
+
+ observe(subject, topic, data) {
+ if (data == aPreference) {
+ if (aOnUpdate) {
+ let previous = this.value;
+
+ // Fetch and cache value.
+ this.value = undefined;
+ let latest = lazyGetter();
+ aOnUpdate(data, previous, latest);
+ } else {
+ // Empty cache, next call to the getter will cause refetch.
+ this.value = undefined;
+ }
+ }
+ },
+ };
+
+ let defineGetter = get => {
+ Object.defineProperty(aObject, aName, {
+ configurable: true,
+ enumerable: true,
+ get,
+ });
+ };
+
+ function lazyGetter() {
+ if (observer.value === undefined) {
+ let prefValue;
+ switch (Services.prefs.getPrefType(aPreference)) {
+ case Ci.nsIPrefBranch.PREF_STRING:
+ prefValue = Services.prefs.getStringPref(aPreference);
+ break;
+
+ case Ci.nsIPrefBranch.PREF_INT:
+ prefValue = Services.prefs.getIntPref(aPreference);
+ break;
+
+ case Ci.nsIPrefBranch.PREF_BOOL:
+ prefValue = Services.prefs.getBoolPref(aPreference);
+ break;
+
+ case Ci.nsIPrefBranch.PREF_INVALID:
+ prefValue = aDefaultPrefValue;
+ break;
+
+ default:
+ // This should never happen.
+ throw new Error(
+ `Error getting pref ${aPreference}; its value's type is ` +
+ `${Services.prefs.getPrefType(aPreference)}, which I don't ` +
+ `know how to handle.`
+ );
+ }
+
+ observer.value = aTransform(prefValue);
+ }
+ return observer.value;
+ }
+
+ defineGetter(() => {
+ Services.prefs.addObserver(aPreference, observer, true);
+
+ defineGetter(lazyGetter);
+ return lazyGetter();
+ });
+ },
+
+ /**
+ * Defines a non-writable property on an object.
+ */
+ defineConstant(aObj, aName, aValue) {
+ Object.defineProperty(aObj, aName, {
+ value: aValue,
+ enumerable: true,
+ writable: false,
+ });
+ },
+};
+
+ChromeUtils.defineLazyGetter(XPCOMUtils, "_scriptloader", () => {
+ return Services.scriptloader;
+});
+
+var XPCU_lazyPreferenceObserverQI = ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+]);
diff --git a/js/xpconnect/loader/moz.build b/js/xpconnect/loader/moz.build
new file mode 100644
index 0000000000..c1934490d0
--- /dev/null
+++ b/js/xpconnect/loader/moz.build
@@ -0,0 +1,67 @@
+# -*- 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/.
+
+UNIFIED_SOURCES += [
+ "AutoMemMap.cpp",
+ "ChromeScriptLoader.cpp",
+ "JSMEnvironmentProxy.cpp",
+ "ModuleEnvironmentProxy.cpp",
+ "mozJSLoaderUtils.cpp",
+ "mozJSSubScriptLoader.cpp",
+ "nsImportModule.cpp",
+ "ScriptCacheActors.cpp",
+ "ScriptPreloader.cpp",
+ "SyncModuleLoader.cpp",
+ "URLPreloader.cpp",
+]
+
+# mozJSModuleLoader.cpp cannot be built in unified mode because it uses
+# windows.h
+SOURCES += [
+ "mozJSModuleLoader.cpp",
+]
+
+IPDL_SOURCES += [
+ "PScriptCache.ipdl",
+]
+
+EXPORTS += ["nsImportModule.h"]
+
+EXPORTS.mozilla += [
+ "AutoMemMap.h",
+ "IOBuffers.h",
+ "ScriptPreloader.h",
+ "URLPreloader.h",
+]
+
+EXPORTS.mozilla.dom += [
+ "PrecompiledScript.h",
+]
+
+EXPORTS.mozilla.loader += [
+ "AutoMemMap.h",
+ "ScriptCacheActors.h",
+ "SkipCheckForBrokenURLOrZeroSized.h",
+ "SyncModuleLoader.h",
+]
+
+EXTRA_JS_MODULES += [
+ "ComponentUtils.sys.mjs",
+ "XPCOMUtils.sys.mjs",
+]
+
+FINAL_LIBRARY = "xul"
+
+LOCAL_INCLUDES += [
+ "../src",
+ "../wrappers",
+ "/dom/base",
+ "/js/loader",
+ "/xpcom/base/",
+ "/xpcom/io", # crc32c.h
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
diff --git a/js/xpconnect/loader/mozJSLoaderUtils.cpp b/js/xpconnect/loader/mozJSLoaderUtils.cpp
new file mode 100644
index 0000000000..b59da16702
--- /dev/null
+++ b/js/xpconnect/loader/mozJSLoaderUtils.cpp
@@ -0,0 +1,76 @@
+/* -*- 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/scache/StartupCache.h"
+
+#include "jsapi.h"
+#include "jsfriendapi.h"
+#include "js/CompileOptions.h"
+#include "js/Transcoding.h"
+#include "js/experimental/JSStencil.h"
+
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/Span.h"
+
+using namespace JS;
+using namespace mozilla::scache;
+using mozilla::UniquePtr;
+
+static nsresult HandleTranscodeResult(JSContext* cx,
+ JS::TranscodeResult result) {
+ if (result == JS::TranscodeResult::Ok) {
+ return NS_OK;
+ }
+
+ if (result == JS::TranscodeResult::Throw) {
+ JS_ClearPendingException(cx);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ MOZ_ASSERT(IsTranscodeFailureResult(result));
+ return NS_ERROR_FAILURE;
+}
+
+nsresult ReadCachedStencil(StartupCache* cache, nsACString& cachePath,
+ JSContext* cx,
+ const JS::ReadOnlyDecodeOptions& options,
+ JS::Stencil** stencilOut) {
+ MOZ_ASSERT(options.borrowBuffer);
+ MOZ_ASSERT(!options.usePinnedBytecode);
+
+ const char* buf;
+ uint32_t len;
+ nsresult rv =
+ cache->GetBuffer(PromiseFlatCString(cachePath).get(), &buf, &len);
+ if (NS_FAILED(rv)) {
+ return rv; // don't warn since NOT_AVAILABLE is an ok error
+ }
+
+ JS::TranscodeRange range(AsBytes(mozilla::Span(buf, len)));
+ JS::TranscodeResult code = JS::DecodeStencil(cx, options, range, stencilOut);
+ return HandleTranscodeResult(cx, code);
+}
+
+nsresult WriteCachedStencil(StartupCache* cache, nsACString& cachePath,
+ JSContext* cx, JS::Stencil* stencil) {
+ JS::TranscodeBuffer buffer;
+ JS::TranscodeResult code = JS::EncodeStencil(cx, stencil, buffer);
+ if (code != JS::TranscodeResult::Ok) {
+ return HandleTranscodeResult(cx, code);
+ }
+
+ size_t size = buffer.length();
+ if (size > UINT32_MAX) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Move the vector buffer into a unique pointer buffer.
+ mozilla::UniqueFreePtr<char[]> buf(
+ reinterpret_cast<char*>(buffer.extractOrCopyRawBuffer()));
+ nsresult rv = cache->PutBuffer(PromiseFlatCString(cachePath).get(),
+ std::move(buf), size);
+ return rv;
+}
diff --git a/js/xpconnect/loader/mozJSLoaderUtils.h b/js/xpconnect/loader/mozJSLoaderUtils.h
new file mode 100644
index 0000000000..95b4cefb2d
--- /dev/null
+++ b/js/xpconnect/loader/mozJSLoaderUtils.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 mozJSLoaderUtils_h
+#define mozJSLoaderUtils_h
+
+#include "nsString.h"
+
+#include "js/experimental/JSStencil.h"
+#include "js/CompileOptions.h" // JS::ReadOnlyDecodeOptions
+
+namespace mozilla {
+namespace scache {
+class StartupCache;
+} // namespace scache
+} // namespace mozilla
+
+nsresult ReadCachedStencil(mozilla::scache::StartupCache* cache,
+ nsACString& cachePath, JSContext* cx,
+ const JS::ReadOnlyDecodeOptions& options,
+ JS::Stencil** stencilOut);
+
+nsresult WriteCachedStencil(mozilla::scache::StartupCache* cache,
+ nsACString& cachePath, JSContext* cx,
+ JS::Stencil* stencil);
+
+#endif /* mozJSLoaderUtils_h */
diff --git a/js/xpconnect/loader/mozJSModuleLoader.cpp b/js/xpconnect/loader/mozJSModuleLoader.cpp
new file mode 100644
index 0000000000..f20306f0a0
--- /dev/null
+++ b/js/xpconnect/loader/mozJSModuleLoader.cpp
@@ -0,0 +1,2227 @@
+/* -*- 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 "ScriptLoadRequest.h"
+#include "mozilla/Assertions.h" // MOZ_ASSERT, MOZ_ASSERT_IF
+#include "mozilla/Attributes.h"
+#include "mozilla/ArrayUtils.h" // mozilla::ArrayLength
+#include "mozilla/RefPtr.h" // RefPtr, mozilla::StaticRefPtr
+#include "mozilla/Utf8.h" // mozilla::Utf8Unit
+
+#include <cstdarg>
+
+#include "mozilla/Logging.h"
+#include "mozilla/dom/RequestBinding.h"
+#ifdef ANDROID
+# include <android/log.h>
+#endif
+#ifdef XP_WIN
+# include <windows.h>
+#endif
+
+#include "jsapi.h"
+#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject
+#include "js/CharacterEncoding.h"
+#include "js/CompilationAndEvaluation.h"
+#include "js/CompileOptions.h" // JS::CompileOptions
+#include "js/ErrorReport.h" // JS_ReportErrorUTF8, JSErrorReport
+#include "js/Exception.h" // JS_ErrorFromException
+#include "js/friend/JSMEnvironment.h" // JS::ExecuteInJSMEnvironment, JS::GetJSMEnvironmentOfScriptedCaller, JS::NewJSMEnvironment
+#include "js/friend/ErrorMessages.h" // JSMSG_*
+#include "js/loader/ModuleLoadRequest.h"
+#include "js/Object.h" // JS::GetCompartment
+#include "js/Printf.h"
+#include "js/PropertyAndElement.h" // JS_DefineFunctions, JS_DefineProperty, JS_Enumerate, JS_GetElement, JS_GetProperty, JS_GetPropertyById, JS_HasOwnProperty, JS_HasOwnPropertyById, JS_SetProperty, JS_SetPropertyById
+#include "js/PropertySpec.h"
+#include "js/SourceText.h" // JS::SourceText
+#include "nsCOMPtr.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsIFile.h"
+#include "mozJSModuleLoader.h"
+#include "mozJSLoaderUtils.h"
+#include "nsIFileURL.h"
+#include "nsIJARURI.h"
+#include "nsIChannel.h"
+#include "nsIStreamListener.h"
+#include "nsNetUtil.h"
+#include "nsJSUtils.h"
+#include "xpcprivate.h"
+#include "xpcpublic.h"
+#include "nsContentUtils.h"
+#include "nsXULAppAPI.h"
+#include "WrapperFactory.h"
+#include "JSMEnvironmentProxy.h"
+#include "ModuleEnvironmentProxy.h"
+#include "JSServices.h"
+
+#include "mozilla/scache/StartupCache.h"
+#include "mozilla/scache/StartupCacheUtils.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/MacroForEach.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/ScriptPreloader.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Try.h"
+#include "mozilla/dom/AutoEntryScript.h"
+#include "mozilla/dom/ReferrerPolicyBinding.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/WorkerCommon.h" // dom::GetWorkerPrivateFromContext
+#include "mozilla/dom/WorkerPrivate.h" // dom::WorkerPrivate, dom::AutoSyncLoopHolder
+#include "mozilla/dom/WorkerRunnable.h" // dom::MainThreadStopSyncLoopRunnable
+#include "mozilla/Unused.h"
+
+using namespace mozilla;
+using namespace mozilla::scache;
+using namespace mozilla::loader;
+using namespace xpc;
+using namespace JS;
+
+#define JS_CACHE_PREFIX(aScopeType, aCompilationTarget) \
+ "jsloader/" aScopeType "/" aCompilationTarget
+
+/**
+ * Buffer sizes for serialization and deserialization of scripts.
+ * FIXME: bug #411579 (tune this macro!) Last updated: Jan 2008
+ */
+#define XPC_SERIALIZATION_BUFFER_SIZE (64 * 1024)
+#define XPC_DESERIALIZATION_BUFFER_SIZE (12 * 8192)
+
+// MOZ_LOG=JSModuleLoader:5
+static LazyLogModule gJSCLLog("JSModuleLoader");
+
+#define LOG(args) MOZ_LOG(gJSCLLog, mozilla::LogLevel::Debug, args)
+
+// Components.utils.import error messages
+#define ERROR_SCOPE_OBJ "%s - Second argument must be an object."
+#define ERROR_NO_TARGET_OBJECT "%s - Couldn't find target object for import."
+#define ERROR_NOT_PRESENT "%s - EXPORTED_SYMBOLS is not present."
+#define ERROR_NOT_AN_ARRAY "%s - EXPORTED_SYMBOLS is not an array."
+#define ERROR_GETTING_ARRAY_LENGTH \
+ "%s - Error getting array length of EXPORTED_SYMBOLS."
+#define ERROR_ARRAY_ELEMENT "%s - EXPORTED_SYMBOLS[%d] is not a string."
+#define ERROR_GETTING_SYMBOL "%s - Could not get symbol '%s'."
+#define ERROR_SETTING_SYMBOL "%s - Could not set symbol '%s' on target object."
+#define ERROR_UNINITIALIZED_SYMBOL \
+ "%s - Symbol '%s' accessed before initialization. Cyclic import?"
+
+static constexpr char JSM_Suffix[] = ".jsm";
+static constexpr size_t JSM_SuffixLength = mozilla::ArrayLength(JSM_Suffix) - 1;
+static constexpr char JSM_JS_Suffix[] = ".jsm.js";
+static constexpr size_t JSM_JS_SuffixLength =
+ mozilla::ArrayLength(JSM_JS_Suffix) - 1;
+static constexpr char JS_Suffix[] = ".js";
+static constexpr size_t JS_SuffixLength = mozilla::ArrayLength(JS_Suffix) - 1;
+static constexpr char MJS_Suffix[] = ".sys.mjs";
+static constexpr size_t MJS_SuffixLength = mozilla::ArrayLength(MJS_Suffix) - 1;
+
+static bool IsJSM(const nsACString& aLocation) {
+ if (aLocation.Length() < JSM_SuffixLength) {
+ return false;
+ }
+ const auto ext = Substring(aLocation, aLocation.Length() - JSM_SuffixLength);
+ return ext == JSM_Suffix;
+}
+
+static bool IsJS(const nsACString& aLocation) {
+ if (aLocation.Length() < JS_SuffixLength) {
+ return false;
+ }
+ const auto ext = Substring(aLocation, aLocation.Length() - JS_SuffixLength);
+ return ext == JS_Suffix;
+}
+
+static bool IsJSM_JS(const nsACString& aLocation) {
+ if (aLocation.Length() < JSM_JS_SuffixLength) {
+ return false;
+ }
+ const auto ext =
+ Substring(aLocation, aLocation.Length() - JSM_JS_SuffixLength);
+ return ext == JSM_JS_Suffix;
+}
+
+static bool IsMJS(const nsACString& aLocation) {
+ if (aLocation.Length() < MJS_SuffixLength) {
+ return false;
+ }
+ const auto ext = Substring(aLocation, aLocation.Length() - MJS_SuffixLength);
+ return ext == MJS_Suffix;
+}
+
+static void MJSToJSM(const nsACString& aLocation, nsAutoCString& aOut) {
+ MOZ_ASSERT(IsMJS(aLocation));
+ aOut = Substring(aLocation, 0, aLocation.Length() - MJS_SuffixLength);
+ aOut += JSM_Suffix;
+}
+
+static bool TryToMJS(const nsACString& aLocation, nsAutoCString& aOut) {
+ if (IsJSM(aLocation)) {
+ aOut = Substring(aLocation, 0, aLocation.Length() - JSM_SuffixLength);
+ aOut += MJS_Suffix;
+ return true;
+ }
+
+ if (IsJSM_JS(aLocation)) {
+ aOut = Substring(aLocation, 0, aLocation.Length() - JSM_JS_SuffixLength);
+ aOut += MJS_Suffix;
+ return true;
+ }
+
+ if (IsJS(aLocation)) {
+ aOut = Substring(aLocation, 0, aLocation.Length() - JS_SuffixLength);
+ aOut += MJS_Suffix;
+ return true;
+ }
+
+ return false;
+}
+
+static bool Dump(JSContext* cx, unsigned argc, Value* vp) {
+ if (!nsJSUtils::DumpEnabled()) {
+ return true;
+ }
+
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ if (args.length() == 0) {
+ return true;
+ }
+
+ RootedString str(cx, JS::ToString(cx, args[0]));
+ if (!str) {
+ return false;
+ }
+
+ JS::UniqueChars utf8str = JS_EncodeStringToUTF8(cx, str);
+ if (!utf8str) {
+ return false;
+ }
+
+ MOZ_LOG(nsContentUtils::DOMDumpLog(), mozilla::LogLevel::Debug,
+ ("[Backstage.Dump] %s", utf8str.get()));
+#ifdef ANDROID
+ __android_log_print(ANDROID_LOG_INFO, "Gecko", "%s", utf8str.get());
+#endif
+#ifdef XP_WIN
+ if (IsDebuggerPresent()) {
+ nsAutoJSString wstr;
+ if (!wstr.init(cx, str)) {
+ return false;
+ }
+ OutputDebugStringW(wstr.get());
+ }
+#endif
+ fputs(utf8str.get(), stdout);
+ fflush(stdout);
+ return true;
+}
+
+static bool Debug(JSContext* cx, unsigned argc, Value* vp) {
+#ifdef DEBUG
+ return Dump(cx, argc, vp);
+#else
+ return true;
+#endif
+}
+
+static const JSFunctionSpec gGlobalFun[] = {
+ JS_FN("dump", Dump, 1, 0), JS_FN("debug", Debug, 1, 0),
+ JS_FN("atob", Atob, 1, 0), JS_FN("btoa", Btoa, 1, 0), JS_FS_END};
+
+class MOZ_STACK_CLASS JSCLContextHelper {
+ public:
+ explicit JSCLContextHelper(JSContext* aCx);
+ ~JSCLContextHelper();
+
+ void reportErrorAfterPop(UniqueChars&& buf);
+
+ private:
+ JSContext* mContext;
+ UniqueChars mBuf;
+
+ // prevent copying and assignment
+ JSCLContextHelper(const JSCLContextHelper&) = delete;
+ const JSCLContextHelper& operator=(const JSCLContextHelper&) = delete;
+};
+
+static nsresult MOZ_FORMAT_PRINTF(2, 3)
+ ReportOnCallerUTF8(JSContext* callerContext, const char* format, ...) {
+ if (!callerContext) {
+ return NS_ERROR_FAILURE;
+ }
+
+ va_list ap;
+ va_start(ap, format);
+
+ UniqueChars buf = JS_vsmprintf(format, ap);
+ if (!buf) {
+ va_end(ap);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ JS_ReportErrorUTF8(callerContext, "%s", buf.get());
+
+ va_end(ap);
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(mozJSModuleLoader, nsIMemoryReporter)
+
+mozJSModuleLoader::mozJSModuleLoader()
+ : mImports(16),
+ mInProgressImports(16),
+ mFallbackImports(16),
+#ifdef STARTUP_RECORDER_ENABLED
+ mImportStacks(16),
+#endif
+ mLocations(16),
+ mInitialized(false),
+ mLoaderGlobal(dom::RootingCx()),
+ mServicesObj(dom::RootingCx()) {
+}
+
+#define ENSURE_DEP(name) \
+ { \
+ nsresult rv = Ensure##name(); \
+ NS_ENSURE_SUCCESS(rv, rv); \
+ }
+#define ENSURE_DEPS(...) MOZ_FOR_EACH(ENSURE_DEP, (), (__VA_ARGS__));
+#define BEGIN_ENSURE(self, ...) \
+ { \
+ if (m##self) return NS_OK; \
+ ENSURE_DEPS(__VA_ARGS__); \
+ }
+
+class MOZ_STACK_CLASS ModuleLoaderInfo {
+ public:
+ explicit ModuleLoaderInfo(const nsACString& aLocation,
+ SkipCheckForBrokenURLOrZeroSized aSkipCheck =
+ SkipCheckForBrokenURLOrZeroSized::No)
+ : mLocation(&aLocation), mIsModule(false), mSkipCheck(aSkipCheck) {}
+ explicit ModuleLoaderInfo(JS::loader::ModuleLoadRequest* aRequest)
+ : mLocation(nullptr),
+ mURI(aRequest->mURI),
+ mIsModule(true),
+ mSkipCheck(aRequest->GetSyncLoadContext()->mSkipCheck) {}
+
+ SkipCheckForBrokenURLOrZeroSized getSkipCheckForBrokenURLOrZeroSized() const {
+ return mSkipCheck;
+ }
+
+ void resetChannelWithCheckForBrokenURLOrZeroSized() {
+ MOZ_ASSERT(mSkipCheck == SkipCheckForBrokenURLOrZeroSized::Yes);
+ mSkipCheck = SkipCheckForBrokenURLOrZeroSized::No;
+ mScriptChannel = nullptr;
+ }
+
+ nsIIOService* IOService() {
+ MOZ_ASSERT(mIOService);
+ return mIOService;
+ }
+ nsresult EnsureIOService() {
+ if (mIOService) {
+ return NS_OK;
+ }
+ nsresult rv;
+ mIOService = do_GetIOService(&rv);
+ return rv;
+ }
+
+ nsIURI* URI() {
+ MOZ_ASSERT(mURI);
+ return mURI;
+ }
+ nsresult EnsureURI() {
+ BEGIN_ENSURE(URI, IOService);
+ MOZ_ASSERT(mLocation);
+ return mIOService->NewURI(*mLocation, nullptr, nullptr,
+ getter_AddRefs(mURI));
+ }
+
+ nsIChannel* ScriptChannel() {
+ MOZ_ASSERT(mScriptChannel);
+ return mScriptChannel;
+ }
+ nsresult EnsureScriptChannel() {
+ BEGIN_ENSURE(ScriptChannel, IOService, URI);
+
+ // Skip check for missing URL when handling JSM-to-ESM fallback.
+ return NS_NewChannel(
+ getter_AddRefs(mScriptChannel), mURI,
+ nsContentUtils::GetSystemPrincipal(),
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_SCRIPT,
+ /* aCookieJarSettings = */ nullptr,
+ /* aPerformanceStorage = */ nullptr,
+ /* aLoadGroup = */ nullptr, /* aCallbacks = */ nullptr,
+ nsIRequest::LOAD_NORMAL, mIOService, /* aSandboxFlags = */ 0,
+ mSkipCheck == SkipCheckForBrokenURLOrZeroSized::Yes);
+ }
+
+ nsIURI* ResolvedURI() {
+ MOZ_ASSERT(mResolvedURI);
+ return mResolvedURI;
+ }
+ nsresult EnsureResolvedURI() {
+ BEGIN_ENSURE(ResolvedURI, URI);
+ return ResolveURI(mURI, getter_AddRefs(mResolvedURI));
+ }
+
+ const nsACString& Key() {
+ MOZ_ASSERT(mLocation);
+ return *mLocation;
+ }
+
+ [[nodiscard]] nsresult GetLocation(nsCString& aLocation) {
+ nsresult rv = EnsureURI();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mURI->GetSpec(aLocation);
+ }
+
+ bool IsModule() const { return mIsModule; }
+
+ private:
+ const nsACString* mLocation;
+ nsCOMPtr<nsIIOService> mIOService;
+ nsCOMPtr<nsIURI> mURI;
+ nsCOMPtr<nsIChannel> mScriptChannel;
+ nsCOMPtr<nsIURI> mResolvedURI;
+ const bool mIsModule;
+ SkipCheckForBrokenURLOrZeroSized mSkipCheck;
+};
+
+template <typename... Args>
+static nsresult ReportOnCallerUTF8(JSCLContextHelper& helper,
+ const char* format, ModuleLoaderInfo& info,
+ Args... args) {
+ nsCString location;
+ MOZ_TRY(info.GetLocation(location));
+
+ UniqueChars buf = JS_smprintf(format, location.get(), args...);
+ if (!buf) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ helper.reportErrorAfterPop(std::move(buf));
+ return NS_ERROR_FAILURE;
+}
+
+#undef BEGIN_ENSURE
+#undef ENSURE_DEPS
+#undef ENSURE_DEP
+
+mozJSModuleLoader::~mozJSModuleLoader() {
+ MOZ_ASSERT(!mInitialized,
+ "UnloadModules() was not explicitly called before cleaning up "
+ "mozJSModuleLoader");
+
+ if (mInitialized) {
+ UnloadModules();
+ }
+}
+
+StaticRefPtr<mozJSModuleLoader> mozJSModuleLoader::sSelf;
+StaticRefPtr<mozJSModuleLoader> mozJSModuleLoader::sDevToolsLoader;
+
+void mozJSModuleLoader::FindTargetObject(JSContext* aCx,
+ MutableHandleObject aTargetObject) {
+ aTargetObject.set(JS::GetJSMEnvironmentOfScriptedCaller(aCx));
+
+ // The above could fail if the scripted caller is not a JSM (it could be a DOM
+ // scope, for instance).
+ //
+ // If the target object was not in the JSM shared global, return the global
+ // instead. This is needed when calling the subscript loader within a frame
+ // script, since it the FrameScript NSVO will have been found.
+ if (!aTargetObject ||
+ !IsLoaderGlobal(JS::GetNonCCWObjectGlobal(aTargetObject))) {
+ aTargetObject.set(JS::GetScriptedCallerGlobal(aCx));
+
+ // Return nullptr if the scripted caller is in a different compartment.
+ if (JS::GetCompartment(aTargetObject) != js::GetContextCompartment(aCx)) {
+ aTargetObject.set(nullptr);
+ }
+ }
+}
+
+void mozJSModuleLoader::InitStatics() {
+ MOZ_ASSERT(!sSelf);
+ sSelf = new mozJSModuleLoader();
+ RegisterWeakMemoryReporter(sSelf);
+ NonSharedGlobalSyncModuleLoaderScope::InitStatics();
+}
+
+void mozJSModuleLoader::UnloadLoaders() {
+ if (sSelf) {
+ sSelf->Unload();
+ }
+ if (sDevToolsLoader) {
+ sDevToolsLoader->Unload();
+ }
+}
+
+void mozJSModuleLoader::Unload() {
+ UnloadModules();
+
+ if (mModuleLoader) {
+ mModuleLoader->Shutdown();
+ mModuleLoader = nullptr;
+ }
+}
+
+void mozJSModuleLoader::ShutdownLoaders() {
+ MOZ_ASSERT(sSelf);
+ UnregisterWeakMemoryReporter(sSelf);
+ sSelf = nullptr;
+
+ if (sDevToolsLoader) {
+ UnregisterWeakMemoryReporter(sDevToolsLoader);
+ sDevToolsLoader = nullptr;
+ }
+}
+
+mozJSModuleLoader* mozJSModuleLoader::GetOrCreateDevToolsLoader() {
+ if (sDevToolsLoader) {
+ return sDevToolsLoader;
+ }
+ sDevToolsLoader = new mozJSModuleLoader();
+ RegisterWeakMemoryReporter(sDevToolsLoader);
+ return sDevToolsLoader;
+}
+
+void mozJSModuleLoader::InitSyncModuleLoaderForGlobal(
+ nsIGlobalObject* aGlobal) {
+ MOZ_ASSERT(!mLoaderGlobal);
+ MOZ_ASSERT(!mModuleLoader);
+
+ RefPtr<SyncScriptLoader> scriptLoader = new SyncScriptLoader;
+ mModuleLoader = new SyncModuleLoader(scriptLoader, aGlobal);
+ mLoaderGlobal = aGlobal->GetGlobalJSObject();
+}
+
+void mozJSModuleLoader::DisconnectSyncModuleLoaderFromGlobal() {
+ MOZ_ASSERT(mLoaderGlobal);
+ MOZ_ASSERT(mModuleLoader);
+
+ mLoaderGlobal = nullptr;
+ Unload();
+}
+
+/* static */
+bool mozJSModuleLoader::IsSharedSystemGlobal(nsIGlobalObject* aGlobal) {
+ return sSelf->IsLoaderGlobal(aGlobal->GetGlobalJSObject());
+}
+
+/* static */
+bool mozJSModuleLoader::IsDevToolsLoaderGlobal(nsIGlobalObject* aGlobal) {
+ return sDevToolsLoader &&
+ sDevToolsLoader->IsLoaderGlobal(aGlobal->GetGlobalJSObject());
+}
+
+// This requires that the keys be strings and the values be pointers.
+template <class Key, class Data, class UserData, class Converter>
+static size_t SizeOfTableExcludingThis(
+ const nsBaseHashtable<Key, Data, UserData, Converter>& aTable,
+ MallocSizeOf aMallocSizeOf) {
+ size_t n = aTable.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (const auto& entry : aTable) {
+ n += entry.GetKey().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ n += entry.GetData()->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ return n;
+}
+
+#ifdef STARTUP_RECORDER_ENABLED
+template <class Key, class Data, class UserData, class Converter>
+static size_t SizeOfStringTableExcludingThis(
+ const nsBaseHashtable<Key, Data, UserData, Converter>& aTable,
+ MallocSizeOf aMallocSizeOf) {
+ size_t n = aTable.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (const auto& entry : aTable) {
+ n += entry.GetKey().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ n += entry.GetData().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+ return n;
+}
+#endif
+
+size_t mozJSModuleLoader::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) {
+ size_t n = aMallocSizeOf(this);
+ n += SizeOfTableExcludingThis(mImports, aMallocSizeOf);
+ n += mLocations.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ n += SizeOfTableExcludingThis(mInProgressImports, aMallocSizeOf);
+ n += SizeOfTableExcludingThis(mFallbackImports, aMallocSizeOf);
+#ifdef STARTUP_RECORDER_ENABLED
+ n += SizeOfStringTableExcludingThis(mImportStacks, aMallocSizeOf);
+#endif
+ return n;
+}
+
+// Memory report paths are split on '/', with each module displayed as a
+// separate layer of a visual tree. Any slashes which are meant to belong to a
+// particular path module, rather than be used to build a hierarchy, therefore
+// need to be replaced with backslashes, which are displayed as slashes in the
+// UI.
+//
+// If `aAnonymize` is true, this function also attempts to translate any file:
+// URLs to replace the path of the GRE directory with a placeholder containing
+// no private information, and strips all other file: URIs of everything upto
+// their last `/`.
+static nsAutoCString MangleURL(const char* aURL, bool aAnonymize) {
+ nsAutoCString url(aURL);
+
+ if (aAnonymize) {
+ static nsCString greDirURI;
+ if (greDirURI.IsEmpty()) {
+ nsCOMPtr<nsIFile> file;
+ Unused << NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(file));
+ if (file) {
+ nsCOMPtr<nsIURI> uri;
+ NS_NewFileURI(getter_AddRefs(uri), file);
+ if (uri) {
+ uri->GetSpec(greDirURI);
+ RunOnShutdown([&]() { greDirURI.Truncate(0); });
+ }
+ }
+ }
+
+ url.ReplaceSubstring(greDirURI, "<GREDir>/"_ns);
+
+ if (FindInReadable("file:"_ns, url)) {
+ if (StringBeginsWith(url, "jar:file:"_ns)) {
+ int32_t idx = url.RFindChar('!');
+ url = "jar:file://<anonymized>!"_ns + Substring(url, idx);
+ } else {
+ int32_t idx = url.RFindChar('/');
+ url = "file://<anonymized>/"_ns + Substring(url, idx);
+ }
+ }
+ }
+
+ url.ReplaceChar('/', '\\');
+ return url;
+}
+
+NS_IMETHODIMP
+mozJSModuleLoader::CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnonymize) {
+ for (const auto& entry : mImports.Values()) {
+ nsAutoCString path("js-module-loader/modules/");
+ path.Append(MangleURL(entry->location, aAnonymize));
+
+ aHandleReport->Callback(""_ns, path, KIND_NONHEAP, UNITS_COUNT, 1,
+ "Loaded JS modules"_ns, aData);
+ }
+
+ return NS_OK;
+}
+
+void mozJSModuleLoader::CreateLoaderGlobal(JSContext* aCx,
+ const nsACString& aLocation,
+ MutableHandleObject aGlobal) {
+ auto backstagePass = MakeRefPtr<BackstagePass>();
+ RealmOptions options;
+ auto& creationOptions = options.creationOptions();
+
+ creationOptions.setFreezeBuiltins(true).setNewCompartmentInSystemZone();
+ if (IsDevToolsLoader()) {
+ creationOptions.setInvisibleToDebugger(true);
+ }
+ xpc::SetPrefableRealmOptions(options);
+
+ // Defer firing OnNewGlobalObject until after the __URI__ property has
+ // been defined so the JS debugger can tell what module the global is
+ // for
+ RootedObject global(aCx);
+
+#ifdef DEBUG
+ // See mozJSModuleLoader::DefineJSServices.
+ mIsInitializingLoaderGlobal = true;
+#endif
+ nsresult rv = xpc::InitClassesWithNewWrappedGlobal(
+ aCx, static_cast<nsIGlobalObject*>(backstagePass),
+ nsContentUtils::GetSystemPrincipal(), xpc::DONT_FIRE_ONNEWGLOBALHOOK,
+ options, &global);
+#ifdef DEBUG
+ mIsInitializingLoaderGlobal = false;
+#endif
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ NS_ENSURE_TRUE_VOID(global);
+
+ backstagePass->SetGlobalObject(global);
+
+ JSAutoRealm ar(aCx, global);
+ if (!JS_DefineFunctions(aCx, global, gGlobalFun)) {
+ return;
+ }
+
+ if (!CreateJSServices(aCx)) {
+ return;
+ }
+
+ if (!DefineJSServices(aCx, global)) {
+ return;
+ }
+
+ // Set the location information for the new global, so that tools like
+ // about:memory may use that information
+ xpc::SetLocationForGlobal(global, aLocation);
+
+ MOZ_ASSERT(!mModuleLoader);
+ RefPtr<SyncScriptLoader> scriptLoader = new SyncScriptLoader;
+ mModuleLoader = new SyncModuleLoader(scriptLoader, backstagePass);
+ backstagePass->InitModuleLoader(mModuleLoader);
+
+ aGlobal.set(global);
+}
+
+JSObject* mozJSModuleLoader::GetSharedGlobal(JSContext* aCx) {
+ if (!mLoaderGlobal) {
+ JS::RootedObject globalObj(aCx);
+
+ CreateLoaderGlobal(
+ aCx, IsDevToolsLoader() ? "DevTools global"_ns : "shared JSM global"_ns,
+ &globalObj);
+
+ // If we fail to create a module global this early, we're not going to
+ // get very far, so just bail out now.
+ MOZ_RELEASE_ASSERT(globalObj);
+ mLoaderGlobal = globalObj;
+
+ // AutoEntryScript required to invoke debugger hook, which is a
+ // Gecko-specific concept at present.
+ dom::AutoEntryScript aes(globalObj, "module loader report global");
+ JS_FireOnNewGlobalObject(aes.cx(), globalObj);
+ }
+
+ return mLoaderGlobal;
+}
+
+// Read script file on the main thread and pass it back to worker.
+class ScriptReaderRunnable final : public nsIRunnable,
+ public nsINamed,
+ public nsIStreamListener {
+ public:
+ ScriptReaderRunnable(dom::WorkerPrivate* aWorkerPrivate,
+ nsIEventTarget* aSyncLoopTarget,
+ const nsCString& aLocation)
+ : mLocation(aLocation),
+ mRv(NS_ERROR_FAILURE),
+ mWorkerPrivate(aWorkerPrivate),
+ mSyncLoopTarget(aSyncLoopTarget) {}
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ nsCString& Data() { return mData; }
+
+ nsresult Result() const { return mRv; }
+
+ // nsIRunnable
+
+ NS_IMETHOD
+ Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult rv = StartReadScriptFromLocation();
+ if (NS_FAILED(rv)) {
+ OnComplete(rv);
+ }
+
+ return NS_OK;
+ }
+
+ // nsINamed
+
+ NS_IMETHOD
+ GetName(nsACString& aName) override {
+ aName.AssignLiteral("ScriptReaderRunnable");
+ return NS_OK;
+ }
+
+ // nsIStreamListener
+
+ NS_IMETHOD OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aInputStream,
+ uint64_t aOffset, uint32_t aCount) override {
+ uint32_t read = 0;
+ return aInputStream->ReadSegments(AppendSegmentToData, this, aCount, &read);
+ }
+
+ // nsIRequestObserver
+
+ NS_IMETHOD OnStartRequest(nsIRequest* aRequest) override { return NS_OK; }
+
+ NS_IMETHOD OnStopRequest(nsIRequest* aRequest,
+ nsresult aStatusCode) override {
+ OnComplete(aStatusCode);
+ return NS_OK;
+ }
+
+ private:
+ ~ScriptReaderRunnable() = default;
+
+ static nsresult AppendSegmentToData(nsIInputStream* aInputStream,
+ void* aClosure, const char* aRawSegment,
+ uint32_t aToOffset, uint32_t aCount,
+ uint32_t* outWrittenCount) {
+ auto* self = static_cast<ScriptReaderRunnable*>(aClosure);
+ self->mData.Append(aRawSegment, aCount);
+ *outWrittenCount = aCount;
+ return NS_OK;
+ }
+
+ void OnComplete(nsresult aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mRv = aRv;
+
+ RefPtr<dom::MainThreadStopSyncLoopRunnable> runnable =
+ new dom::MainThreadStopSyncLoopRunnable(
+ mWorkerPrivate, std::move(mSyncLoopTarget), mRv);
+ MOZ_ALWAYS_TRUE(runnable->Dispatch());
+
+ mWorkerPrivate = nullptr;
+ mSyncLoopTarget = nullptr;
+ }
+
+ nsresult StartReadScriptFromLocation() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ ModuleLoaderInfo info(mLocation);
+ nsresult rv = info.EnsureScriptChannel();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return info.ScriptChannel()->AsyncOpen(this);
+ }
+
+ private:
+ nsAutoCString mLocation;
+ nsCString mData;
+ nsresult mRv;
+
+ // This pointer is guaranteed to be alive until OnComplete, given
+ // the worker thread is synchronously waiting with AutoSyncLoopHolder::Run
+ // until the corresponding WorkerPrivate::StopSyncLoop is called by
+ // MainThreadStopSyncLoopRunnable, which is dispatched from OnComplete.
+ dom::WorkerPrivate* mWorkerPrivate;
+
+ nsCOMPtr<nsIEventTarget> mSyncLoopTarget;
+};
+
+NS_IMPL_ISUPPORTS(ScriptReaderRunnable, nsIRunnable, nsINamed,
+ nsIStreamListener)
+
+/* static */
+nsresult mozJSModuleLoader::ReadScriptOnMainThread(JSContext* aCx,
+ const nsCString& aLocation,
+ nsCString& aData) {
+ dom::WorkerPrivate* workerPrivate = dom::GetWorkerPrivateFromContext(aCx);
+ MOZ_ASSERT(workerPrivate);
+
+ dom::AutoSyncLoopHolder syncLoop(workerPrivate, dom::WorkerStatus::Canceling);
+ nsCOMPtr<nsISerialEventTarget> syncLoopTarget =
+ syncLoop.GetSerialEventTarget();
+ if (!syncLoopTarget) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ RefPtr<ScriptReaderRunnable> runnable =
+ new ScriptReaderRunnable(workerPrivate, syncLoopTarget, aLocation);
+
+ if (NS_FAILED(NS_DispatchToMainThread(runnable))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ syncLoop.Run();
+
+ if (NS_FAILED(runnable->Result())) {
+ return runnable->Result();
+ }
+
+ aData = std::move(runnable->Data());
+
+ return NS_OK;
+}
+
+/* static */
+nsresult mozJSModuleLoader::LoadSingleModuleScriptOnWorker(
+ SyncModuleLoader* aModuleLoader, JSContext* aCx,
+ JS::loader::ModuleLoadRequest* aRequest, MutableHandleScript aScriptOut) {
+ nsAutoCString location;
+ nsresult rv = aRequest->mURI->GetSpec(location);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString data;
+ rv = ReadScriptOnMainThread(aCx, location, data);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ CompileOptions options(aCx);
+ ScriptPreloader::FillCompileOptionsForCachedStencil(options);
+ options.setFileAndLine(location.BeginReading(), 1);
+ SetModuleOptions(options);
+
+ JS::SourceText<mozilla::Utf8Unit> srcBuf;
+ if (!srcBuf.init(aCx, data.get(), data.Length(),
+ JS::SourceOwnership::Borrowed)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<JS::Stencil> stencil =
+ CompileStencil(aCx, options, srcBuf, /* aIsModule = */ true);
+ if (!stencil) {
+ return NS_ERROR_FAILURE;
+ }
+
+ aScriptOut.set(InstantiateStencil(aCx, stencil, /* aIsModule = */ true));
+
+ return NS_OK;
+}
+
+/* static */
+nsresult mozJSModuleLoader::LoadSingleModuleScript(
+ SyncModuleLoader* aModuleLoader, JSContext* aCx,
+ JS::loader::ModuleLoadRequest* aRequest, MutableHandleScript aScriptOut) {
+ AUTO_PROFILER_MARKER_TEXT(
+ "ChromeUtils.importESModule static import", JS,
+ MarkerOptions(MarkerStack::Capture(),
+ MarkerInnerWindowIdFromJSContext(aCx)),
+ nsContentUtils::TruncatedURLForDisplay(aRequest->mURI));
+
+ if (!NS_IsMainThread()) {
+ return LoadSingleModuleScriptOnWorker(aModuleLoader, aCx, aRequest,
+ aScriptOut);
+ }
+
+ ModuleLoaderInfo info(aRequest);
+ nsresult rv = info.EnsureResolvedURI();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> sourceFile;
+ rv = GetSourceFile(info.ResolvedURI(), getter_AddRefs(sourceFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool realFile = LocationIsRealFile(aRequest->mURI);
+
+ RootedScript script(aCx);
+ rv = GetScriptForLocation(aCx, info, sourceFile, realFile, aScriptOut);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+#ifdef STARTUP_RECORDER_ENABLED
+ if (aModuleLoader == sSelf->mModuleLoader) {
+ sSelf->RecordImportStack(aCx, aRequest);
+ } else if (sDevToolsLoader &&
+ aModuleLoader == sDevToolsLoader->mModuleLoader) {
+ sDevToolsLoader->RecordImportStack(aCx, aRequest);
+ } else {
+ // NOTE: Do not record import stack for non-shared globals, given the
+ // loader is associated with the global only while importing.
+ }
+#endif
+
+ return NS_OK;
+}
+
+/* static */
+nsresult mozJSModuleLoader::GetSourceFile(nsIURI* aResolvedURI,
+ nsIFile** aSourceFileOut) {
+ // Get the JAR if there is one.
+ nsCOMPtr<nsIJARURI> jarURI;
+ nsresult rv = NS_OK;
+ jarURI = do_QueryInterface(aResolvedURI, &rv);
+ nsCOMPtr<nsIFileURL> baseFileURL;
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIURI> baseURI;
+ while (jarURI) {
+ jarURI->GetJARFile(getter_AddRefs(baseURI));
+ jarURI = do_QueryInterface(baseURI, &rv);
+ }
+ baseFileURL = do_QueryInterface(baseURI, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ baseFileURL = do_QueryInterface(aResolvedURI, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return baseFileURL->GetFile(aSourceFileOut);
+}
+
+/* static */
+bool mozJSModuleLoader::LocationIsRealFile(nsIURI* aURI) {
+ // We need to be extra careful checking for URIs pointing to files.
+ // EnsureFile may not always get called, especially on resource URIs so we
+ // need to call GetFile to make sure this is a valid file.
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(aURI, &rv);
+ nsCOMPtr<nsIFile> testFile;
+ if (NS_SUCCEEDED(rv)) {
+ fileURL->GetFile(getter_AddRefs(testFile));
+ }
+
+ return bool(testFile);
+}
+
+JSObject* mozJSModuleLoader::PrepareObjectForLocation(JSContext* aCx,
+ nsIFile* aModuleFile,
+ nsIURI* aURI,
+ bool aRealFile) {
+ RootedObject globalObj(aCx, GetSharedGlobal(aCx));
+ NS_ENSURE_TRUE(globalObj, nullptr);
+ JSAutoRealm ar(aCx, globalObj);
+
+ // |thisObj| is the object we set properties on for a particular .jsm.
+ RootedObject thisObj(aCx, JS::NewJSMEnvironment(aCx));
+ NS_ENSURE_TRUE(thisObj, nullptr);
+
+ if (aRealFile) {
+ if (XRE_IsParentProcess()) {
+ RootedObject locationObj(aCx);
+
+ nsresult rv = nsXPConnect::XPConnect()->WrapNative(
+ aCx, thisObj, aModuleFile, NS_GET_IID(nsIFile),
+ locationObj.address());
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ NS_ENSURE_TRUE(locationObj, nullptr);
+
+ if (!JS_DefineProperty(aCx, thisObj, "__LOCATION__", locationObj, 0)) {
+ return nullptr;
+ }
+ }
+ }
+
+ // Expose the URI from which the script was imported through a special
+ // variable that we insert into the JSM.
+ nsAutoCString nativePath;
+ NS_ENSURE_SUCCESS(aURI->GetSpec(nativePath), nullptr);
+
+ RootedString exposedUri(
+ aCx, JS_NewStringCopyN(aCx, nativePath.get(), nativePath.Length()));
+ NS_ENSURE_TRUE(exposedUri, nullptr);
+
+ if (!JS_DefineProperty(aCx, thisObj, "__URI__", exposedUri, 0)) {
+ return nullptr;
+ }
+
+ return thisObj;
+}
+
+static mozilla::Result<nsCString, nsresult> ReadScript(
+ ModuleLoaderInfo& aInfo) {
+ MOZ_TRY(aInfo.EnsureScriptChannel());
+
+ nsCOMPtr<nsIInputStream> scriptStream;
+ MOZ_TRY(aInfo.ScriptChannel()->Open(getter_AddRefs(scriptStream)));
+
+ uint64_t len64;
+ uint32_t bytesRead;
+
+ MOZ_TRY(scriptStream->Available(&len64));
+ NS_ENSURE_TRUE(len64 < UINT32_MAX, Err(NS_ERROR_FILE_TOO_BIG));
+ NS_ENSURE_TRUE(len64, Err(NS_ERROR_FAILURE));
+ uint32_t len = (uint32_t)len64;
+
+ /* malloc an internal buf the size of the file */
+ nsCString str;
+ if (!str.SetLength(len, fallible)) {
+ return Err(NS_ERROR_OUT_OF_MEMORY);
+ }
+
+ /* read the file in one swoop */
+ MOZ_TRY(scriptStream->Read(str.BeginWriting(), len, &bytesRead));
+ if (bytesRead != len) {
+ return Err(NS_BASE_STREAM_OSERROR);
+ }
+
+ return std::move(str);
+}
+
+nsresult mozJSModuleLoader::ObjectForLocation(
+ ModuleLoaderInfo& aInfo, nsIFile* aModuleFile, MutableHandleObject aObject,
+ MutableHandleScript aTableScript, char** aLocation,
+ bool aPropagateExceptions, MutableHandleValue aException) {
+ MOZ_ASSERT(NS_IsMainThread(), "Must be on main thread.");
+
+ dom::AutoJSAPI jsapi;
+ jsapi.Init();
+ JSContext* cx = jsapi.cx();
+
+ nsresult rv = aInfo.EnsureURI();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool realFile = LocationIsRealFile(aInfo.URI());
+
+ RootedObject obj(
+ cx, PrepareObjectForLocation(cx, aModuleFile, aInfo.URI(), realFile));
+ NS_ENSURE_TRUE(obj, NS_ERROR_FAILURE);
+ MOZ_ASSERT(!JS_IsGlobalObject(obj));
+
+ JSAutoRealm ar(cx, obj);
+
+ RootedScript script(cx);
+ rv = GetScriptForLocation(cx, aInfo, aModuleFile, realFile, &script,
+ aLocation);
+ if (NS_FAILED(rv)) {
+ // Propagate the exception, if one exists. Also, don't leave the stale
+ // exception on this context.
+ if (aPropagateExceptions && jsapi.HasException()) {
+ if (!jsapi.StealException(aException)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+
+ return rv;
+ }
+
+ // Assign aObject here so that it's available to recursive imports.
+ // See bug 384168.
+ aObject.set(obj);
+
+ aTableScript.set(script);
+
+ { // Scope for AutoEntryScript
+ AutoAllowLegacyScriptExecution exemption;
+
+ // We're going to run script via JS_ExecuteScript, so we need an
+ // AutoEntryScript. This is Gecko-specific and not in any spec.
+ dom::AutoEntryScript aes(CurrentGlobalOrNull(cx),
+ "module loader load module");
+ JSContext* aescx = aes.cx();
+
+ bool executeOk = false;
+ if (JS_IsGlobalObject(obj)) {
+ JS::RootedValue rval(cx);
+ executeOk = JS_ExecuteScript(aescx, script, &rval);
+ } else {
+ executeOk = JS::ExecuteInJSMEnvironment(aescx, script, obj);
+ }
+
+ if (!executeOk) {
+ if (aPropagateExceptions && aes.HasException()) {
+ // Ignore return value because we're returning an error code
+ // anyway.
+ Unused << aes.StealException(aException);
+ }
+ aObject.set(nullptr);
+ aTableScript.set(nullptr);
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ return rv;
+}
+
+/* static */
+void mozJSModuleLoader::SetModuleOptions(CompileOptions& aOptions) {
+ aOptions.setModule();
+
+ // Top level await is not supported in synchronously loaded modules.
+ aOptions.topLevelAwait = false;
+
+ // Make all top-level `vars` available in `ModuleEnvironmentObject`.
+ aOptions.deoptimizeModuleGlobalVars = true;
+}
+
+/* static */
+nsresult mozJSModuleLoader::GetScriptForLocation(
+ JSContext* aCx, ModuleLoaderInfo& aInfo, nsIFile* aModuleFile,
+ bool aUseMemMap, MutableHandleScript aScriptOut, char** aLocationOut) {
+ // JS compilation errors are returned via an exception on the context.
+ MOZ_ASSERT(!JS_IsExceptionPending(aCx));
+
+ aScriptOut.set(nullptr);
+
+ nsAutoCString nativePath;
+ nsresult rv = aInfo.URI()->GetSpec(nativePath);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Before compiling the script, first check to see if we have it in
+ // the preloader cache or the startupcache. Note: as a rule, preloader cache
+ // errors and startupcache errors are not fatal to loading the script, since
+ // we can always slow-load.
+
+ bool storeIntoStartupCache = false;
+ StartupCache* cache = StartupCache::GetSingleton();
+
+ aInfo.EnsureResolvedURI();
+
+ nsAutoCString cachePath;
+ if (aInfo.IsModule()) {
+ rv = PathifyURI(JS_CACHE_PREFIX("non-syntactic", "module"),
+ aInfo.ResolvedURI(), cachePath);
+ } else {
+ rv = PathifyURI(JS_CACHE_PREFIX("non-syntactic", "script"),
+ aInfo.ResolvedURI(), cachePath);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ JS::DecodeOptions decodeOptions;
+ ScriptPreloader::FillDecodeOptionsForCachedStencil(decodeOptions);
+
+ RefPtr<JS::Stencil> stencil =
+ ScriptPreloader::GetSingleton().GetCachedStencil(aCx, decodeOptions,
+ cachePath);
+
+ if (!stencil && cache) {
+ ReadCachedStencil(cache, cachePath, aCx, decodeOptions,
+ getter_AddRefs(stencil));
+ if (!stencil) {
+ JS_ClearPendingException(aCx);
+
+ storeIntoStartupCache = true;
+ }
+ }
+
+ if (stencil) {
+ LOG(("Successfully loaded %s from cache\n", nativePath.get()));
+ } else {
+ // The script wasn't in the cache , so compile it now.
+ LOG(("Slow loading %s\n", nativePath.get()));
+
+ CompileOptions options(aCx);
+ ScriptPreloader::FillCompileOptionsForCachedStencil(options);
+ options.setFileAndLine(nativePath.get(), 1);
+ if (aInfo.IsModule()) {
+ SetModuleOptions(options);
+ } else {
+ options.setForceStrictMode();
+ options.setNonSyntacticScope(true);
+ }
+
+ // If we can no longer write to caches, we should stop using lazy sources
+ // and instead let normal syntax parsing occur. This can occur in content
+ // processes after the ScriptPreloader is flushed where we can read but no
+ // longer write.
+ if (!storeIntoStartupCache && !ScriptPreloader::GetSingleton().Active()) {
+ options.setSourceIsLazy(false);
+ }
+
+ if (aUseMemMap) {
+ AutoMemMap map;
+ MOZ_TRY(map.init(aModuleFile));
+
+ // Note: exceptions will get handled further down;
+ // don't early return for them here.
+ auto buf = map.get<char>();
+
+ JS::SourceText<mozilla::Utf8Unit> srcBuf;
+ if (srcBuf.init(aCx, buf.get(), map.size(),
+ JS::SourceOwnership::Borrowed)) {
+ stencil = CompileStencil(aCx, options, srcBuf, aInfo.IsModule());
+ }
+ } else {
+ nsCString str;
+ MOZ_TRY_VAR(str, ReadScript(aInfo));
+
+ JS::SourceText<mozilla::Utf8Unit> srcBuf;
+ if (srcBuf.init(aCx, str.get(), str.Length(),
+ JS::SourceOwnership::Borrowed)) {
+ stencil = CompileStencil(aCx, options, srcBuf, aInfo.IsModule());
+ }
+ }
+
+#ifdef DEBUG
+ // The above shouldn't touch any options for instantiation.
+ JS::InstantiateOptions instantiateOptions(options);
+ instantiateOptions.assertDefault();
+#endif
+
+ if (!stencil) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ aScriptOut.set(InstantiateStencil(aCx, stencil, aInfo.IsModule()));
+ if (!aScriptOut) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // ScriptPreloader::NoteScript needs to be called unconditionally, to
+ // reflect the usage into the next session's cache.
+ ScriptPreloader::GetSingleton().NoteStencil(nativePath, cachePath, stencil);
+
+ // Write to startup cache only when we didn't have any cache for the script
+ // and compiled it.
+ if (storeIntoStartupCache) {
+ MOZ_ASSERT(stencil);
+
+ // We successfully compiled the script, so cache it.
+ rv = WriteCachedStencil(cache, cachePath, aCx, stencil);
+
+ // Don't treat failure to write as fatal, since we might be working
+ // with a read-only cache.
+ if (NS_SUCCEEDED(rv)) {
+ LOG(("Successfully wrote to cache\n"));
+ } else {
+ LOG(("Failed to write to cache\n"));
+ }
+ }
+
+ /* Owned by ModuleEntry. Freed when we remove from the table. */
+ if (aLocationOut) {
+ *aLocationOut = ToNewCString(nativePath, mozilla::fallible);
+ if (!*aLocationOut) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+
+ return NS_OK;
+}
+
+void mozJSModuleLoader::UnloadModules() {
+ mInitialized = false;
+
+ if (mLoaderGlobal) {
+ MOZ_ASSERT(JS_HasExtensibleLexicalEnvironment(mLoaderGlobal));
+ JS::RootedObject lexicalEnv(dom::RootingCx(),
+ JS_ExtensibleLexicalEnvironment(mLoaderGlobal));
+ JS_SetAllNonReservedSlotsToUndefined(lexicalEnv);
+ JS_SetAllNonReservedSlotsToUndefined(mLoaderGlobal);
+ mLoaderGlobal = nullptr;
+ }
+ mServicesObj = nullptr;
+
+#ifdef STARTUP_RECORDER_ENABLED
+ mImportStacks.Clear();
+#endif
+ mFallbackImports.Clear();
+ mInProgressImports.Clear();
+ mImports.Clear();
+ mLocations.Clear();
+}
+
+/* static */
+already_AddRefed<Stencil> mozJSModuleLoader::CompileStencil(
+ JSContext* aCx, const JS::CompileOptions& aOptions,
+ JS::SourceText<mozilla::Utf8Unit>& aSource, bool aIsModule) {
+ if (aIsModule) {
+ return CompileModuleScriptToStencil(aCx, aOptions, aSource);
+ }
+
+ return CompileGlobalScriptToStencil(aCx, aOptions, aSource);
+}
+
+/* static */
+JSScript* mozJSModuleLoader::InstantiateStencil(JSContext* aCx,
+ JS::Stencil* aStencil,
+ bool aIsModule) {
+ JS::InstantiateOptions instantiateOptions;
+
+ if (aIsModule) {
+ RootedObject module(aCx);
+ module = JS::InstantiateModuleStencil(aCx, instantiateOptions, aStencil);
+ if (!module) {
+ return nullptr;
+ }
+
+ return JS::GetModuleScript(module);
+ }
+
+ return JS::InstantiateGlobalStencil(aCx, instantiateOptions, aStencil);
+}
+
+nsresult mozJSModuleLoader::ImportInto(const nsACString& registryLocation,
+ HandleValue targetValArg, JSContext* cx,
+ uint8_t optionalArgc,
+ MutableHandleValue retval) {
+ MOZ_ASSERT(nsContentUtils::IsCallerChrome());
+
+ RootedValue targetVal(cx, targetValArg);
+ RootedObject targetObject(cx, nullptr);
+
+ if (optionalArgc) {
+ // The caller passed in the optional second argument. Get it.
+ if (targetVal.isObject()) {
+ // If we're passing in something like a content DOM window, chances
+ // are the caller expects the properties to end up on the object
+ // proper and not on the Xray holder. This is dubious, but can be used
+ // during testing. Given that dumb callers can already leak JSMs into
+ // content by passing a raw content JS object (where Xrays aren't
+ // possible), we aim for consistency here. Waive xray.
+ if (WrapperFactory::IsXrayWrapper(&targetVal.toObject()) &&
+ !WrapperFactory::WaiveXrayAndWrap(cx, &targetVal)) {
+ return NS_ERROR_FAILURE;
+ }
+ targetObject = &targetVal.toObject();
+ } else if (!targetVal.isNull()) {
+ // If targetVal isNull(), we actually want to leave targetObject null.
+ // Not doing so breaks |make package|.
+ return ReportOnCallerUTF8(cx, ERROR_SCOPE_OBJ,
+ PromiseFlatCString(registryLocation).get());
+ }
+ } else {
+ FindTargetObject(cx, &targetObject);
+ if (!targetObject) {
+ return ReportOnCallerUTF8(cx, ERROR_NO_TARGET_OBJECT,
+ PromiseFlatCString(registryLocation).get());
+ }
+ }
+
+ js::AssertSameCompartment(cx, targetObject);
+
+ RootedObject global(cx);
+ nsresult rv = ImportInto(registryLocation, targetObject, cx, &global);
+
+ if (global) {
+ if (!JS_WrapObject(cx, &global)) {
+ NS_ERROR("can't wrap return value");
+ return NS_ERROR_FAILURE;
+ }
+
+ retval.setObject(*global);
+ }
+ return rv;
+}
+
+nsresult mozJSModuleLoader::IsModuleLoaded(const nsACString& aLocation,
+ bool* retval) {
+ MOZ_ASSERT(nsContentUtils::IsCallerChrome());
+
+ mInitialized = true;
+ ModuleLoaderInfo info(aLocation);
+ if (mImports.Get(info.Key())) {
+ *retval = true;
+ return NS_OK;
+ }
+
+ if (mModuleLoader) {
+ nsAutoCString mjsLocation;
+ if (!TryToMJS(aLocation, mjsLocation)) {
+ *retval = false;
+ return NS_OK;
+ }
+
+ ModuleLoaderInfo mjsInfo(mjsLocation);
+
+ nsresult rv = mjsInfo.EnsureURI();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mModuleLoader->IsModuleFetched(mjsInfo.URI())) {
+ *retval = true;
+ return NS_OK;
+ }
+ }
+
+ *retval = false;
+ return NS_OK;
+}
+
+nsresult mozJSModuleLoader::IsJSModuleLoaded(const nsACString& aLocation,
+ bool* retval) {
+ MOZ_ASSERT(nsContentUtils::IsCallerChrome());
+
+ mInitialized = true;
+ ModuleLoaderInfo info(aLocation);
+ if (mImports.Get(info.Key())) {
+ *retval = true;
+ return NS_OK;
+ }
+
+ *retval = false;
+ return NS_OK;
+}
+
+nsresult mozJSModuleLoader::IsESModuleLoaded(const nsACString& aLocation,
+ bool* retval) {
+ MOZ_ASSERT(nsContentUtils::IsCallerChrome());
+
+ mInitialized = true;
+ ModuleLoaderInfo info(aLocation);
+
+ nsresult rv = info.EnsureURI();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mModuleLoader->IsModuleFetched(info.URI())) {
+ *retval = true;
+ return NS_OK;
+ }
+
+ *retval = false;
+ return NS_OK;
+}
+
+void mozJSModuleLoader::GetLoadedModules(nsTArray<nsCString>& aLoadedModules) {
+ aLoadedModules.SetCapacity(mImports.Count());
+ for (const auto& data : mImports.Values()) {
+ aLoadedModules.AppendElement(data->location);
+ }
+}
+
+nsresult mozJSModuleLoader::GetLoadedESModules(
+ nsTArray<nsCString>& aLoadedModules) {
+ return mModuleLoader->GetFetchedModuleURLs(aLoadedModules);
+}
+
+nsresult mozJSModuleLoader::GetLoadedJSAndESModules(
+ nsTArray<nsCString>& aLoadedModules) {
+ GetLoadedModules(aLoadedModules);
+
+ nsTArray<nsCString> modules;
+ nsresult rv = GetLoadedESModules(modules);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (const auto& location : modules) {
+ if (IsMJS(location)) {
+ nsAutoCString jsmLocation;
+ // NOTE: Unconditionally convert to *.jsm. This doesn't cover *.js case
+ // but given `Cu.loadedModules` is rarely used for system modules,
+ // this won't cause much compat issue.
+ MJSToJSM(location, jsmLocation);
+ aLoadedModules.AppendElement(jsmLocation);
+ }
+ }
+
+ return NS_OK;
+}
+
+#ifdef STARTUP_RECORDER_ENABLED
+void mozJSModuleLoader::RecordImportStack(JSContext* aCx,
+ const nsACString& aLocation) {
+ if (!Preferences::GetBool("browser.startup.record", false)) {
+ return;
+ }
+
+ mImportStacks.InsertOrUpdate(
+ aLocation, xpc_PrintJSStack(aCx, false, false, false).get());
+}
+
+void mozJSModuleLoader::RecordImportStack(
+ JSContext* aCx, JS::loader::ModuleLoadRequest* aRequest) {
+ if (!Preferences::GetBool("browser.startup.record", false)) {
+ return;
+ }
+
+ nsAutoCString location;
+ nsresult rv = aRequest->mURI->GetSpec(location);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ auto recordJSStackOnly = [&]() {
+ mImportStacks.InsertOrUpdate(
+ location, xpc_PrintJSStack(aCx, false, false, false).get());
+ };
+
+ if (aRequest->IsTopLevel()) {
+ recordJSStackOnly();
+ return;
+ }
+
+ nsAutoCString importerSpec;
+ rv = aRequest->mReferrer->GetSpec(importerSpec);
+ if (NS_FAILED(rv)) {
+ recordJSStackOnly();
+ return;
+ }
+
+ ModuleLoaderInfo importerInfo(importerSpec);
+ auto importerStack = mImportStacks.Lookup(importerInfo.Key());
+ if (!importerStack) {
+ // The importer's stack is not collected, possibly due to OOM.
+ recordJSStackOnly();
+ return;
+ }
+
+ nsAutoCString stack;
+
+ stack += "* import [\"";
+ stack += importerSpec;
+ stack += "\"]\n";
+ stack += *importerStack;
+
+ mImportStacks.InsertOrUpdate(location, stack);
+}
+#endif
+
+nsresult mozJSModuleLoader::GetModuleImportStack(const nsACString& aLocation,
+ nsACString& retval) {
+#ifdef STARTUP_RECORDER_ENABLED
+ MOZ_ASSERT(nsContentUtils::IsCallerChrome());
+
+ // When querying the DevTools loader, it may not be initialized yet
+ if (!mInitialized) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ModuleLoaderInfo info(aLocation);
+ auto str = mImportStacks.Lookup(info.Key());
+ if (!str) {
+ return NS_ERROR_FAILURE;
+ }
+
+ retval = *str;
+ return NS_OK;
+#else
+ return NS_ERROR_NOT_IMPLEMENTED;
+#endif
+}
+
+nsresult mozJSModuleLoader::ImportInto(const nsACString& aLocation,
+ HandleObject targetObj, JSContext* cx,
+ MutableHandleObject vp) {
+ vp.set(nullptr);
+
+ JS::RootedObject exports(cx);
+ MOZ_TRY(Import(cx, aLocation, vp, &exports, !targetObj));
+
+ if (targetObj) {
+ JS::Rooted<JS::IdVector> ids(cx, JS::IdVector(cx));
+ if (!JS_Enumerate(cx, exports, &ids)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ JS::RootedValue value(cx);
+ JS::RootedId id(cx);
+ for (jsid idVal : ids) {
+ id = idVal;
+ if (!JS_GetPropertyById(cx, exports, id, &value) ||
+ !JS_SetPropertyById(cx, targetObj, id, value)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult mozJSModuleLoader::ExtractExports(JSContext* aCx,
+ ModuleLoaderInfo& aInfo,
+ ModuleEntry* aMod,
+ JS::MutableHandleObject aExports) {
+ // cxhelper must be created before jsapi, so that jsapi is destroyed and
+ // pops any context it has pushed before we report to the caller context.
+ JSCLContextHelper cxhelper(aCx);
+
+ // Even though we are calling JS_SetPropertyById on targetObj, we want
+ // to ensure that we never run script here, so we use an AutoJSAPI and
+ // not an AutoEntryScript.
+ dom::AutoJSAPI jsapi;
+ jsapi.Init();
+ JSContext* cx = jsapi.cx();
+ JSAutoRealm ar(cx, aMod->obj);
+
+ RootedValue symbols(cx);
+ {
+ RootedObject obj(
+ cx, ResolveModuleObjectProperty(cx, aMod->obj, "EXPORTED_SYMBOLS"));
+ if (!obj || !JS_GetProperty(cx, obj, "EXPORTED_SYMBOLS", &symbols)) {
+ return ReportOnCallerUTF8(cxhelper, ERROR_NOT_PRESENT, aInfo);
+ }
+ }
+
+ bool isArray;
+ if (!JS::IsArrayObject(cx, symbols, &isArray)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!isArray) {
+ return ReportOnCallerUTF8(cxhelper, ERROR_NOT_AN_ARRAY, aInfo);
+ }
+
+ RootedObject symbolsObj(cx, &symbols.toObject());
+
+ // Iterate over symbols array, installing symbols on targetObj:
+
+ uint32_t symbolCount = 0;
+ if (!JS::GetArrayLength(cx, symbolsObj, &symbolCount)) {
+ return ReportOnCallerUTF8(cxhelper, ERROR_GETTING_ARRAY_LENGTH, aInfo);
+ }
+
+#ifdef DEBUG
+ nsAutoCString logBuffer;
+#endif
+
+ aExports.set(JS_NewPlainObject(cx));
+ if (!aExports) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ bool missing = false;
+
+ RootedValue value(cx);
+ RootedId symbolId(cx);
+ RootedObject symbolHolder(cx);
+ for (uint32_t i = 0; i < symbolCount; ++i) {
+ if (!JS_GetElement(cx, symbolsObj, i, &value) || !value.isString() ||
+ !JS_ValueToId(cx, value, &symbolId)) {
+ return ReportOnCallerUTF8(cxhelper, ERROR_ARRAY_ELEMENT, aInfo, i);
+ }
+
+ symbolHolder = ResolveModuleObjectPropertyById(cx, aMod->obj, symbolId);
+ if (!symbolHolder ||
+ !JS_GetPropertyById(cx, symbolHolder, symbolId, &value)) {
+ RootedString symbolStr(cx, symbolId.toString());
+ JS::UniqueChars bytes = JS_EncodeStringToUTF8(cx, symbolStr);
+ if (!bytes) {
+ return NS_ERROR_FAILURE;
+ }
+ return ReportOnCallerUTF8(cxhelper, ERROR_GETTING_SYMBOL, aInfo,
+ bytes.get());
+ }
+
+ // It's possible |value| is the uninitialized lexical MagicValue when
+ // there's a cyclic import: const obj = ChromeUtils.import("parent.jsm").
+ if (value.isMagic(JS_UNINITIALIZED_LEXICAL)) {
+ RootedString symbolStr(cx, symbolId.toString());
+ JS::UniqueChars bytes = JS_EncodeStringToUTF8(cx, symbolStr);
+ if (!bytes) {
+ return NS_ERROR_FAILURE;
+ }
+ return ReportOnCallerUTF8(cxhelper, ERROR_UNINITIALIZED_SYMBOL, aInfo,
+ bytes.get());
+ }
+
+ if (value.isUndefined()) {
+ missing = true;
+ }
+
+ if (!JS_SetPropertyById(cx, aExports, symbolId, value)) {
+ RootedString symbolStr(cx, symbolId.toString());
+ JS::UniqueChars bytes = JS_EncodeStringToUTF8(cx, symbolStr);
+ if (!bytes) {
+ return NS_ERROR_FAILURE;
+ }
+ return ReportOnCallerUTF8(cxhelper, ERROR_GETTING_SYMBOL, aInfo,
+ bytes.get());
+ }
+#ifdef DEBUG
+ if (i == 0) {
+ logBuffer.AssignLiteral("Installing symbols [ ");
+ }
+ JS::UniqueChars bytes = JS_EncodeStringToLatin1(cx, symbolId.toString());
+ if (!!bytes) {
+ logBuffer.Append(bytes.get());
+ }
+ logBuffer.Append(' ');
+ if (i == symbolCount - 1) {
+ nsCString location;
+ MOZ_TRY(aInfo.GetLocation(location));
+ LOG(("%s] from %s\n", logBuffer.get(), location.get()));
+ }
+#endif
+ }
+
+ // Don't cache the exports object if any of its exported symbols are
+ // missing. If the module hasn't finished loading yet, they may be
+ // defined the next time we try to import it.
+ if (!missing) {
+ aMod->exports = aExports;
+ }
+ return NS_OK;
+}
+
+/* static */
+bool mozJSModuleLoader::IsTrustedScheme(nsIURI* aURI) {
+ return aURI->SchemeIs("resource") || aURI->SchemeIs("chrome");
+}
+
+nsresult mozJSModuleLoader::Import(JSContext* aCx, const nsACString& aLocation,
+ JS::MutableHandleObject aModuleGlobal,
+ JS::MutableHandleObject aModuleExports,
+ bool aIgnoreExports) {
+ mInitialized = true;
+
+ AUTO_PROFILER_MARKER_TEXT(
+ "ChromeUtils.import", JS,
+ MarkerOptions(MarkerStack::Capture(),
+ MarkerInnerWindowIdFromJSContext(aCx)),
+ Substring(aLocation, 0, std::min(size_t(128), aLocation.Length())));
+
+ // The JSM may already be ESM-ified, and in that case the load is expected
+ // to fail. Suppress the error message, the crash, and also the telemetry
+ // event for the failure.
+ //
+ // If this load fails, it will be redirected to `.sys.mjs` URL
+ // in TryFallbackToImportESModule, and if the redirect also fails,
+ // the load is performed again below, with the check enabled.
+ ModuleLoaderInfo info(aLocation, SkipCheckForBrokenURLOrZeroSized::Yes);
+
+ nsresult rv;
+ ModuleEntry* mod;
+ UniquePtr<ModuleEntry> newEntry;
+ if (!mImports.Get(info.Key(), &mod) &&
+ !mInProgressImports.Get(info.Key(), &mod)) {
+ // We're trying to import a new JSM, but we're late in shutdown and this
+ // will likely not succeed and might even crash, so fail here.
+ if (PastShutdownPhase(ShutdownPhase::XPCOMShutdownFinal)) {
+ return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
+ }
+
+ // If we've hit file-not-found and fallback was successful,
+ // return the cached data.
+ bool aFound;
+ rv = TryCachedFallbackToImportESModule(
+ aCx, aLocation, aModuleGlobal, aModuleExports, aIgnoreExports, &aFound);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (aFound) {
+ return NS_OK;
+ }
+
+ newEntry = MakeUnique<ModuleEntry>(RootingContext::get(aCx));
+
+ // Note: This implies EnsureURI().
+ MOZ_TRY(info.EnsureResolvedURI());
+
+ // Reject imports from untrusted sources.
+ if (!IsTrustedScheme(info.URI())) {
+ return NS_ERROR_DOM_SECURITY_ERR;
+ }
+
+ nsCOMPtr<nsIFile> sourceFile;
+ rv = GetSourceFile(info.ResolvedURI(), getter_AddRefs(sourceFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = info.ResolvedURI()->GetSpec(newEntry->resolvedURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString* existingPath;
+ if (mLocations.Get(newEntry->resolvedURL, &existingPath) &&
+ *existingPath != info.Key()) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ mLocations.InsertOrUpdate(newEntry->resolvedURL,
+ MakeUnique<nsCString>(info.Key()));
+
+ RootedValue exception(aCx);
+ {
+ mInProgressImports.InsertOrUpdate(info.Key(), newEntry.get());
+ auto cleanup =
+ MakeScopeExit([&]() { mInProgressImports.Remove(info.Key()); });
+
+ rv = ObjectForLocation(info, sourceFile, &newEntry->obj,
+ &newEntry->thisObjectKey, &newEntry->location,
+ true, &exception);
+ }
+
+ if (NS_FAILED(rv)) {
+ mLocations.Remove(newEntry->resolvedURL);
+ if (!exception.isUndefined()) {
+ // An exception was thrown during compilation. Propagate it
+ // out to our caller so they can report it.
+ bool isModuleSyntaxError = false;
+
+ if (exception.isObject()) {
+ JS::Rooted<JSObject*> exceptionObj(aCx, &exception.toObject());
+ JSAutoRealm ar(aCx, exceptionObj);
+ JSErrorReport* report = JS_ErrorFromException(aCx, exceptionObj);
+ if (report) {
+ switch (report->errorNumber) {
+ case JSMSG_IMPORT_DECL_AT_TOP_LEVEL:
+ case JSMSG_EXPORT_DECL_AT_TOP_LEVEL:
+ // If the exception is related to module syntax, it's most
+ // likely because of misuse of API.
+ // Provide better error message.
+ isModuleSyntaxError = true;
+
+ JS_ReportErrorUTF8(aCx,
+ "ChromeUtils.import is called against "
+ "an ES module script (%s). Please use "
+ "ChromeUtils.importESModule instead "
+ "(SyntaxError: %s)",
+ aLocation.BeginReading(),
+ report->message().c_str());
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ if (!isModuleSyntaxError) {
+ if (!JS_WrapValue(aCx, &exception)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ JS_SetPendingException(aCx, exception);
+ }
+
+ return NS_ERROR_FAILURE;
+ }
+
+ if (rv == NS_ERROR_FILE_NOT_FOUND || rv == NS_ERROR_FILE_ACCESS_DENIED) {
+ // NS_ERROR_FILE_ACCESS_DENIED happens if the access is blocked by
+ // sandbox.
+ rv = TryFallbackToImportESModule(aCx, aLocation, aModuleGlobal,
+ aModuleExports, aIgnoreExports);
+
+ if (rv == NS_ERROR_FILE_NOT_FOUND ||
+ rv == NS_ERROR_FILE_ACCESS_DENIED) {
+ // Both JSM and ESM are not found, with the check inside necko
+ // skipped (See EnsureScriptChannel and mSkipCheck).
+ //
+ // Perform the load again with the check enabled, so that
+ // logging, crash-on-autonation, and telemetry event happen.
+ if (NS_SUCCEEDED(info.EnsureURI()) &&
+ !LocationIsRealFile(info.URI())) {
+ info.resetChannelWithCheckForBrokenURLOrZeroSized();
+ (void)ReadScript(info);
+ }
+ }
+
+ return rv;
+ }
+
+ // Something failed, but we don't know what it is, guess.
+ return NS_ERROR_FILE_NOT_FOUND;
+ }
+
+#ifdef STARTUP_RECORDER_ENABLED
+ RecordImportStack(aCx, aLocation);
+#endif
+
+ mod = newEntry.get();
+ }
+
+ MOZ_ASSERT(mod->obj, "Import table contains entry with no object");
+ JS::RootedObject globalProxy(aCx);
+ {
+ JSAutoRealm ar(aCx, mod->obj);
+
+ globalProxy = CreateJSMEnvironmentProxy(aCx, mod->obj);
+ if (!globalProxy) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ if (!JS_WrapObject(aCx, &globalProxy)) {
+ return NS_ERROR_FAILURE;
+ }
+ aModuleGlobal.set(globalProxy);
+
+ JS::RootedObject exports(aCx, mod->exports);
+ if (!exports && !aIgnoreExports) {
+ MOZ_TRY(ExtractExports(aCx, info, mod, &exports));
+ }
+
+ if (exports && !JS_WrapObject(aCx, &exports)) {
+ mLocations.Remove(newEntry->resolvedURL);
+ return NS_ERROR_FAILURE;
+ }
+ aModuleExports.set(exports);
+
+ // Cache this module for later
+ if (newEntry) {
+ mImports.InsertOrUpdate(info.Key(), std::move(newEntry));
+ }
+
+ return NS_OK;
+}
+
+nsresult mozJSModuleLoader::TryFallbackToImportESModule(
+ JSContext* aCx, const nsACString& aLocation,
+ JS::MutableHandleObject aModuleGlobal,
+ JS::MutableHandleObject aModuleExports, bool aIgnoreExports) {
+ nsAutoCString mjsLocation;
+ if (!TryToMJS(aLocation, mjsLocation)) {
+ return NS_ERROR_FILE_NOT_FOUND;
+ }
+
+ JS::RootedObject moduleNamespace(aCx);
+ // The fallback can fail if the URL was not for ESMified JSM. Suppress the
+ // error message, the crash, and also the telemetry event for the failure.
+ nsresult rv = ImportESModule(aCx, mjsLocation, &moduleNamespace,
+ SkipCheckForBrokenURLOrZeroSized::Yes);
+ if (rv == NS_ERROR_FILE_NOT_FOUND || rv == NS_ERROR_FILE_ACCESS_DENIED) {
+ // The error for ESModule shouldn't be exposed if the file does not exist,
+ // or the access is blocked by sandbox.
+ if (JS_IsExceptionPending(aCx)) {
+ JS_ClearPendingException(aCx);
+ }
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ JS::RootedObject globalProxy(aCx);
+ {
+ JSAutoRealm ar(aCx, moduleNamespace);
+
+ JS::RootedObject moduleObject(
+ aCx, JS::GetModuleForNamespace(aCx, moduleNamespace));
+ if (!moduleObject) {
+ return NS_ERROR_FAILURE;
+ }
+
+ globalProxy = CreateModuleEnvironmentProxy(aCx, moduleObject);
+ if (!globalProxy) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Cache the redirect to use in subsequent imports.
+ ModuleLoaderInfo info(aLocation);
+ auto newEntry = MakeUnique<FallbackModuleEntry>(RootingContext::get(aCx));
+ newEntry->globalProxy = globalProxy;
+ newEntry->moduleNamespace = moduleNamespace;
+ mFallbackImports.InsertOrUpdate(info.Key(), std::move(newEntry));
+ }
+
+ if (!JS_WrapObject(aCx, &globalProxy)) {
+ return NS_ERROR_FAILURE;
+ }
+ aModuleGlobal.set(globalProxy);
+
+ if (!aIgnoreExports) {
+ JS::RootedObject exports(aCx, moduleNamespace);
+ if (!JS_WrapObject(aCx, &exports)) {
+ return NS_ERROR_FAILURE;
+ }
+ aModuleExports.set(exports);
+ }
+
+ return NS_OK;
+}
+
+nsresult mozJSModuleLoader::TryCachedFallbackToImportESModule(
+ JSContext* aCx, const nsACString& aLocation,
+ JS::MutableHandleObject aModuleGlobal,
+ JS::MutableHandleObject aModuleExports, bool aIgnoreExports, bool* aFound) {
+ ModuleLoaderInfo info(aLocation);
+ FallbackModuleEntry* fallbackMod;
+ if (!mFallbackImports.Get(info.Key(), &fallbackMod)) {
+ *aFound = false;
+ return NS_OK;
+ }
+
+ JS::RootedObject globalProxy(aCx, fallbackMod->globalProxy);
+ if (!JS_WrapObject(aCx, &globalProxy)) {
+ return NS_ERROR_FAILURE;
+ }
+ aModuleGlobal.set(globalProxy);
+
+ if (!aIgnoreExports) {
+ JS::RootedObject exports(aCx, fallbackMod->moduleNamespace);
+ if (!JS_WrapObject(aCx, &exports)) {
+ return NS_ERROR_FAILURE;
+ }
+ aModuleExports.set(exports);
+ }
+
+ *aFound = true;
+ return NS_OK;
+}
+
+nsresult mozJSModuleLoader::ImportESModule(
+ JSContext* aCx, const nsACString& aLocation,
+ JS::MutableHandleObject aModuleNamespace,
+ SkipCheckForBrokenURLOrZeroSized
+ aSkipCheck /* = SkipCheckForBrokenURLOrZeroSized::No */) {
+ using namespace JS::loader;
+
+ mInitialized = true;
+
+ // Called from ChromeUtils::ImportESModule.
+ nsCString str(aLocation);
+
+ AUTO_PROFILER_MARKER_TEXT(
+ "ChromeUtils.importESModule", JS,
+ MarkerOptions(MarkerStack::Capture(),
+ MarkerInnerWindowIdFromJSContext(aCx)),
+ Substring(aLocation, 0, std::min(size_t(128), aLocation.Length())));
+
+ RootedObject globalObj(aCx, GetSharedGlobal(aCx));
+ NS_ENSURE_TRUE(globalObj, NS_ERROR_FAILURE);
+ MOZ_ASSERT_IF(NS_IsMainThread(),
+ xpc::Scriptability::Get(globalObj).Allowed());
+
+ // The module loader should be instantiated when fetching the shared global
+ MOZ_ASSERT(mModuleLoader);
+
+ JSAutoRealm ar(aCx, globalObj);
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), aLocation);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrincipal> principal =
+ mModuleLoader->GetGlobalObject()->PrincipalOrNull();
+ MOZ_ASSERT(principal);
+
+ RefPtr<ScriptFetchOptions> options = new ScriptFetchOptions(
+ CORS_NONE, /* aNonce = */ u""_ns, dom::RequestPriority::Auto,
+ ParserMetadata::NotParserInserted, principal);
+
+ RefPtr<SyncLoadContext> context = new SyncLoadContext();
+ context->mSkipCheck = aSkipCheck;
+
+ RefPtr<VisitedURLSet> visitedSet =
+ ModuleLoadRequest::NewVisitedSetForTopLevelImport(uri);
+
+ RefPtr<ModuleLoadRequest> request = new ModuleLoadRequest(
+ uri, dom::ReferrerPolicy::No_referrer, options, dom::SRIMetadata(),
+ /* aReferrer = */ nullptr, context,
+ /* aIsTopLevel = */ true,
+ /* aIsDynamicImport = */ false, mModuleLoader, visitedSet, nullptr);
+
+ request->NoCacheEntryFound();
+
+ rv = request->StartModuleLoad();
+ if (NS_FAILED(rv)) {
+ mModuleLoader->MaybeReportLoadError(aCx);
+ return rv;
+ }
+
+ rv = mModuleLoader->ProcessRequests();
+ if (NS_FAILED(rv)) {
+ mModuleLoader->MaybeReportLoadError(aCx);
+ return rv;
+ }
+
+ MOZ_ASSERT(request->IsFinished());
+ if (!request->mModuleScript) {
+ mModuleLoader->MaybeReportLoadError(aCx);
+ return NS_ERROR_FAILURE;
+ }
+
+ // All modules are loaded. MaybeReportLoadError isn't necessary from here.
+
+ if (!request->InstantiateModuleGraph()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mModuleLoader->EvaluateModuleInContext(aCx, request,
+ JS::ThrowModuleErrorsSync);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (JS_IsExceptionPending(aCx)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<ModuleScript> moduleScript = request->mModuleScript;
+ JS::Rooted<JSObject*> module(aCx, moduleScript->ModuleRecord());
+ aModuleNamespace.set(JS::GetModuleNamespace(aCx, module));
+
+ return NS_OK;
+}
+
+nsresult mozJSModuleLoader::Unload(const nsACString& aLocation) {
+ if (!mInitialized) {
+ return NS_OK;
+ }
+
+ ModuleLoaderInfo info(aLocation);
+
+ ModuleEntry* mod;
+ if (mImports.Get(info.Key(), &mod)) {
+ mLocations.Remove(mod->resolvedURL);
+ mImports.Remove(info.Key());
+ }
+
+ // If this is the last module to be unloaded, we will leak mLoaderGlobal
+ // until UnloadModules is called. So be it.
+
+ return NS_OK;
+}
+
+bool mozJSModuleLoader::CreateJSServices(JSContext* aCx) {
+ JSObject* services = NewJSServices(aCx);
+ if (!services) {
+ return false;
+ }
+
+ mServicesObj = services;
+ return true;
+}
+
+bool mozJSModuleLoader::DefineJSServices(JSContext* aCx,
+ JS::Handle<JSObject*> aGlobal) {
+ if (!mServicesObj) {
+ // This function is called whenever creating a new global that needs
+ // `Services`, including the loader's shared global.
+ //
+ // This function is no-op if it's called during creating the loader's
+ // shared global.
+ //
+ // See also CreateAndDefineJSServices.
+ MOZ_ASSERT(!mLoaderGlobal);
+ MOZ_ASSERT(mIsInitializingLoaderGlobal);
+ return true;
+ }
+
+ JS::Rooted<JS::Value> services(aCx, ObjectValue(*mServicesObj));
+ if (!JS_WrapValue(aCx, &services)) {
+ return false;
+ }
+
+ JS::Rooted<JS::PropertyKey> servicesId(
+ aCx, XPCJSContext::Get()->GetStringID(XPCJSContext::IDX_SERVICES));
+ return JS_DefinePropertyById(aCx, aGlobal, servicesId, services, 0);
+}
+
+size_t mozJSModuleLoader::ModuleEntry::SizeOfIncludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ size_t n = aMallocSizeOf(this);
+ n += aMallocSizeOf(location);
+
+ return n;
+}
+
+//----------------------------------------------------------------------
+
+/* static */
+MOZ_THREAD_LOCAL(mozJSModuleLoader*)
+NonSharedGlobalSyncModuleLoaderScope::sTlsActiveLoader;
+
+void NonSharedGlobalSyncModuleLoaderScope::InitStatics() {
+ sTlsActiveLoader.infallibleInit();
+}
+
+NonSharedGlobalSyncModuleLoaderScope::NonSharedGlobalSyncModuleLoaderScope(
+ JSContext* aCx, nsIGlobalObject* aGlobal) {
+ MOZ_ASSERT_IF(NS_IsMainThread(),
+ !mozJSModuleLoader::IsSharedSystemGlobal(aGlobal));
+ MOZ_ASSERT_IF(NS_IsMainThread(),
+ !mozJSModuleLoader::IsDevToolsLoaderGlobal(aGlobal));
+
+ mAsyncModuleLoader = aGlobal->GetModuleLoader(aCx);
+ MOZ_ASSERT(mAsyncModuleLoader,
+ "The consumer should guarantee the global returns non-null module "
+ "loader");
+
+ mLoader = new mozJSModuleLoader();
+ RegisterWeakMemoryReporter(mLoader);
+ mLoader->InitSyncModuleLoaderForGlobal(aGlobal);
+
+ mAsyncModuleLoader->CopyModulesTo(mLoader->mModuleLoader);
+
+ mMaybeOverride.emplace(mAsyncModuleLoader, mLoader->mModuleLoader);
+
+ MOZ_ASSERT(!sTlsActiveLoader.get());
+ sTlsActiveLoader.set(mLoader);
+}
+
+NonSharedGlobalSyncModuleLoaderScope::~NonSharedGlobalSyncModuleLoaderScope() {
+ MOZ_ASSERT(sTlsActiveLoader.get() == mLoader);
+ sTlsActiveLoader.set(nullptr);
+
+ mLoader->DisconnectSyncModuleLoaderFromGlobal();
+ UnregisterWeakMemoryReporter(mLoader);
+}
+
+void NonSharedGlobalSyncModuleLoaderScope::Finish() {
+ mLoader->mModuleLoader->MoveModulesTo(mAsyncModuleLoader);
+}
+
+/* static */
+bool NonSharedGlobalSyncModuleLoaderScope::IsActive() {
+ return !!sTlsActiveLoader.get();
+}
+
+/*static */
+mozJSModuleLoader* NonSharedGlobalSyncModuleLoaderScope::ActiveLoader() {
+ return sTlsActiveLoader.get();
+}
+
+//----------------------------------------------------------------------
+
+JSCLContextHelper::JSCLContextHelper(JSContext* aCx)
+ : mContext(aCx), mBuf(nullptr) {}
+
+JSCLContextHelper::~JSCLContextHelper() {
+ if (mBuf) {
+ JS_ReportErrorUTF8(mContext, "%s", mBuf.get());
+ }
+}
+
+void JSCLContextHelper::reportErrorAfterPop(UniqueChars&& buf) {
+ MOZ_ASSERT(!mBuf, "Already called reportErrorAfterPop");
+ mBuf = std::move(buf);
+}
diff --git a/js/xpconnect/loader/mozJSModuleLoader.h b/js/xpconnect/loader/mozJSModuleLoader.h
new file mode 100644
index 0000000000..0b8e5f85d9
--- /dev/null
+++ b/js/xpconnect/loader/mozJSModuleLoader.h
@@ -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/. */
+
+#ifndef mozJSModuleLoader_h
+#define mozJSModuleLoader_h
+
+#include "SyncModuleLoader.h"
+#include "mozilla/Attributes.h" // MOZ_STACK_CLASS
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/FileLocation.h"
+#include "mozilla/Maybe.h" // mozilla::Maybe
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/RefPtr.h" // RefPtr, mozilla::StaticRefPtr
+#include "mozilla/StaticPtr.h"
+#include "mozilla/ThreadLocal.h" // MOZ_THREAD_LOCAL
+#include "nsIMemoryReporter.h"
+#include "nsISupports.h"
+#include "nsIURI.h"
+#include "nsClassHashtable.h"
+#include "jsapi.h"
+#include "js/CompileOptions.h"
+#include "js/experimental/JSStencil.h"
+#include "SkipCheckForBrokenURLOrZeroSized.h"
+
+#include "xpcpublic.h"
+
+class nsIFile;
+class ModuleLoaderInfo;
+
+namespace mozilla {
+class ScriptPreloader;
+} // namespace mozilla
+
+namespace JS::loader {
+class ModuleLoadRequest;
+} // namespace JS::loader
+
+#if defined(NIGHTLY_BUILD) || defined(MOZ_DEV_EDITION) || defined(DEBUG)
+# define STARTUP_RECORDER_ENABLED
+#endif
+
+namespace mozilla::loader {
+
+class NonSharedGlobalSyncModuleLoaderScope;
+
+} // namespace mozilla::loader
+
+class mozJSModuleLoader final : public nsIMemoryReporter {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMEMORYREPORTER
+
+ // Returns the list of all JSMs.
+ void GetLoadedModules(nsTArray<nsCString>& aLoadedModules);
+
+ // Returns the list of all ESMs.
+ nsresult GetLoadedESModules(nsTArray<nsCString>& aLoadedModules);
+
+ // Returns the list of all JSMs and ESMs.
+ nsresult GetLoadedJSAndESModules(nsTArray<nsCString>& aLoadedModules);
+
+ nsresult GetModuleImportStack(const nsACString& aLocation,
+ nsACString& aRetval);
+
+ void FindTargetObject(JSContext* aCx, JS::MutableHandleObject aTargetObject);
+
+ static void InitStatics();
+ static void UnloadLoaders();
+ static void ShutdownLoaders();
+
+ static mozJSModuleLoader* Get() {
+ MOZ_ASSERT(sSelf, "Should have already created the module loader");
+ return sSelf;
+ }
+
+ JSObject* GetSharedGlobal(JSContext* aCx);
+
+ private:
+ void InitSyncModuleLoaderForGlobal(nsIGlobalObject* aGlobal);
+ void DisconnectSyncModuleLoaderFromGlobal();
+
+ friend class mozilla::loader::NonSharedGlobalSyncModuleLoaderScope;
+
+ public:
+ static mozJSModuleLoader* GetDevToolsLoader() { return sDevToolsLoader; }
+ static mozJSModuleLoader* GetOrCreateDevToolsLoader();
+
+ nsresult ImportInto(const nsACString& aResourceURI,
+ JS::HandleValue aTargetObj, JSContext* aCx, uint8_t aArgc,
+ JS::MutableHandleValue aRetval);
+
+ // Load a JSM.
+ nsresult Import(JSContext* aCx, const nsACString& aResourceURI,
+ JS::MutableHandleObject aModuleGlobal,
+ JS::MutableHandleObject aModuleExports,
+ bool aIgnoreExports = false);
+
+ // Synchronously load an ES6 module and all its dependencies.
+ nsresult ImportESModule(
+ JSContext* aCx, const nsACString& aResourceURI,
+ JS::MutableHandleObject aModuleNamespace,
+ mozilla::loader::SkipCheckForBrokenURLOrZeroSized aSkipCheck =
+ mozilla::loader::SkipCheckForBrokenURLOrZeroSized::No);
+
+ // Fallback from Import to ImportESModule.
+ nsresult TryFallbackToImportESModule(JSContext* aCx,
+ const nsACString& aResourceURI,
+ JS::MutableHandleObject aModuleGlobal,
+ JS::MutableHandleObject aModuleExports,
+ bool aIgnoreExports);
+
+ // If the request was handled by fallback before, fills the output and
+ // sets *aFound to true and returns NS_OK.
+ // If the request wasn't yet handled by fallback, sets *Found to false
+ // and returns NS_OK.
+ nsresult TryCachedFallbackToImportESModule(
+ JSContext* aCx, const nsACString& aResourceURI,
+ JS::MutableHandleObject aModuleGlobal,
+ JS::MutableHandleObject aModuleExports, bool aIgnoreExports,
+ bool* aFound);
+
+#ifdef STARTUP_RECORDER_ENABLED
+ void RecordImportStack(JSContext* aCx, const nsACString& aLocation);
+ void RecordImportStack(JSContext* aCx,
+ JS::loader::ModuleLoadRequest* aRequest);
+#endif
+
+ nsresult Unload(const nsACString& aResourceURI);
+ nsresult IsModuleLoaded(const nsACString& aResourceURI, bool* aRetval);
+ nsresult IsJSModuleLoaded(const nsACString& aResourceURI, bool* aRetval);
+ nsresult IsESModuleLoaded(const nsACString& aResourceURI, bool* aRetval);
+ bool IsLoaderGlobal(JSObject* aObj) { return mLoaderGlobal == aObj; }
+ bool IsDevToolsLoader() const { return this == sDevToolsLoader; }
+
+ static bool IsSharedSystemGlobal(nsIGlobalObject* aGlobal);
+ static bool IsDevToolsLoaderGlobal(nsIGlobalObject* aGlobal);
+
+ // Public methods for use from SyncModuleLoader.
+ static bool IsTrustedScheme(nsIURI* aURI);
+ static nsresult LoadSingleModuleScript(
+ mozilla::loader::SyncModuleLoader* aModuleLoader, JSContext* aCx,
+ JS::loader::ModuleLoadRequest* aRequest,
+ JS::MutableHandleScript aScriptOut);
+
+ private:
+ static nsresult ReadScriptOnMainThread(JSContext* aCx,
+ const nsCString& aLocation,
+ nsCString& aData);
+ static nsresult LoadSingleModuleScriptOnWorker(
+ mozilla::loader::SyncModuleLoader* aModuleLoader, JSContext* aCx,
+ JS::loader::ModuleLoadRequest* aRequest,
+ JS::MutableHandleScript aScriptOut);
+
+ public:
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+
+ bool DefineJSServices(JSContext* aCx, JS::Handle<JSObject*> aGlobal);
+
+ protected:
+ mozJSModuleLoader();
+ ~mozJSModuleLoader();
+
+ friend class XPCJSRuntime;
+
+ private:
+ static mozilla::StaticRefPtr<mozJSModuleLoader> sSelf;
+ static mozilla::StaticRefPtr<mozJSModuleLoader> sDevToolsLoader;
+
+ void Unload();
+ void UnloadModules();
+
+ void CreateLoaderGlobal(JSContext* aCx, const nsACString& aLocation,
+ JS::MutableHandleObject aGlobal);
+ void CreateDevToolsLoaderGlobal(JSContext* aCx, const nsACString& aLocation,
+ JS::MutableHandleObject aGlobal);
+
+ bool CreateJSServices(JSContext* aCx);
+
+ static nsresult GetSourceFile(nsIURI* aResolvedURI, nsIFile** aSourceFileOut);
+
+ static bool LocationIsRealFile(nsIURI* aURI);
+
+ JSObject* PrepareObjectForLocation(JSContext* aCx, nsIFile* aModuleFile,
+ nsIURI* aURI, bool aRealFile);
+
+ nsresult ObjectForLocation(ModuleLoaderInfo& aInfo, nsIFile* aModuleFile,
+ JS::MutableHandleObject aObject,
+ JS::MutableHandleScript aTableScript,
+ char** aLocation, bool aCatchException,
+ JS::MutableHandleValue aException);
+
+ static void SetModuleOptions(JS::CompileOptions& aOptions);
+
+ // Get the script for a given location, either from a cached stencil or by
+ // compiling it from source.
+ static nsresult GetScriptForLocation(JSContext* aCx, ModuleLoaderInfo& aInfo,
+ nsIFile* aModuleFile, bool aUseMemMap,
+ JS::MutableHandleScript aScriptOut,
+ char** aLocationOut = nullptr);
+
+ static already_AddRefed<JS::Stencil> CompileStencil(
+ JSContext* aCx, const JS::CompileOptions& aOptions,
+ JS::SourceText<mozilla::Utf8Unit>& aSource, bool aIsModule);
+ static JSScript* InstantiateStencil(JSContext* aCx, JS::Stencil* aStencil,
+ bool aIsModule);
+
+ nsresult ImportInto(const nsACString& aLocation, JS::HandleObject targetObj,
+ JSContext* callercx, JS::MutableHandleObject vp);
+
+ class ModuleEntry {
+ public:
+ explicit ModuleEntry(JS::RootingContext* aRootingCx)
+ : obj(aRootingCx), exports(aRootingCx), thisObjectKey(aRootingCx) {
+ location = nullptr;
+ }
+
+ ~ModuleEntry() { Clear(); }
+
+ void Clear() {
+ if (obj) {
+ if (JS_HasExtensibleLexicalEnvironment(obj)) {
+ JS::RootedObject lexicalEnv(mozilla::dom::RootingCx(),
+ JS_ExtensibleLexicalEnvironment(obj));
+ JS_SetAllNonReservedSlotsToUndefined(lexicalEnv);
+ }
+ JS_SetAllNonReservedSlotsToUndefined(obj);
+ obj = nullptr;
+ thisObjectKey = nullptr;
+ }
+
+ if (location) {
+ free(location);
+ }
+
+ obj = nullptr;
+ thisObjectKey = nullptr;
+ location = nullptr;
+ }
+
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ JS::PersistentRootedObject obj;
+ JS::PersistentRootedObject exports;
+ JS::PersistentRootedScript thisObjectKey;
+ char* location;
+ nsCString resolvedURL;
+ };
+
+ class FallbackModuleEntry {
+ public:
+ explicit FallbackModuleEntry(JS::RootingContext* aRootingCx)
+ : globalProxy(aRootingCx), moduleNamespace(aRootingCx) {}
+
+ ~FallbackModuleEntry() { Clear(); }
+
+ void Clear() {
+ globalProxy = nullptr;
+ moduleNamespace = nullptr;
+ }
+
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this);
+ }
+
+ JS::PersistentRootedObject globalProxy;
+ JS::PersistentRootedObject moduleNamespace;
+ };
+
+ nsresult ExtractExports(JSContext* aCx, ModuleLoaderInfo& aInfo,
+ ModuleEntry* aMod, JS::MutableHandleObject aExports);
+
+ nsClassHashtable<nsCStringHashKey, ModuleEntry> mImports;
+ nsTHashMap<nsCStringHashKey, ModuleEntry*> mInProgressImports;
+ nsClassHashtable<nsCStringHashKey, FallbackModuleEntry> mFallbackImports;
+#ifdef STARTUP_RECORDER_ENABLED
+ nsTHashMap<nsCStringHashKey, nsCString> mImportStacks;
+#endif
+
+ // A map of on-disk file locations which are loaded as modules to the
+ // pre-resolved URIs they were loaded from. Used to prevent the same file
+ // from being loaded separately, from multiple URLs.
+ nsClassHashtable<nsCStringHashKey, nsCString> mLocations;
+
+ bool mInitialized;
+#ifdef DEBUG
+ bool mIsInitializingLoaderGlobal = false;
+#endif
+ JS::PersistentRooted<JSObject*> mLoaderGlobal;
+ JS::PersistentRooted<JSObject*> mServicesObj;
+
+ RefPtr<mozilla::loader::SyncModuleLoader> mModuleLoader;
+};
+
+namespace mozilla::loader {
+
+// Automatically allocate and initialize a sync module loader for given
+// non-shared global, and override the module loader for the global with sync
+// module loader.
+//
+// This is not re-entrant, and the consumer must check IsActive method before
+// allocating this on the stack.
+//
+// The consumer should ensure the target global's module loader has no
+// ongoing fetching modules (ModuleLoaderBase::HasFetchingModules).
+// If there's any fetching modules, the consumer should wait for them before
+// allocating this class on the stack.
+//
+// The consumer should also verify that the target global has module loader,
+// as a part of the above step.
+//
+// The loader returned by ActiveLoader can be reused only when
+// ActiveLoader's global matches the global the consumer wants to use.
+class MOZ_STACK_CLASS NonSharedGlobalSyncModuleLoaderScope {
+ public:
+ NonSharedGlobalSyncModuleLoaderScope(JSContext* aCx,
+ nsIGlobalObject* aGlobal);
+ ~NonSharedGlobalSyncModuleLoaderScope();
+
+ // After successfully importing a module graph, move all imported modules to
+ // the target global's module loader.
+ void Finish();
+
+ // Returns true if another instance of NonSharedGlobalSyncModuleLoaderScope
+ // is on stack.
+ static bool IsActive();
+
+ static mozJSModuleLoader* ActiveLoader();
+
+ static void InitStatics();
+
+ private:
+ RefPtr<mozJSModuleLoader> mLoader;
+
+ // Reference to thread-local module loader on the stack.
+ // This is used by another sync module load during a sync module load is
+ // ongoing.
+ static MOZ_THREAD_LOCAL(mozJSModuleLoader*) sTlsActiveLoader;
+
+ // The module loader of the target global.
+ RefPtr<JS::loader::ModuleLoaderBase> mAsyncModuleLoader;
+
+ mozilla::Maybe<JS::loader::AutoOverrideModuleLoader> mMaybeOverride;
+};
+
+} // namespace mozilla::loader
+
+#endif // mozJSModuleLoader_h
diff --git a/js/xpconnect/loader/mozJSSubScriptLoader.cpp b/js/xpconnect/loader/mozJSSubScriptLoader.cpp
new file mode 100644
index 0000000000..33192bff29
--- /dev/null
+++ b/js/xpconnect/loader/mozJSSubScriptLoader.cpp
@@ -0,0 +1,476 @@
+/* -*- 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 "mozJSSubScriptLoader.h"
+#include "js/experimental/JSStencil.h"
+#include "mozJSModuleLoader.h"
+#include "mozJSLoaderUtils.h"
+
+#include "nsIURI.h"
+#include "nsIIOService.h"
+#include "nsIChannel.h"
+#include "nsIInputStream.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+
+#include "jsapi.h"
+#include "jsfriendapi.h"
+#include "xpcprivate.h" // xpc::OptionsBase
+#include "js/CompilationAndEvaluation.h" // JS::Compile
+#include "js/CompileOptions.h" // JS::ReadOnlyCompileOptions, JS::DecodeOptions
+#include "js/friend/JSMEnvironment.h" // JS::ExecuteInJSMEnvironment, JS::IsJSMEnvironment
+#include "js/SourceText.h" // JS::Source{Ownership,Text}
+#include "js/Wrapper.h"
+
+#include "mozilla/ContentPrincipal.h"
+#include "mozilla/dom/ScriptLoader.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "mozilla/ScriptPreloader.h"
+#include "mozilla/SystemPrincipal.h"
+#include "mozilla/scache/StartupCache.h"
+#include "mozilla/scache/StartupCacheUtils.h"
+#include "mozilla/Unused.h"
+#include "mozilla/Utf8.h" // mozilla::Utf8Unit
+#include "nsContentUtils.h"
+#include "nsString.h"
+
+using namespace mozilla::scache;
+using namespace JS;
+using namespace xpc;
+using namespace mozilla;
+using namespace mozilla::dom;
+
+class MOZ_STACK_CLASS LoadSubScriptOptions : public OptionsBase {
+ public:
+ explicit LoadSubScriptOptions(JSContext* cx = xpc_GetSafeJSContext(),
+ JSObject* options = nullptr)
+ : OptionsBase(cx, options),
+ target(cx),
+ ignoreCache(false),
+ wantReturnValue(false) {}
+
+ virtual bool Parse() override {
+ return ParseObject("target", &target) &&
+ ParseBoolean("ignoreCache", &ignoreCache) &&
+ ParseBoolean("wantReturnValue", &wantReturnValue);
+ }
+
+ RootedObject target;
+ bool ignoreCache;
+ bool wantReturnValue;
+};
+
+/* load() error msgs, XXX localize? */
+#define LOAD_ERROR_NOSERVICE "Error creating IO Service."
+#define LOAD_ERROR_NOURI "Error creating URI (invalid URL scheme?)"
+#define LOAD_ERROR_NOSCHEME "Failed to get URI scheme. This is bad."
+#define LOAD_ERROR_URI_NOT_LOCAL "Trying to load a non-local URI."
+#define LOAD_ERROR_NOSTREAM "Error opening input stream (invalid filename?)"
+#define LOAD_ERROR_NOCONTENT "ContentLength not available (not a local URL?)"
+#define LOAD_ERROR_BADCHARSET "Error converting to specified charset"
+#define LOAD_ERROR_NOSPEC "Failed to get URI spec. This is bad."
+#define LOAD_ERROR_CONTENTTOOBIG "ContentLength is too large"
+
+mozJSSubScriptLoader::mozJSSubScriptLoader() = default;
+
+mozJSSubScriptLoader::~mozJSSubScriptLoader() = default;
+
+NS_IMPL_ISUPPORTS(mozJSSubScriptLoader, mozIJSSubScriptLoader)
+
+#define JSSUB_CACHE_PREFIX(aScopeType, aCompilationTarget) \
+ "jssubloader/" aScopeType "/" aCompilationTarget
+
+static void SubscriptCachePath(JSContext* cx, nsIURI* uri,
+ JS::HandleObject targetObj,
+ nsACString& cachePath) {
+ // StartupCache must distinguish between non-syntactic vs global when
+ // computing the cache key.
+ if (!JS_IsGlobalObject(targetObj)) {
+ PathifyURI(JSSUB_CACHE_PREFIX("non-syntactic", "script"), uri, cachePath);
+ } else {
+ PathifyURI(JSSUB_CACHE_PREFIX("global", "script"), uri, cachePath);
+ }
+}
+
+static void ReportError(JSContext* cx, const nsACString& msg) {
+ NS_ConvertUTF8toUTF16 ucMsg(msg);
+
+ RootedValue exn(cx);
+ if (xpc::NonVoidStringToJsval(cx, ucMsg, &exn)) {
+ JS_SetPendingException(cx, exn);
+ }
+}
+
+static void ReportError(JSContext* cx, const char* origMsg, nsIURI* uri) {
+ if (!uri) {
+ ReportError(cx, nsDependentCString(origMsg));
+ return;
+ }
+
+ nsAutoCString spec;
+ nsresult rv = uri->GetSpec(spec);
+ if (NS_FAILED(rv)) {
+ spec.AssignLiteral("(unknown)");
+ }
+
+ nsAutoCString msg(origMsg);
+ msg.AppendLiteral(": ");
+ msg.Append(spec);
+ ReportError(cx, msg);
+}
+
+static bool EvalStencil(JSContext* cx, HandleObject targetObj,
+ HandleObject loadScope, MutableHandleValue retval,
+ nsIURI* uri, bool storeIntoStartupCache,
+ bool storeIntoPreloadCache, JS::Stencil* stencil) {
+ MOZ_ASSERT(!js::IsWrapper(targetObj));
+
+ JS::InstantiateOptions options;
+ JS::RootedScript script(cx,
+ JS::InstantiateGlobalStencil(cx, options, stencil));
+ if (!script) {
+ return false;
+ }
+
+ if (JS_IsGlobalObject(targetObj)) {
+ if (!JS_ExecuteScript(cx, script, retval)) {
+ return false;
+ }
+ } else if (JS::IsJSMEnvironment(targetObj)) {
+ if (!JS::ExecuteInJSMEnvironment(cx, script, targetObj)) {
+ return false;
+ }
+ retval.setUndefined();
+ } else {
+ JS::RootedObjectVector envChain(cx);
+ if (!envChain.append(targetObj)) {
+ return false;
+ }
+ if (!loadScope) {
+ // A null loadScope means we are cross-realm. In this case, we should
+ // check the target isn't in the JSM loader shared-global or we will
+ // contaminate all JSMs in the realm.
+ //
+ // NOTE: If loadScope is already a shared-global JSM, we can't
+ // determine which JSM the target belongs to and have to assume it
+ // is in our JSM.
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ JSObject* targetGlobal = JS::GetNonCCWObjectGlobal(targetObj);
+ MOZ_DIAGNOSTIC_ASSERT(
+ !mozJSModuleLoader::Get()->IsLoaderGlobal(targetGlobal),
+ "Don't load subscript into target in a shared-global JSM");
+#endif
+ if (!JS_ExecuteScript(cx, envChain, script, retval)) {
+ return false;
+ }
+ } else if (JS_IsGlobalObject(loadScope)) {
+ if (!JS_ExecuteScript(cx, envChain, script, retval)) {
+ return false;
+ }
+ } else {
+ MOZ_ASSERT(JS::IsJSMEnvironment(loadScope));
+ if (!JS::ExecuteInJSMEnvironment(cx, script, loadScope, envChain)) {
+ return false;
+ }
+ retval.setUndefined();
+ }
+ }
+
+ JSAutoRealm rar(cx, targetObj);
+ if (!JS_WrapValue(cx, retval)) {
+ return false;
+ }
+
+ if (script && (storeIntoStartupCache || storeIntoPreloadCache)) {
+ nsAutoCString cachePath;
+ SubscriptCachePath(cx, uri, targetObj, cachePath);
+
+ nsCString uriStr;
+ if (storeIntoPreloadCache && NS_SUCCEEDED(uri->GetSpec(uriStr))) {
+ ScriptPreloader::GetSingleton().NoteStencil(uriStr, cachePath, stencil);
+ }
+
+ if (storeIntoStartupCache) {
+ JSAutoRealm ar(cx, script);
+ WriteCachedStencil(StartupCache::GetSingleton(), cachePath, cx, stencil);
+ }
+ }
+
+ return true;
+}
+
+bool mozJSSubScriptLoader::ReadStencil(
+ JS::Stencil** stencilOut, nsIURI* uri, JSContext* cx,
+ const JS::ReadOnlyCompileOptions& options, nsIIOService* serv,
+ bool useCompilationScope) {
+ // We create a channel and call SetContentType, to avoid expensive MIME type
+ // lookups (bug 632490).
+ nsCOMPtr<nsIChannel> chan;
+ nsCOMPtr<nsIInputStream> instream;
+ nsresult rv;
+ rv = NS_NewChannel(getter_AddRefs(chan), uri,
+ nsContentUtils::GetSystemPrincipal(),
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER,
+ nullptr, // nsICookieJarSettings
+ nullptr, // PerformanceStorage
+ nullptr, // aLoadGroup
+ nullptr, // aCallbacks
+ nsIRequest::LOAD_NORMAL, serv);
+
+ if (NS_SUCCEEDED(rv)) {
+ chan->SetContentType("application/javascript"_ns);
+ rv = chan->Open(getter_AddRefs(instream));
+ }
+
+ if (NS_FAILED(rv)) {
+ ReportError(cx, LOAD_ERROR_NOSTREAM, uri);
+ return false;
+ }
+
+ int64_t len = -1;
+
+ rv = chan->GetContentLength(&len);
+ if (NS_FAILED(rv)) {
+ ReportError(cx, LOAD_ERROR_NOCONTENT, uri);
+ return false;
+ }
+
+ if (len > INT32_MAX) {
+ ReportError(cx, LOAD_ERROR_CONTENTTOOBIG, uri);
+ return false;
+ }
+
+ nsCString buf;
+ rv = NS_ReadInputStreamToString(instream, buf, len);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ if (len < 0) {
+ len = buf.Length();
+ }
+
+ Maybe<JSAutoRealm> ar;
+
+ // Note that when using the ScriptPreloader cache with loadSubScript, there
+ // will be a side-effect of keeping the global that the script was compiled
+ // for alive. See note above in EvalScript().
+ //
+ // This will compile the script in XPConnect compilation scope. When the
+ // script is evaluated, it will be cloned into the target scope to be
+ // executed, avoiding leaks on the first session when we don't have a
+ // startup cache.
+ if (useCompilationScope) {
+ ar.emplace(cx, xpc::CompilationScope());
+ }
+
+ JS::SourceText<Utf8Unit> srcBuf;
+ if (!srcBuf.init(cx, buf.get(), len, JS::SourceOwnership::Borrowed)) {
+ return false;
+ }
+
+ RefPtr<JS::Stencil> stencil =
+ JS::CompileGlobalScriptToStencil(cx, options, srcBuf);
+ stencil.forget(stencilOut);
+ return *stencilOut;
+}
+
+NS_IMETHODIMP
+mozJSSubScriptLoader::LoadSubScript(const nsAString& url, HandleValue target,
+ JSContext* cx, MutableHandleValue retval) {
+ /*
+ * Loads a local url, referring to UTF-8-encoded data, and evals it into the
+ * current cx. Synchronous. ChromeUtils.compileScript() should be used for
+ * async loads.
+ * url: The url to load. Must be local so that it can be loaded
+ * synchronously.
+ * targetObj: Optional object to eval the script onto (defaults to context
+ * global)
+ * returns: Whatever jsval the script pointed to by the url returns.
+ * Should ONLY (O N L Y !) be called from JavaScript code.
+ */
+ LoadSubScriptOptions options(cx);
+ options.target = target.isObject() ? &target.toObject() : nullptr;
+ return DoLoadSubScriptWithOptions(url, options, cx, retval);
+}
+
+NS_IMETHODIMP
+mozJSSubScriptLoader::LoadSubScriptWithOptions(const nsAString& url,
+ HandleValue optionsVal,
+ JSContext* cx,
+ MutableHandleValue retval) {
+ if (!optionsVal.isObject()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ LoadSubScriptOptions options(cx, &optionsVal.toObject());
+ if (!options.Parse()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ return DoLoadSubScriptWithOptions(url, options, cx, retval);
+}
+
+nsresult mozJSSubScriptLoader::DoLoadSubScriptWithOptions(
+ const nsAString& url, LoadSubScriptOptions& options, JSContext* cx,
+ MutableHandleValue retval) {
+ nsresult rv = NS_OK;
+ RootedObject targetObj(cx);
+ RootedObject loadScope(cx);
+ mozJSModuleLoader* loader = mozJSModuleLoader::Get();
+ loader->FindTargetObject(cx, &loadScope);
+
+ if (options.target) {
+ targetObj = options.target;
+ } else {
+ targetObj = loadScope;
+ }
+
+ targetObj = JS_FindCompilationScope(cx, targetObj);
+ if (!targetObj || !loadScope) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT(!js::IsWrapper(targetObj), "JS_FindCompilationScope must unwrap");
+
+ if (js::GetNonCCWObjectRealm(loadScope) !=
+ js::GetNonCCWObjectRealm(targetObj)) {
+ loadScope = nullptr;
+ }
+
+ /* load up the url. From here on, failures are reflected as ``custom''
+ * js exceptions */
+ nsCOMPtr<nsIURI> uri;
+ nsAutoCString uriStr;
+ nsAutoCString scheme;
+
+ // Figure out who's calling us
+ JS::AutoFilename filename;
+ if (!JS::DescribeScriptedCaller(cx, &filename)) {
+ // No scripted frame means we don't know who's calling, bail.
+ return NS_ERROR_FAILURE;
+ }
+
+ JSAutoRealm ar(cx, targetObj);
+
+ nsCOMPtr<nsIIOService> serv = do_GetService(NS_IOSERVICE_CONTRACTID);
+ if (!serv) {
+ ReportError(cx, nsLiteralCString(LOAD_ERROR_NOSERVICE));
+ return NS_OK;
+ }
+
+ NS_LossyConvertUTF16toASCII asciiUrl(url);
+ const nsDependentCSubstring profilerUrl =
+ Substring(asciiUrl, 0, std::min(size_t(128), asciiUrl.Length()));
+ AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING_NONSENSITIVE(
+ "mozJSSubScriptLoader::DoLoadSubScriptWithOptions", OTHER, profilerUrl);
+ AUTO_PROFILER_MARKER_TEXT("SubScript", JS,
+ MarkerOptions(MarkerStack::Capture(),
+ MarkerInnerWindowIdFromJSContext(cx)),
+ profilerUrl);
+
+ // Make sure to explicitly create the URI, since we'll need the
+ // canonicalized spec.
+ rv = NS_NewURI(getter_AddRefs(uri), asciiUrl);
+ if (NS_FAILED(rv)) {
+ ReportError(cx, nsLiteralCString(LOAD_ERROR_NOURI));
+ return NS_OK;
+ }
+
+ rv = uri->GetSpec(uriStr);
+ if (NS_FAILED(rv)) {
+ ReportError(cx, nsLiteralCString(LOAD_ERROR_NOSPEC));
+ return NS_OK;
+ }
+
+ rv = uri->GetScheme(scheme);
+ if (NS_FAILED(rv)) {
+ ReportError(cx, LOAD_ERROR_NOSCHEME, uri);
+ return NS_OK;
+ }
+
+ // Suppress caching if we're compiling as content or if we're loading a
+ // blob: URI.
+ bool useCompilationScope = false;
+ auto* principal = BasePrincipal::Cast(GetObjectPrincipal(targetObj));
+ bool isSystem = principal->Is<SystemPrincipal>();
+ if (!isSystem && principal->Is<ContentPrincipal>()) {
+ nsAutoCString scheme;
+ principal->GetScheme(scheme);
+
+ // We want to enable caching for scripts with Activity Stream's
+ // codebase URLs.
+ if (scheme.EqualsLiteral("about")) {
+ nsAutoCString filePath;
+ principal->GetFilePath(filePath);
+
+ useCompilationScope = filePath.EqualsLiteral("home") ||
+ filePath.EqualsLiteral("newtab") ||
+ filePath.EqualsLiteral("welcome");
+ isSystem = true;
+ }
+ }
+ bool ignoreCache =
+ options.ignoreCache || !isSystem || scheme.EqualsLiteral("blob");
+
+ StartupCache* cache = ignoreCache ? nullptr : StartupCache::GetSingleton();
+
+ nsAutoCString cachePath;
+ SubscriptCachePath(cx, uri, targetObj, cachePath);
+
+ JS::DecodeOptions decodeOptions;
+ ScriptPreloader::FillDecodeOptionsForCachedStencil(decodeOptions);
+
+ RefPtr<JS::Stencil> stencil;
+ if (!options.ignoreCache) {
+ if (!options.wantReturnValue) {
+ // NOTE: If we need the return value, we cannot use ScriptPreloader.
+ stencil = ScriptPreloader::GetSingleton().GetCachedStencil(
+ cx, decodeOptions, cachePath);
+ }
+ if (!stencil && cache) {
+ rv = ReadCachedStencil(cache, cachePath, cx, decodeOptions,
+ getter_AddRefs(stencil));
+ if (NS_FAILED(rv) || !stencil) {
+ JS_ClearPendingException(cx);
+ }
+ }
+ }
+
+ bool storeIntoStartupCache = false;
+ if (!stencil) {
+ // Store into startup cache only when the script isn't come from any cache.
+ storeIntoStartupCache = cache;
+
+ JS::CompileOptions compileOptions(cx);
+ ScriptPreloader::FillCompileOptionsForCachedStencil(compileOptions);
+ compileOptions.setFileAndLine(uriStr.get(), 1);
+ compileOptions.setNonSyntacticScope(!JS_IsGlobalObject(targetObj));
+
+ if (options.wantReturnValue) {
+ compileOptions.setNoScriptRval(false);
+ }
+
+ if (!ReadStencil(getter_AddRefs(stencil), uri, cx, compileOptions, serv,
+ useCompilationScope)) {
+ return NS_OK;
+ }
+
+#ifdef DEBUG
+ // The above shouldn't touch any options for instantiation.
+ JS::InstantiateOptions instantiateOptions(compileOptions);
+ instantiateOptions.assertDefault();
+#endif
+ }
+
+ // As a policy choice, we don't store scripts that want return values
+ // into the preload cache.
+ bool storeIntoPreloadCache = !ignoreCache && !options.wantReturnValue;
+
+ Unused << EvalStencil(cx, targetObj, loadScope, retval, uri,
+ storeIntoStartupCache, storeIntoPreloadCache, stencil);
+ return NS_OK;
+}
diff --git a/js/xpconnect/loader/mozJSSubScriptLoader.h b/js/xpconnect/loader/mozJSSubScriptLoader.h
new file mode 100644
index 0000000000..01909da10c
--- /dev/null
+++ b/js/xpconnect/loader/mozJSSubScriptLoader.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsCOMPtr.h"
+#include "mozIJSSubScriptLoader.h"
+
+#include "js/experimental/JSStencil.h"
+#include "js/CompileOptions.h" // JS::ReadOnlyCompileOptions
+
+class nsIPrincipal;
+class nsIURI;
+class LoadSubScriptOptions;
+
+#define MOZ_JSSUBSCRIPTLOADER_CID \
+ { /* 829814d6-1dd2-11b2-8e08-82fa0a339b00 */ \
+ 0x929814d6, 0x1dd2, 0x11b2, { \
+ 0x8e, 0x08, 0x82, 0xfa, 0x0a, 0x33, 0x9b, 0x00 \
+ } \
+ }
+
+class nsIIOService;
+
+class mozJSSubScriptLoader : public mozIJSSubScriptLoader {
+ public:
+ mozJSSubScriptLoader();
+
+ // all the interface method declarations...
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZIJSSUBSCRIPTLOADER
+
+ private:
+ virtual ~mozJSSubScriptLoader();
+
+ bool ReadStencil(JS::Stencil** stencilOut, nsIURI* uri, JSContext* cx,
+ const JS::ReadOnlyCompileOptions& options,
+ nsIIOService* serv, bool useCompilationScope);
+
+ nsresult ReadScriptAsync(nsIURI* uri, JS::HandleObject targetObj,
+ JS::HandleObject loadScope, nsIIOService* serv,
+ bool wantReturnValue, bool cache,
+ JS::MutableHandleValue retval);
+
+ nsresult DoLoadSubScriptWithOptions(const nsAString& url,
+ LoadSubScriptOptions& options,
+ JSContext* cx,
+ JS::MutableHandleValue retval);
+};
diff --git a/js/xpconnect/loader/nsImportModule.cpp b/js/xpconnect/loader/nsImportModule.cpp
new file mode 100644
index 0000000000..a313c44388
--- /dev/null
+++ b/js/xpconnect/loader/nsImportModule.cpp
@@ -0,0 +1,113 @@
+/* -*- 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 "nsImportModule.h"
+
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozJSModuleLoader.h"
+#include "nsContentUtils.h"
+#include "nsExceptionHandler.h"
+#include "nsPrintfCString.h"
+#include "xpcpublic.h"
+#include "xpcprivate.h"
+#include "js/PropertyAndElement.h" // JS_GetProperty
+
+using mozilla::dom::AutoJSAPI;
+
+namespace mozilla {
+namespace loader {
+
+static void AnnotateCrashReportWithJSException(JSContext* aCx,
+ const char* aURI) {
+ JS::RootedValue exn(aCx);
+ if (JS_GetPendingException(aCx, &exn)) {
+ JS_ClearPendingException(aCx);
+
+ JSAutoRealm ar(aCx, xpc::PrivilegedJunkScope());
+ JS_WrapValue(aCx, &exn);
+
+ nsAutoCString file;
+ uint32_t line;
+ uint32_t column;
+ nsAutoString msg;
+ nsContentUtils::ExtractErrorValues(aCx, exn, file, &line, &column, msg);
+
+ nsPrintfCString errorString("Failed to load module \"%s\": %s:%u:%u: %s",
+ aURI, file.get(), line, column,
+ NS_ConvertUTF16toUTF8(msg).get());
+
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::JSModuleLoadError, errorString);
+ }
+}
+
+nsresult ImportModule(const char* aURI, const char* aExportName,
+ const nsIID& aIID, void** aResult, bool aInfallible) {
+ AutoJSAPI jsapi;
+ MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
+ JSContext* cx = jsapi.cx();
+
+ JS::RootedObject global(cx);
+ JS::RootedObject exports(cx);
+ nsresult rv = mozJSModuleLoader::Get()->Import(cx, nsDependentCString(aURI),
+ &global, &exports);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ if (aInfallible) {
+ AnnotateCrashReportWithJSException(cx, aURI);
+
+ MOZ_CRASH_UNSAFE_PRINTF("Failed to load critical module \"%s\"", aURI);
+ }
+ return rv;
+ }
+
+ if (aExportName) {
+ JS::RootedValue namedExport(cx);
+ if (!JS_GetProperty(cx, exports, aExportName, &namedExport)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!namedExport.isObject()) {
+ return NS_ERROR_XPC_BAD_CONVERT_JS;
+ }
+ exports.set(&namedExport.toObject());
+ }
+
+ return nsXPConnect::XPConnect()->WrapJS(cx, exports, aIID, aResult);
+}
+
+nsresult ImportESModule(const char* aURI, const char* aExportName,
+ const nsIID& aIID, void** aResult, bool aInfallible) {
+ AutoJSAPI jsapi;
+ MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
+ JSContext* cx = jsapi.cx();
+
+ JS::RootedObject moduleNamespace(cx);
+ nsresult rv = mozJSModuleLoader::Get()->ImportESModule(
+ cx, nsDependentCString(aURI), &moduleNamespace);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ if (aInfallible) {
+ AnnotateCrashReportWithJSException(cx, aURI);
+
+ MOZ_CRASH_UNSAFE_PRINTF("Failed to load critical module \"%s\"", aURI);
+ }
+ return rv;
+ }
+
+ if (aExportName) {
+ JS::RootedValue namedExport(cx);
+ if (!JS_GetProperty(cx, moduleNamespace, aExportName, &namedExport)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!namedExport.isObject()) {
+ return NS_ERROR_XPC_BAD_CONVERT_JS;
+ }
+ moduleNamespace.set(&namedExport.toObject());
+ }
+
+ return nsXPConnect::XPConnect()->WrapJS(cx, moduleNamespace, aIID, aResult);
+}
+
+} // namespace loader
+} // namespace mozilla
diff --git a/js/xpconnect/loader/nsImportModule.h b/js/xpconnect/loader/nsImportModule.h
new file mode 100644
index 0000000000..31f6f8c7c1
--- /dev/null
+++ b/js/xpconnect/loader/nsImportModule.h
@@ -0,0 +1,240 @@
+/* -*- 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 nsImportModule_h
+#define nsImportModule_h
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+
+#include "nsCOMPtr.h"
+#include "mozilla/RefPtr.h"
+
+namespace mozilla {
+namespace loader {
+
+nsresult ImportModule(const char* aURI, const char* aExportName,
+ const nsIID& aIID, void** aResult, bool aInfallible);
+
+nsresult ImportESModule(const char* aURI, const char* aExportName,
+ const nsIID& aIID, void** aResult, bool aInfallible);
+
+} // namespace loader
+} // namespace mozilla
+
+class MOZ_STACK_CLASS nsImportModule final : public nsCOMPtr_helper {
+ public:
+ nsImportModule(const char* aURI, const char* aExportName, nsresult* aErrorPtr,
+ bool aInfallible)
+ : mURI(aURI),
+ mExportName(aExportName),
+ mErrorPtr(aErrorPtr),
+ mInfallible(aInfallible) {
+ MOZ_ASSERT_IF(mErrorPtr, !mInfallible);
+ }
+
+ virtual nsresult NS_FASTCALL operator()(const nsIID& aIID,
+ void** aResult) const override {
+ nsresult rv = ::mozilla::loader::ImportModule(mURI, mExportName, aIID,
+ aResult, mInfallible);
+ if (mErrorPtr) {
+ *mErrorPtr = rv;
+ }
+ return rv;
+ }
+
+ private:
+ const char* mURI;
+ const char* mExportName;
+ nsresult* mErrorPtr;
+ bool mInfallible;
+};
+
+/**
+ * These helpers make it considerably easier for C++ code to import a JS module
+ * and wrap it in an appropriately-defined XPIDL interface for its exports.
+ * Typical usage is something like:
+ *
+ * Foo.jsm:
+ *
+ * var EXPORTED_SYMBOLS = ["foo"];
+ *
+ * function foo(bar) {
+ * return bar.toString();
+ * }
+ *
+ * mozIFoo.idl:
+ *
+ * interface mozIFoo : nsISupports {
+ * AString foo(double meh);
+ * }
+ *
+ * Thing.cpp:
+ *
+ * nsCOMPtr<mozIFoo> foo = do_ImportModule(
+ * "resource://meh/Foo.jsm");
+ *
+ * MOZ_TRY(foo->Foo(42));
+ *
+ * For JS modules which export all fields within a single named object, a second
+ * argument can be passed naming that object.
+ *
+ * Foo.jsm:
+ *
+ * var EXPORTED_SYMBOLS = ["Foo"];
+ *
+ * var Foo = {
+ * function foo(bar) {
+ * return bar.toString();
+ * }
+ * };
+ *
+ * Thing.cpp:
+ *
+ * nsCOMPtr<mozIFoo> foo = do_ImportModule(
+ * "resource:://meh/Foo.jsm", "Foo");
+ */
+
+template <size_t N>
+inline nsImportModule do_ImportModule(const char (&aURI)[N]) {
+ return {aURI, nullptr, nullptr, /* infallible */ true};
+}
+
+template <size_t N>
+inline nsImportModule do_ImportModule(const char (&aURI)[N],
+ const mozilla::fallible_t&) {
+ return {aURI, nullptr, nullptr, /* infallible */ false};
+}
+
+template <size_t N>
+inline nsImportModule do_ImportModule(const char (&aURI)[N], nsresult* aRv) {
+ return {aURI, nullptr, aRv, /* infallible */ false};
+}
+
+template <size_t N, size_t N2>
+inline nsImportModule do_ImportModule(const char (&aURI)[N],
+ const char (&aExportName)[N2]) {
+ return {aURI, aExportName, nullptr, /* infallible */ true};
+}
+
+template <size_t N, size_t N2>
+inline nsImportModule do_ImportModule(const char (&aURI)[N],
+ const char (&aExportName)[N2],
+ const mozilla::fallible_t&) {
+ return {aURI, aExportName, nullptr, /* infallible */ false};
+}
+
+template <size_t N, size_t N2>
+inline nsImportModule do_ImportModule(const char (&aURI)[N],
+ const char (&aExportName)[N2],
+ nsresult* aRv) {
+ return {aURI, aExportName, aRv, /* infallible */ false};
+}
+
+class MOZ_STACK_CLASS nsImportESModule final : public nsCOMPtr_helper {
+ public:
+ nsImportESModule(const char* aURI, const char* aExportName,
+ nsresult* aErrorPtr, bool aInfallible)
+ : mURI(aURI),
+ mExportName(aExportName),
+ mErrorPtr(aErrorPtr),
+ mInfallible(aInfallible) {
+ MOZ_ASSERT_IF(mErrorPtr, !mInfallible);
+ }
+
+ virtual nsresult NS_FASTCALL operator()(const nsIID& aIID,
+ void** aResult) const override {
+ nsresult rv = ::mozilla::loader::ImportESModule(mURI, mExportName, aIID,
+ aResult, mInfallible);
+ if (mErrorPtr) {
+ *mErrorPtr = rv;
+ }
+ return rv;
+ }
+
+ private:
+ const char* mURI;
+ const char* mExportName;
+ nsresult* mErrorPtr;
+ bool mInfallible;
+};
+
+/**
+ * Usage with exported name:
+ *
+ * Foo.sys.mjs:
+ *
+ * export function foo(bar) {
+ * return bar.toString();
+ * }
+ *
+ * mozIFoo.idl:
+ *
+ * interface mozIFoo : nsISupports {
+ * AString foo(double meh);
+ * }
+ *
+ * Thing.cpp:
+ *
+ * nsCOMPtr<mozIFoo> foo = do_ImportESModule(
+ * "resource://meh/Foo.sys.mjs");
+ *
+ * MOZ_TRY(foo->Foo(42));
+ *
+ * Usage with a single named object:
+ *
+ * Foo.sys.mjs:
+ *
+ * export var Foo = {
+ * function foo(bar) {
+ * return bar.toString();
+ * }
+ * };
+ *
+ * Thing.cpp:
+ *
+ * nsCOMPtr<mozIFoo> foo = do_ImportESModule(
+ * "resource:://meh/Foo.sys.mjs", "Foo");
+ */
+
+template <size_t N>
+inline nsImportESModule do_ImportESModule(const char (&aURI)[N]) {
+ return {aURI, nullptr, nullptr, /* infallible */ true};
+}
+
+template <size_t N>
+inline nsImportESModule do_ImportESModule(const char (&aURI)[N],
+ const mozilla::fallible_t&) {
+ return {aURI, nullptr, nullptr, /* infallible */ false};
+}
+
+template <size_t N>
+inline nsImportESModule do_ImportESModule(const char (&aURI)[N],
+ nsresult* aRv) {
+ return {aURI, nullptr, aRv, /* infallible */ false};
+}
+
+template <size_t N, size_t N2>
+inline nsImportESModule do_ImportESModule(const char (&aURI)[N],
+ const char (&aExportName)[N2]) {
+ return {aURI, aExportName, nullptr, /* infallible */ true};
+}
+
+template <size_t N, size_t N2>
+inline nsImportESModule do_ImportESModule(const char (&aURI)[N],
+ const char (&aExportName)[N2],
+ const mozilla::fallible_t&) {
+ return {aURI, aExportName, nullptr, /* infallible */ false};
+}
+
+template <size_t N, size_t N2>
+inline nsImportESModule do_ImportESModule(const char (&aURI)[N],
+ const char (&aExportName)[N2],
+ nsresult* aRv) {
+ return {aURI, aExportName, aRv, /* infallible */ false};
+}
+
+#endif // defined nsImportModule_h
diff --git a/js/xpconnect/loader/script_cache.py b/js/xpconnect/loader/script_cache.py
new file mode 100755
index 0000000000..bd3a746fcf
--- /dev/null
+++ b/js/xpconnect/loader/script_cache.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env 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/.
+
+import io
+import os
+import struct
+import sys
+
+MAGIC = b"mozXDRcachev002\0"
+
+
+def usage():
+ print(
+ """Usage: script_cache.py <file.bin> ...
+
+ Decodes and prints out the contents of a startup script cache file
+ (e.g., startupCache/scriptCache.bin) in human-readable form."""
+ )
+
+ sys.exit(1)
+
+
+class ProcessTypes:
+ Uninitialized = 0
+ Parent = 1
+ Web = 2
+ Extension = 3
+ Privileged = 4
+
+ def __init__(self, val):
+ self.val = val
+
+ def __str__(self):
+ res = []
+ if self.val & (1 << self.Uninitialized):
+ raise Exception("Uninitialized process type")
+ if self.val & (1 << self.Parent):
+ res.append("Parent")
+ if self.val & (1 << self.Web):
+ res.append("Web")
+ if self.val & (1 << self.Extension):
+ res.append("Extension")
+ if self.val & (1 << self.Privileged):
+ res.append("Privileged")
+ return "|".join(res)
+
+
+class InputBuffer(object):
+ def __init__(self, data):
+ self.data = data
+ self.offset = 0
+
+ @property
+ def remaining(self):
+ return len(self.data) - self.offset
+
+ def unpack(self, fmt):
+ res = struct.unpack_from(fmt, self.data, self.offset)
+ self.offset += struct.calcsize(fmt)
+ return res
+
+ def unpack_str(self):
+ (size,) = self.unpack("<H")
+ res = self.data[self.offset : self.offset + size].decode("utf-8")
+ self.offset += size
+ return res
+
+
+if len(sys.argv) < 2 or not os.path.exists(sys.argv[1]):
+ usage()
+
+for filename in sys.argv[1:]:
+ with io.open(filename, "rb") as f:
+ magic = f.read(len(MAGIC))
+ if magic != MAGIC:
+ raise Exception("Bad magic number")
+
+ (hdrSize,) = struct.unpack("<I", f.read(4))
+
+ hdr = InputBuffer(f.read(hdrSize))
+
+ i = 0
+ while hdr.remaining:
+ i += 1
+ print("{}: {}".format(i, hdr.unpack_str()))
+ print(" Key: {}".format(hdr.unpack_str()))
+ print(" Offset: {:>9,}".format(*hdr.unpack("<I")))
+ print(" Size: {:>9,}".format(*hdr.unpack("<I")))
+ print(" Processes: {}".format(ProcessTypes(*hdr.unpack("B"))))
+ print("")