summaryrefslogtreecommitdiffstats
path: root/js/xpconnect/loader
diff options
context:
space:
mode:
Diffstat (limited to 'js/xpconnect/loader')
-rw-r--r--js/xpconnect/loader/AutoMemMap.cpp154
-rw-r--r--js/xpconnect/loader/AutoMemMap.h100
-rw-r--r--js/xpconnect/loader/ChromeScriptLoader.cpp374
-rw-r--r--js/xpconnect/loader/ComponentUtils.jsm120
-rw-r--r--js/xpconnect/loader/IOBuffers.h148
-rw-r--r--js/xpconnect/loader/PScriptCache.ipdl33
-rw-r--r--js/xpconnect/loader/PrecompiledScript.h62
-rw-r--r--js/xpconnect/loader/ScriptCacheActors.cpp92
-rw-r--r--js/xpconnect/loader/ScriptCacheActors.h59
-rw-r--r--js/xpconnect/loader/ScriptPreloader-inl.h153
-rw-r--r--js/xpconnect/loader/ScriptPreloader.cpp1247
-rw-r--r--js/xpconnect/loader/ScriptPreloader.h563
-rw-r--r--js/xpconnect/loader/URLPreloader.cpp682
-rw-r--r--js/xpconnect/loader/URLPreloader.h316
-rw-r--r--js/xpconnect/loader/XPCOMUtils.jsm547
-rw-r--r--js/xpconnect/loader/moz.build63
-rw-r--r--js/xpconnect/loader/mozJSComponentLoader.cpp1356
-rw-r--r--js/xpconnect/loader/mozJSComponentLoader.h194
-rw-r--r--js/xpconnect/loader/mozJSLoaderUtils.cpp78
-rw-r--r--js/xpconnect/loader/mozJSLoaderUtils.h29
-rw-r--r--js/xpconnect/loader/mozJSSubScriptLoader.cpp507
-rw-r--r--js/xpconnect/loader/mozJSSubScriptLoader.h49
-rw-r--r--js/xpconnect/loader/nsImportModule.cpp46
-rw-r--r--js/xpconnect/loader/nsImportModule.h113
-rwxr-xr-xjs/xpconnect/loader/script_cache.py95
25 files changed, 7180 insertions, 0 deletions
diff --git a/js/xpconnect/loader/AutoMemMap.cpp b/js/xpconnect/loader/AutoMemMap.cpp
new file mode 100644
index 0000000000..92cb5bfd55
--- /dev/null
+++ b/js/xpconnect/loader/AutoMemMap.cpp
@@ -0,0 +1,154 @@
+/* -*- 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/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, &fd.rwget()));
+
+ 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 = 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, 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);
+
+ 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.dispose();
+}
+
+} // 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..210dd8ffd4
--- /dev/null
+++ b/js/xpconnect/loader/ChromeScriptLoader.cpp
@@ -0,0 +1,374 @@
+/* -*- 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/CompilationAndEvaluation.h"
+#include "js/SourceText.h"
+#include "js/Utility.h"
+
+#include "mozilla/Attributes.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/dom/ChromeUtils.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/ScriptLoader.h"
+#include "mozilla/HoldDropJSObjects.h"
+#include "nsCCUncollectableMarker.h"
+#include "nsCycleCollectionParticipant.h"
+
+using namespace JS;
+using namespace mozilla;
+using namespace mozilla::dom;
+
+class AsyncScriptCompiler final : public nsIIncrementalStreamLoaderObserver,
+ public Runnable {
+ 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_INHERITED
+ NS_DECL_NSIINCREMENTALSTREAMLOADEROBSERVER
+ NS_DECL_NSIRUNNABLE
+
+ AsyncScriptCompiler(JSContext* aCx, nsIGlobalObject* aGlobal,
+ const nsACString& aURL, Promise* aPromise)
+ : mozilla::Runnable("AsyncScriptCompiler"),
+ mOptions(aCx),
+ mURL(aURL),
+ mGlobalObject(aGlobal),
+ mPromise(aPromise),
+ mToken(nullptr),
+ mScriptLength(0) {}
+
+ MOZ_MUST_USE nsresult Start(JSContext* aCx,
+ const CompileScriptOptionsDictionary& aOptions,
+ nsIPrincipal* aPrincipal);
+
+ inline void SetToken(JS::OffThreadToken* aToken) { mToken = aToken; }
+
+ 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);
+ void FinishCompile(JSContext* aCx);
+ void Finish(JSContext* aCx, Handle<JSScript*> script);
+
+ OwningCompileOptions mOptions;
+ nsCString mURL;
+ nsCOMPtr<nsIGlobalObject> mGlobalObject;
+ RefPtr<Promise> mPromise;
+ nsString mCharset;
+ JS::OffThreadToken* mToken;
+ UniqueTwoByteChars mScriptText;
+ size_t mScriptLength;
+};
+
+NS_IMPL_QUERY_INTERFACE_INHERITED(AsyncScriptCompiler, Runnable,
+ nsIIncrementalStreamLoaderObserver)
+NS_IMPL_ADDREF_INHERITED(AsyncScriptCompiler, Runnable)
+NS_IMPL_RELEASE_INHERITED(AsyncScriptCompiler, Runnable)
+
+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);
+}
+
+static void OffThreadScriptLoaderCallback(JS::OffThreadToken* aToken,
+ void* aCallbackData) {
+ RefPtr<AsyncScriptCompiler> scriptCompiler =
+ dont_AddRef(static_cast<AsyncScriptCompiler*>(aCallbackData));
+
+ scriptCompiler->SetToken(aToken);
+
+ SchedulerGroup::Dispatch(TaskCategory::Other, scriptCompiler.forget());
+}
+
+bool AsyncScriptCompiler::StartCompile(JSContext* aCx) {
+ Rooted<JSObject*> global(aCx, mGlobalObject->GetGlobalJSObject());
+
+ JS::SourceText<char16_t> srcBuf;
+ if (!srcBuf.init(aCx, std::move(mScriptText), mScriptLength)) {
+ return false;
+ }
+
+ if (JS::CanCompileOffThread(aCx, mOptions, mScriptLength)) {
+ if (!JS::CompileOffThread(aCx, mOptions, srcBuf,
+ OffThreadScriptLoaderCallback,
+ static_cast<void*>(this))) {
+ return false;
+ }
+
+ NS_ADDREF(this);
+ return true;
+ }
+
+ Rooted<JSScript*> script(aCx, JS::Compile(aCx, mOptions, srcBuf));
+ if (!script) {
+ return false;
+ }
+
+ Finish(aCx, script);
+ return true;
+}
+
+NS_IMETHODIMP
+AsyncScriptCompiler::Run() {
+ AutoJSAPI jsapi;
+ if (jsapi.Init(mGlobalObject)) {
+ FinishCompile(jsapi.cx());
+ } else {
+ jsapi.Init();
+ JS::CancelOffThreadScript(jsapi.cx(), mToken);
+
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ }
+
+ return NS_OK;
+}
+
+void AsyncScriptCompiler::FinishCompile(JSContext* aCx) {
+ Rooted<JSScript*> script(aCx, JS::FinishOffThreadScript(aCx, mToken));
+ if (script) {
+ Finish(aCx, script);
+ } else {
+ Reject(aCx);
+ }
+}
+
+void AsyncScriptCompiler::Finish(JSContext* aCx, Handle<JSScript*> aScript) {
+ RefPtr<PrecompiledScript> result =
+ new PrecompiledScript(mGlobalObject, aScript, 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::ConvertToUTF16(
+ 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,
+ Handle<JSScript*> aScript,
+ JS::ReadOnlyCompileOptions& aOptions)
+ : mParent(aParent),
+ mScript(aScript),
+ mURL(aOptions.filename()),
+ mHasReturnValue(!aOptions.noScriptRval) {
+ MOZ_ASSERT(aParent);
+ MOZ_ASSERT(aScript);
+
+ mozilla::HoldJSObjects(this);
+};
+
+PrecompiledScript::~PrecompiledScript() { mozilla::DropJSObjects(this); }
+
+void PrecompiledScript::ExecuteInGlobal(JSContext* aCx, HandleObject aGlobal,
+ MutableHandleValue aRval,
+ ErrorResult& aRv) {
+ {
+ RootedObject targetObj(aCx, JS_FindCompilationScope(aCx, aGlobal));
+ JSAutoRealm ar(aCx, targetObj);
+
+ Rooted<JSScript*> script(aCx, mScript);
+ if (!JS::CloneAndExecuteScript(aCx, script, aRval)) {
+ aRv.NoteJSContextException(aCx);
+ 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_CLASS(PrecompiledScript)
+
+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_UNLINK_BEGIN(PrecompiledScript)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+
+ tmp->mScript = nullptr;
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(PrecompiledScript)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(PrecompiledScript)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mScript)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(PrecompiledScript)
+ if (tmp->IsBlackForCC(false)) {
+ tmp->mScript.exposeToActiveJS();
+ return true;
+ }
+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.jsm b/js/xpconnect/loader/ComponentUtils.jsm
new file mode 100644
index 0000000000..664d13a7db
--- /dev/null
+++ b/js/xpconnect/loader/ComponentUtils.jsm
@@ -0,0 +1,120 @@
+/* -*- 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.
+ *
+ * Import into a JS component using
+ * 'Components.utils.import("resource://gre/modules/ComponentUtils.jsm");'
+ *
+ * Exposing a JS 'class' as a component using these utility methods consists
+ * of several steps:
+ * 0. Import ComponentUtils, as described above.
+ * 1. Declare the 'class' (or multiple classes) implementing the component(s):
+ * function MyComponent() {
+ * // constructor
+ * }
+ * MyComponent.prototype = {
+ * // properties required for XPCOM registration:
+ * classID: Components.ID("{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}"),
+ *
+ * // [optional] custom factory (an object implementing nsIFactory). If not
+ * // provided, the default factory is used, which returns
+ * // |(new MyComponent()).QueryInterface(iid)| in its createInstance().
+ * _xpcom_factory: { ... },
+ *
+ * // QueryInterface implementation, e.g. using the generateQI helper
+ * QueryInterface: ChromeUtils.generateQI(
+ * [Components.interfaces.nsIObserver,
+ * Components.interfaces.nsIMyInterface,
+ * "nsIFoo",
+ * "nsIBar" ]),
+ *
+ * // The following properties were used prior to Mozilla 2, but are no
+ * // longer supported. They may still be included for compatibility with
+ * // prior versions of ComponentUtils. In Mozilla 2, this information is
+ * // included in the .manifest file which registers this JS component.
+ * classDescription: "unique text description",
+ * contractID: "@example.com/xxx;1",
+ *
+ * // ...component implementation...
+ * };
+ *
+ * 2. Create an array of component constructors (like the one
+ * created in step 1):
+ * var components = [MyComponent];
+ *
+ * 3. Define the NSGetFactory entry point:
+ * this.NSGetFactory = ComponentUtils.generateNSGetFactory(components);
+ */
+
+var EXPORTED_SYMBOLS = ["ComponentUtils"];
+
+const nsIFactoryQI = ChromeUtils.generateQI(["nsIFactory"]);
+
+var ComponentUtils = {
+ /**
+ * Generate a NSGetFactory function given an array of components.
+ */
+ generateNSGetFactory: function XPCU_generateNSGetFactory(componentsArray) {
+ let classes = {};
+ for (let i = 0; i < componentsArray.length; i++) {
+ let component = componentsArray[i];
+ if (!(component.prototype.classID instanceof Components.ID))
+ throw Error("In generateNSGetFactory, classID missing or incorrect for component " + component);
+
+ classes[component.prototype.classID] = this._getFactory(component);
+ }
+ return function NSGetFactory(cid) {
+ let cidstring = cid.toString();
+ if (cidstring in classes)
+ return classes[cidstring];
+ throw Cr.NS_ERROR_FACTORY_NOT_REGISTERED;
+ }
+ },
+
+ /**
+ * Returns an nsIFactory for |component|.
+ */
+ _getFactory: function XPCOMUtils__getFactory(component) {
+ var factory = component.prototype._xpcom_factory;
+ if (!factory) {
+ factory = {
+ createInstance: function(outer, iid) {
+ if (outer)
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ return (new component()).QueryInterface(iid);
+ },
+ QueryInterface: nsIFactoryQI
+ }
+ }
+ return factory;
+ },
+
+ /**
+ * generates a singleton nsIFactory implementation that can be used as
+ * the _xpcom_factory of the component.
+ * @param aServiceConstructor
+ * Constructor function of the component.
+ */
+ generateSingletonFactory:
+ function XPCOMUtils_generateSingletonFactory(aServiceConstructor) {
+ return {
+ _instance: null,
+ createInstance: function XPCU_SF_createInstance(aOuter, aIID) {
+ if (aOuter !== null) {
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+ 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/PScriptCache.ipdl b/js/xpconnect/loader/PScriptCache.ipdl
new file mode 100644
index 0000000000..20485e03ca
--- /dev/null
+++ b/js/xpconnect/loader/PScriptCache.ipdl
@@ -0,0 +1,33 @@
+/* -*- 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;
+
+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;
+};
+
+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..29212db0e4
--- /dev/null
+++ b/js/xpconnect/loader/PrecompiledScript.h
@@ -0,0 +1,62 @@
+/* -*- 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 "js/RootingAPI.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_SCRIPT_HOLDER_CLASS(PrecompiledScript)
+
+ explicit PrecompiledScript(nsISupports* aParent,
+ JS::Handle<JSScript*> aScript,
+ JS::ReadOnlyCompileOptions& aOptions);
+
+ void ExecuteInGlobal(JSContext* aCx, JS::HandleObject aGlobal,
+ JS::MutableHandleValue 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();
+
+ private:
+ bool IsBlackForCC(bool aTracingNeeded);
+
+ nsCOMPtr<nsISupports> mParent;
+
+ JS::Heap<JSScript*> mScript;
+ 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..b074b7cbb8
--- /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.NoteScript(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..86422ad084
--- /dev/null
+++ b/js/xpconnect/loader/ScriptPreloader-inl.h
@@ -0,0 +1,153 @@
+/* -*- 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();
+}
+
+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_;
+ Maybe<Iterator> iter_;
+
+ public:
+ explicit HashElemIter(T& hash, Matcher<ElemType>* matcher = nullptr)
+ : hash_(hash), matcher_(matcher) {
+ iter_.emplace(std::move(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_.ref(); }
+
+ 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) {
+ 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..f53498a5e0
--- /dev/null
+++ b/js/xpconnect/loader/ScriptPreloader.cpp
@@ -0,0 +1,1247 @@
+/* -*- 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/ScriptPreloader.h"
+#include "mozilla/loader/ScriptCacheActors.h"
+
+#include "mozilla/URLPreloader.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Components.h"
+#include "mozilla/FileUtils.h"
+#include "mozilla/IOBuffers.h"
+#include "mozilla/Logging.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Services.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/Document.h"
+
+#include "js/CompileOptions.h" // JS::ReadOnlyCompileOptions
+#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 CLEANUP_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;
+
+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;
+}
+
+ScriptPreloader& ScriptPreloader::GetSingleton() {
+ static RefPtr<ScriptPreloader> singleton;
+
+ if (!singleton) {
+ if (XRE_IsParentProcess()) {
+ singleton = new ScriptPreloader();
+ singleton->mChildCache = &GetChildSingleton();
+ Unused << singleton->InitCache();
+ } else {
+ singleton = &GetChildSingleton();
+ }
+
+ ClearOnShutdown(&singleton);
+ }
+
+ return *singleton;
+}
+
+// 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() {
+ static RefPtr<ScriptPreloader> singleton;
+
+ if (!singleton) {
+ singleton = new ScriptPreloader();
+ if (XRE_IsParentProcess()) {
+ Unused << singleton->InitCache(u"scriptCache-child"_ns);
+ }
+ ClearOnShutdown(&singleton);
+ }
+
+ return *singleton;
+}
+
+void ScriptPreloader::InitContentChild(ContentParent& parent) {
+ auto& cache = GetChildSingleton();
+
+ // 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;
+}
+
+namespace {
+
+static void TraceOp(JSTracer* trc, void* data) {
+ auto preloader = static_cast<ScriptPreloader*>(data);
+
+ preloader->Trace(trc);
+}
+
+} // anonymous namespace
+
+void ScriptPreloader::Trace(JSTracer* trc) {
+ for (auto& script : IterHash(mScripts)) {
+ script->mScript.Trace(trc);
+ }
+}
+
+ScriptPreloader::ScriptPreloader()
+ : mMonitor("[ScriptPreloader.mMonitor]"),
+ mSaveMonitor("[ScriptPreloader.mSaveMonitor]") {
+ // 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, CLEANUP_TOPIC, false);
+ obs->AddObserver(this, CACHE_INVALIDATE_TOPIC, false);
+
+ AutoSafeJSAPI jsapi;
+ JS_AddExtraGCRootsTracer(jsapi.cx(), TraceOp, this);
+}
+
+void ScriptPreloader::Cleanup() {
+ // Wait for any pending parses to finish before clearing the mScripts
+ // hashtable, since the parse tasks depend on memory allocated by those
+ // scripts.
+ {
+ MonitorAutoLock mal(mMonitor);
+ FinishPendingParses(mal);
+
+ mScripts.Clear();
+ }
+
+ AutoSafeJSAPI jsapi;
+ JS_RemoveExtraGCRootsTracer(jsapi.cx(), TraceOp, this);
+
+ 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 CachedScripts, and can't be canceled
+ // asynchronously.
+ FinishPendingParses(mal);
+
+ // Pending scripts should have been cleared by the above, and new parses
+ // should not have been queued.
+ MOZ_ASSERT(mParsingScripts.empty());
+ MOZ_ASSERT(mParsingSources.empty());
+ MOZ_ASSERT(mPendingScripts.isEmpty());
+
+ for (auto& script : IterHash(mScripts)) {
+ script.Remove();
+ }
+
+ // 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 && mChildCache) {
+ mSaveComplete = false;
+
+ StartCacheWrite();
+ }
+ }
+
+ {
+ MonitorAutoLock 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;
+ } 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, CLEANUP_TOPIC)) {
+ Cleanup();
+ } 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[] = "mozXDRcachev002";
+
+Result<Ok, nsresult> ScriptPreloader::OpenCache() {
+ 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) {
+ 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) {
+ 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;
+ if (size < sizeof(MAGIC) + sizeof(headerSize)) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ auto data = mCacheData.get<uint8_t>();
+ 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);
+
+ if (data + headerSize > end) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ {
+ auto cleanup = MakeScopeExit([&]() { mScripts.Clear(); });
+
+ LinkedList<CachedScript> scripts;
+
+ Range<uint8_t> header(data, data + headerSize);
+ data += headerSize;
+
+ InputBuffer buf(header);
+
+ size_t offset = 0;
+ while (!buf.finished()) {
+ auto script = MakeUnique<CachedScript>(*this, buf);
+ MOZ_RELEASE_ASSERT(script);
+
+ auto scriptData = data + script->mOffset;
+ 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;
+ }
+
+ mScripts.Put(script->mCachePath, script.get());
+ Unused << script.release();
+ }
+
+ if (buf.error()) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ mPendingScripts = std::move(scripts);
+ cleanup.release();
+ }
+
+ DecodeNextBatch(OFF_THREAD_FIRST_CHUNK_SIZE, scope);
+ return Ok();
+}
+
+void ScriptPreloader::PrepareCacheWriteInternal() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mMonitor.AssertCurrentThreadOwns();
+
+ auto cleanup = MakeScopeExit([&]() {
+ if (mChildCache) {
+ mChildCache->PrepareCacheWrite();
+ }
+ });
+
+ if (mDataPrepared) {
+ return;
+ }
+
+ AutoSafeJSAPI jsapi;
+ 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.
+ CachedScript* 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());
+
+ if (!mDataPrepared && !mSaveComplete) {
+ MonitorAutoUnlock mau(mSaveMonitor);
+
+ NS_DispatchToMainThread(
+ NewRunnableMethod("ScriptPreloader::PrepareCacheWrite", this,
+ &ScriptPreloader::PrepareCacheWrite),
+ NS_DISPATCH_SYNC);
+ }
+
+ 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 fd;
+ MOZ_TRY(cacheFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE, 0644,
+ &fd.rwget()));
+
+ // 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<CachedScript*> 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(CachedScript::Comparator());
+
+ OutputBuffer buf;
+ size_t offset = 0;
+ for (auto script : scripts) {
+ script->mOffset = offset;
+ script->Code(buf);
+
+ offset += script->mSize;
+ }
+
+ uint8_t headerSize[4];
+ LittleEndian::writeUint32(headerSize, buf.cursor());
+
+ MOZ_TRY(Write(fd, MAGIC, sizeof(MAGIC)));
+ MOZ_TRY(Write(fd, headerSize, sizeof(headerSize)));
+ MOZ_TRY(Write(fd, buf.Get(), buf.cursor()));
+ for (auto script : scripts) {
+ MOZ_TRY(Write(fd, script->Range().begin().get(), script->mSize));
+
+ if (script->mScript) {
+ script->FreeData();
+ }
+ }
+ }
+
+ MOZ_TRY(cacheFile->MoveTo(nullptr, mBaseName + u".bin"_ns));
+
+ return Ok();
+}
+
+// Runs in the mSaveThread thread, and writes out the cache file for the next
+// session after a reasonable delay.
+nsresult ScriptPreloader::Run() {
+ MonitorAutoLock 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());
+
+ 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::NoteScript(const nsCString& url,
+ const nsCString& cachePath,
+ JS::HandleScript jsscript, bool isRunOnce) {
+ if (!Active()) {
+ if (isRunOnce) {
+ if (auto script = mScripts.Get(cachePath)) {
+ script->mIsRunOnce = true;
+ script->MaybeDropScript();
+ }
+ }
+ 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.LookupOrAdd(cachePath, *this, url, cachePath, jsscript);
+ if (isRunOnce) {
+ script->mIsRunOnce = true;
+ }
+
+ if (!script->MaybeDropScript() && !script->mScript) {
+ MOZ_ASSERT(jsscript);
+ script->mScript.Set(jsscript);
+ script->mReadyToExecute = true;
+ }
+
+ script->UpdateLoadTime(TimeStamp::Now());
+ script->mProcessTypes += CurrentProcessType();
+}
+
+void ScriptPreloader::NoteScript(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.LookupOrAdd(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->mScript) {
+ // If the content process is sending us a script entry for a script
+ // 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::FillCompileOptionsForCachedScript(
+ JS::CompileOptions& options) {
+ // See IsMultiDecodeCompileOptionsMatching in js/src/vm/JSScript.cpp.
+ options.setNoScriptRval(true);
+ MOZ_ASSERT(!options.selfHostingMode);
+ MOZ_ASSERT(!options.isRunOnce);
+}
+
+JSScript* ScriptPreloader::GetCachedScript(
+ JSContext* cx, const JS::ReadOnlyCompileOptions& options,
+ const nsCString& path) {
+ // If a script is used by both the parent and the child, it's stored only
+ // in the child cache.
+ if (mChildCache) {
+ RootedScript script(
+ cx, mChildCache->GetCachedScriptInternal(cx, options, path));
+ if (script) {
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_SCRIPT_PRELOADER_REQUESTS::HitChild);
+ return script;
+ }
+ }
+
+ RootedScript script(cx, GetCachedScriptInternal(cx, options, path));
+ Telemetry::AccumulateCategorical(
+ script ? Telemetry::LABELS_SCRIPT_PRELOADER_REQUESTS::Hit
+ : Telemetry::LABELS_SCRIPT_PRELOADER_REQUESTS::Miss);
+ return script;
+}
+
+JSScript* ScriptPreloader::GetCachedScriptInternal(
+ JSContext* cx, const JS::ReadOnlyCompileOptions& options,
+ const nsCString& path) {
+ auto script = mScripts.Get(path);
+ if (script) {
+ return WaitForCachedScript(cx, options, script);
+ }
+
+ return nullptr;
+}
+
+JSScript* ScriptPreloader::WaitForCachedScript(
+ JSContext* cx, const JS::ReadOnlyCompileOptions& options,
+ CachedScript* script) {
+ // Always check for finished operations so that we can move on to decoding the
+ // next batch as soon as possible after the pending batch is ready. If we wait
+ // until we hit an unfinished script, we wind up having at most one batch of
+ // buffered scripts, and occasionally under-running that buffer.
+ if (JS::OffThreadToken* token = mToken.exchange(nullptr)) {
+ FinishOffThreadDecode(token);
+ }
+
+ if (!script->mReadyToExecute) {
+ LOG(Info, "Must wait for async script load: %s\n", script->mURL.get());
+ auto start = TimeStamp::Now();
+
+ // If script is small enough, we'd rather recompile 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 {
+ MonitorAutoLock mal(mMonitor);
+
+ // Process script batches until our target is found.
+ while (!script->mReadyToExecute) {
+ if (JS::OffThreadToken* token = mToken.exchange(nullptr)) {
+ MonitorAutoUnlock mau(mMonitor);
+ FinishOffThreadDecode(token);
+ } else {
+ MOZ_ASSERT(!mParsingScripts.empty());
+ 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->GetJSScript(cx, options);
+}
+
+/* static */
+void ScriptPreloader::OffThreadDecodeCallback(JS::OffThreadToken* token,
+ void* context) {
+ auto cache = static_cast<ScriptPreloader*>(context);
+
+ // Make the token available to main-thread asynchronously. The lock below is
+ // used for Wait/Notify machinery and isn't needed to update the token itself.
+ MOZ_ALWAYS_FALSE(cache->mToken.exchange(token));
+
+ cache->mMonitor.AssertNotCurrentThreadOwns();
+ MonitorAutoLock mal(cache->mMonitor);
+
+ if (cache->mWaitingForDecode) {
+ // Wake up the blocked main thread.
+ mal.Notify();
+ } else if (!cache->mFinishDecodeRunnablePending) {
+ // Issue a Runnable to ensure batches continue to decode even if the next
+ // WaitForCachedScript call has not happened yet.
+ cache->mFinishDecodeRunnablePending = true;
+ NS_DispatchToMainThread(
+ NewRunnableMethod("ScriptPreloader::DoFinishOffThreadDecode", cache,
+ &ScriptPreloader::DoFinishOffThreadDecode));
+ }
+}
+
+void ScriptPreloader::FinishPendingParses(MonitorAutoLock& aMal) {
+ mMonitor.AssertCurrentThreadOwns();
+
+ // Clear out scripts that we have not issued batch for yet.
+ mPendingScripts.clear();
+
+ // Process any pending decodes that are in flight.
+ while (!mParsingScripts.empty()) {
+ if (JS::OffThreadToken* token = mToken.exchange(nullptr)) {
+ MonitorAutoUnlock mau(mMonitor);
+ FinishOffThreadDecode(token);
+ } else {
+ mWaitingForDecode = true;
+ aMal.Wait();
+ mWaitingForDecode = false;
+ }
+ }
+}
+
+void ScriptPreloader::DoFinishOffThreadDecode() {
+ {
+ MonitorAutoLock mal(mMonitor);
+ mFinishDecodeRunnablePending = false;
+ }
+
+ if (JS::OffThreadToken* token = mToken.exchange(nullptr)) {
+ FinishOffThreadDecode(token);
+ }
+}
+
+void ScriptPreloader::FinishOffThreadDecode(JS::OffThreadToken* token) {
+ mMonitor.AssertNotCurrentThreadOwns();
+ MOZ_ASSERT(token);
+
+ auto cleanup = MakeScopeExit([&]() {
+ mParsingSources.clear();
+ mParsingScripts.clear();
+
+ DecodeNextBatch(OFF_THREAD_CHUNK_SIZE);
+ });
+
+ AutoSafeJSAPI jsapi;
+ JSContext* cx = jsapi.cx();
+
+ JSAutoRealm ar(cx, xpc::CompilationScope());
+ JS::Rooted<JS::ScriptVector> jsScripts(cx, JS::ScriptVector(cx));
+
+ // If this fails, we still need to mark the scripts as finished. Any that
+ // weren't successfully compiled in this operation (which should never
+ // happen under ordinary circumstances) will be re-decoded on the main
+ // thread, and raise the appropriate errors when they're executed.
+ //
+ // The exception from the off-thread decode operation will be reported when
+ // we pop the AutoJSAPI off the stack.
+ Unused << JS::FinishMultiOffThreadScriptsDecoder(cx, token, &jsScripts);
+
+ unsigned i = 0;
+ for (auto script : mParsingScripts) {
+ LOG(Debug, "Finished off-thread decode of %s\n", script->mURL.get());
+ if (i < jsScripts.length()) {
+ script->mScript.Set(jsScripts[i++]);
+ }
+ script->mReadyToExecute = true;
+ }
+}
+
+void ScriptPreloader::DecodeNextBatch(size_t chunkSize,
+ JS::HandleObject scope) {
+ MOZ_ASSERT(mParsingSources.length() == 0);
+ MOZ_ASSERT(mParsingScripts.length() == 0);
+
+ auto cleanup = MakeScopeExit([&]() {
+ mParsingScripts.clearAndFree();
+ mParsingSources.clearAndFree();
+ });
+
+ auto start = TimeStamp::Now();
+ LOG(Debug, "Off-thread decoding scripts...\n");
+
+ size_t size = 0;
+ for (CachedScript* next = mPendingScripts.getFirst(); next;) {
+ auto script = next;
+ next = script->getNext();
+
+ // 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 we have enough data for one chunk and this script would put us
+ // over our chunk size limit, we're done.
+ if (size > SMALL_SCRIPT_CHUNK_THRESHOLD &&
+ size + script->mSize > chunkSize) {
+ break;
+ }
+ if (!mParsingScripts.append(script) ||
+ !mParsingSources.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);
+
+ script->remove();
+ size += script->mSize;
+ }
+
+ if (size == 0 && mPendingScripts.isEmpty()) {
+ return;
+ }
+
+ AutoSafeJSAPI jsapi;
+ JSContext* cx = jsapi.cx();
+ JSAutoRealm ar(cx, scope ? scope : xpc::CompilationScope());
+
+ JS::CompileOptions options(cx);
+ FillCompileOptionsForCachedScript(options);
+ options.setSourceIsLazy(true);
+
+ if (!JS::CanCompileOffThread(cx, options, size) ||
+ !JS::DecodeMultiOffThreadScripts(cx, options, mParsingSources,
+ OffThreadDecodeCallback,
+ static_cast<void*>(this))) {
+ // If we fail here, we don't move on to process the next batch, so make
+ // sure we don't have any other scripts left to process.
+ MOZ_ASSERT(mPendingScripts.isEmpty());
+ for (auto script : mPendingScripts) {
+ script->mReadyToExecute = true;
+ }
+
+ LOG(Info, "Can't decode %lu bytes of scripts off-thread",
+ (unsigned long)size);
+ for (auto script : mParsingScripts) {
+ script->mReadyToExecute = true;
+ }
+ return;
+ }
+
+ cleanup.release();
+
+ LOG(Debug, "Initialized decoding of %u scripts (%u bytes) in %fms\n",
+ (unsigned)mParsingSources.length(), (unsigned)size,
+ (TimeStamp::Now() - start).ToMilliseconds());
+}
+
+ScriptPreloader::CachedScript::CachedScript(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 = {};
+}
+
+// JS::TraceEdge() can change the value of mScript, but not whether it is
+// null, so we don't update mHasScript to avoid a race.
+void ScriptPreloader::CachedScript::ScriptHolder::Trace(JSTracer* trc) {
+ JS::TraceEdge(trc, &mScript, "ScriptPreloader::CachedScript.mScript");
+}
+
+void ScriptPreloader::CachedScript::ScriptHolder::Set(
+ JS::HandleScript jsscript) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mScript = jsscript;
+ mHasScript = mScript;
+}
+
+void ScriptPreloader::CachedScript::ScriptHolder::Clear() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mScript = nullptr;
+ mHasScript = false;
+}
+
+bool ScriptPreloader::CachedScript::XDREncode(JSContext* cx) {
+ auto cleanup = MakeScopeExit([&]() { MaybeDropScript(); });
+
+ JSAutoRealm ar(cx, mScript.Get());
+ JS::RootedScript jsscript(cx, mScript.Get());
+
+ mXDRData.construct<JS::TranscodeBuffer>();
+
+ JS::TranscodeResult code = JS::EncodeScript(cx, Buffer(), jsscript);
+ if (code == JS::TranscodeResult_Ok) {
+ mXDRRange.emplace(Buffer().begin(), Buffer().length());
+ mSize = Range().length();
+ return true;
+ }
+ mXDRData.destroy();
+ JS_ClearPendingException(cx);
+ return false;
+}
+
+JSScript* ScriptPreloader::CachedScript::GetJSScript(
+ JSContext* cx, const JS::ReadOnlyCompileOptions& options) {
+ MOZ_ASSERT(mReadyToExecute);
+ if (mScript) {
+ if (JS::CheckCompileOptionsMatch(options, mScript.Get())) {
+ return mScript.Get();
+ }
+ LOG(Error, "Cached script %s has different options\n", mURL.get());
+ MOZ_DIAGNOSTIC_ASSERT(false, "Cached script has different options");
+ }
+
+ 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 script %s on main thread...\n", mURL.get());
+
+ JS::RootedScript script(cx);
+ if (JS::DecodeScript(cx, options, Range(), &script)) {
+ mScript.Set(script);
+
+ if (mCache.mSaveComplete) {
+ FreeData();
+ }
+ }
+
+ LOG(Debug, "Finished decoding in %fms",
+ (TimeStamp::Now() - start).ToMilliseconds());
+
+ return mScript.Get();
+}
+
+// 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,
+ 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..7b04b7fe74
--- /dev/null
+++ b/js/xpconnect/loader/ScriptPreloader.h
@@ -0,0 +1,563 @@
+/* -*- 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/Vector.h"
+#include "mozilla/Result.h"
+#include "mozilla/loader/AutoMemMap.h"
+#include "nsClassHashtable.h"
+#include "nsIAsyncShutdown.h"
+#include "nsIFile.h"
+#include "nsIMemoryReporter.h"
+#include "nsIObserver.h"
+#include "nsIThread.h"
+#include "nsITimer.h"
+
+#include "js/GCAnnotations.h" // for JS_HAZ_NON_GC_POINTER
+#include "js/RootingAPI.h" // for Handle, Heap
+#include "js/Transcoding.h" // for TranscodeBuffer, TranscodeRange, TranscodeSources
+#include "js/TypeDecls.h" // for HandleObject, HandleScript
+
+#include <prio.h>
+
+namespace JS {
+class CompileOptions;
+class OffThreadToken;
+} // namespace JS
+
+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 nsIAsyncShutdownBlocker {
+ 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_NSIASYNCSHUTDOWNBLOCKER
+
+ static ScriptPreloader& GetSingleton();
+ static ScriptPreloader& GetChildSingleton();
+
+ static ProcessType GetChildProcessType(const nsACString& remoteType);
+
+ // Fill some options that should be consistent across all scripts stored
+ // into preloader cache.
+ static void FillCompileOptionsForCachedScript(JS::CompileOptions& options);
+
+ // Retrieves the script with the given cache key from the script cache.
+ // Returns null if the script is not cached.
+ JSScript* GetCachedScript(JSContext* cx,
+ const JS::ReadOnlyCompileOptions& 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 NoteScript(const nsCString& url, const nsCString& cachePath,
+ JS::HandleScript script, bool isRunOnce = false);
+
+ void NoteScript(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() { return mCacheInitialized && !mStartupFinished; }
+
+ private:
+ Result<Ok, nsresult> InitCacheInternal(JS::HandleObject scope = nullptr);
+ JSScript* GetCachedScriptInternal(JSContext* cx,
+ const JS::ReadOnlyCompileOptions& options,
+ const nsCString& path);
+
+ public:
+ void Trace(JSTracer* trc);
+
+ static ProcessType CurrentProcessType() {
+ MOZ_ASSERT(sProcessType != ProcessType::Uninitialized);
+ return sProcessType;
+ }
+
+ static void InitContentChild(dom::ContentParent& parent);
+
+ protected:
+ virtual ~ScriptPreloader() = default;
+
+ private:
+ enum class ScriptStatus {
+ Restored,
+ Saved,
+ };
+
+ // Represents a cached JS script, either initially read from the script
+ // cache file, to be added to the next session's script cache file, or
+ // both.
+ //
+ // A script which was read from the cache file may be in any of the
+ // following states:
+ //
+ // - Read from the cache, and being compiled off thread. In this case,
+ // mReadyToExecute is false, and mToken is null.
+ // - Off-thread compilation has finished, but the script has not yet been
+ // executed. In this case, mReadyToExecute is true, and mToken has a
+ // non-null value.
+ // - Read from the cache, but too small or needed to immediately to be
+ // compiled off-thread. In this case, mReadyToExecute is true, and both
+ // mToken and mScript are null.
+ // - Fully decoded, and ready to be added to the next session's cache
+ // file. In this case, mReadyToExecute is true, and mScript is non-null.
+ //
+ // A script to be added to the next session's cache file always has a
+ // non-null mScript 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 CachedScript : public LinkedListElement<CachedScript> {
+ public:
+ CachedScript(CachedScript&&) = delete;
+
+ CachedScript(ScriptPreloader& cache, const nsCString& url,
+ const nsCString& cachePath, JSScript* script)
+ : mCache(cache),
+ mURL(url),
+ mCachePath(cachePath),
+ mScript(script),
+ mReadyToExecute(true),
+ mIsRunOnce(false) {}
+
+ inline CachedScript(ScriptPreloader& cache, InputBuffer& buf);
+
+ ~CachedScript() = 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 CachedScript* a, const CachedScript* b) const {
+ return a->mLoadTime == b->mLoadTime;
+ }
+
+ bool LessThan(const CachedScript* a, const CachedScript* b) const {
+ return a->mLoadTime < b->mLoadTime;
+ }
+ };
+
+ struct StatusMatcher final : public Matcher<CachedScript*> {
+ explicit StatusMatcher(ScriptStatus status) : mStatus(status) {}
+
+ virtual bool Matches(CachedScript* script) override {
+ return script->Status() == mStatus;
+ }
+
+ const ScriptStatus mStatus;
+ };
+
+ // The purpose of this helper class is to avoid a race between
+ // ScriptPreloader::WriteCache() and the GC on a JSScript*.
+ // The former checks if the actual JSScript* is null on the save thread
+ // while holding mMonitor. Aside from GC tracing, all places that mutate
+ // the JSScript* either hold mMonitor or don't run at the same time as the
+ // save thread. The GC can move the script, which will cause the value to
+ // change, but this will not change whether it is null or not.
+ //
+ // We can't hold mMonitor while tracing, because we can end running the
+ // GC while the current thread already holds mMonitor. Instead, this class
+ // avoids the race by storing a separate field to indicate if the script is
+ // null or not. To enforce this, the mutation by the GC that cannot affect
+ // the nullness of the script is split out from other mutation.
+ class MOZ_HEAP_CLASS ScriptHolder {
+ public:
+ explicit ScriptHolder(JSScript* script)
+ : mScript(script), mHasScript(script) {}
+ ScriptHolder() : mHasScript(false) {}
+
+ // This should only be called on the main thread (either while holding
+ // the preloader's mMonitor or while the save thread isn't running), or on
+ // the save thread while holding the preloader's mMonitor.
+ explicit operator bool() const { return mHasScript; }
+
+ // This should only be called on the main thread.
+ JSScript* Get() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mScript;
+ }
+
+ // This should only be called on the main thread (or from a GC thread
+ // while the main thread is GCing).
+ void Trace(JSTracer* trc);
+
+ // These should only be called on the main thread, either while holding
+ // the preloader's mMonitor or while the save thread isn't running.
+ void Set(JS::HandleScript jsscript);
+ void Clear();
+
+ private:
+ JS::Heap<JSScript*> mScript;
+ bool mHasScript; // true iff mScript is non-null.
+ };
+
+ 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 (!mXDRData.empty()) {
+ 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 MaybeDropScript() {
+ if (mIsRunOnce && (HasRange() || !mCache.WillWriteScripts())) {
+ mScript.Clear();
+ 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(); }
+
+ nsTArray<uint8_t>& Array() {
+ MOZ_ASSERT(HasArray());
+ return mXDRData.ref<nsTArray<uint8_t>>();
+ }
+
+ bool HasArray() { return mXDRData.constructed<nsTArray<uint8_t>>(); }
+
+ JSScript* GetJSScript(JSContext* cx,
+ const JS::ReadOnlyCompileOptions& options);
+
+ size_t HeapSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) {
+ auto size = mallocSizeOf(this);
+
+ if (HasArray()) {
+ size += Array().ShallowSizeOfExcludingThis(mallocSizeOf);
+ } else if (HasBuffer()) {
+ size += Buffer().sizeOfExcludingThis(mallocSizeOf);
+ } else {
+ return size;
+ }
+
+ // 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{};
+
+ ScriptHolder mScript;
+
+ // 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 script
+ // is too small to be decoded off-thread, 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.
+ MaybeOneOf<JS::TranscodeBuffer, nsTArray<uint8_t>> mXDRData;
+ } JS_HAZ_NON_GC_POINTER;
+
+ template <ScriptStatus status>
+ static Matcher<CachedScript*>* Match() {
+ static CachedScript::StatusMatcher matcher{status};
+ return &matcher;
+ }
+
+ // There's a significant setup cost for each off-thread decode operation,
+ // so scripts are decoded in chunks to minimize the overhead. There's a
+ // careful balancing act in choosing the size of chunks, to minimize the
+ // number of decode operations, while also minimizing the number of buffer
+ // underruns that require the main thread to wait for a script to finish
+ // decoding.
+ //
+ // For the first chunk, we don't have much time between the start of the
+ // decode operation and the time the first script is needed, so that chunk
+ // needs to be fairly small. After the first chunk is finished, we have
+ // some buffered scripts to fall back on, and a lot more breathing room,
+ // so the chunks can be a bit bigger, but still not too big.
+ static constexpr int OFF_THREAD_FIRST_CHUNK_SIZE = 128 * 1024;
+ static constexpr int OFF_THREAD_CHUNK_SIZE = 512 * 1024;
+
+ // Ideally, we want every chunk to be smaller than the chunk sizes
+ // specified above. However, if we have some number of small scripts
+ // followed by a huge script that would put us over the normal chunk size,
+ // we're better off processing them as a single chunk.
+ //
+ // In order to guarantee that the JS engine will process a chunk
+ // off-thread, it needs to be at least 100K (which is an implementation
+ // detail that can change at any time), so make sure that we always hit at
+ // least that size, with a bit of breathing room to be safe.
+ static constexpr int SMALL_SCRIPT_CHUNK_THRESHOLD = 128 * 1024;
+
+ // 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;
+
+ ScriptPreloader();
+
+ 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();
+
+ 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.
+ JSScript* WaitForCachedScript(JSContext* cx,
+ const JS::ReadOnlyCompileOptions& options,
+ CachedScript* script);
+
+ void DecodeNextBatch(size_t chunkSize, JS::HandleObject scope = nullptr);
+
+ static void OffThreadDecodeCallback(JS::OffThreadToken* token, void* context);
+ void FinishOffThreadDecode(JS::OffThreadToken* token);
+ 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, CachedScript>;
+
+ 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 = false;
+
+ // The list of scripts that we read from the initial startup cache file,
+ // but have yet to initiate a decode task for.
+ LinkedList<CachedScript> mPendingScripts;
+
+ // The lists of scripts and their sources that make up the chunk currently
+ // being decoded in a background thread.
+ JS::TranscodeSources mParsingSources;
+ Vector<CachedScript*> mParsingScripts;
+
+ // The token for the completed off-thread decode task.
+ Atomic<JS::OffThreadToken*, ReleaseAcquire> mToken{nullptr};
+
+ // True if a runnable has been dispatched to the main thread to finish an
+ // off-thread decode operation. Access only while 'mMonitor' is held.
+ bool mFinishDecodeRunnablePending = false;
+
+ // True is main-thread is blocked and we should notify with Monitor. Access
+ // only while `mMonitor` is held.
+ bool mWaitingForDecode = 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.
+ AutoMemMap mCacheData;
+
+ Monitor mMonitor;
+ Monitor mSaveMonitor;
+};
+
+} // namespace mozilla
+
+#endif // ScriptPreloader_h
diff --git a/js/xpconnect/loader/URLPreloader.cpp b/js/xpconnect/loader/URLPreloader.cpp
new file mode 100644
index 0000000000..912b13235f
--- /dev/null
+++ b/js/xpconnect/loader/URLPreloader.cpp
@@ -0,0 +1,682 @@
+/* -*- 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/Logging.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Services.h"
+#include "mozilla/Unused.h"
+#include "mozilla/Vector.h"
+
+#include "MainThreadUtils.h"
+#include "nsPrintfCString.h"
+#include "nsDebug.h"
+#include "nsIFile.h"
+#include "nsIFileURL.h"
+#include "nsIObserverService.h"
+#include "nsNetUtil.h"
+#include "nsPromiseFlatString.h"
+#include "nsProxyRelease.h"
+#include "nsThreadUtils.h"
+#include "nsXULAppAPI.h"
+#include "nsZipArchive.h"
+#include "xpcpublic.h"
+
+#undef DELAYED_STARTUP_TOPIC
+#define DELAYED_STARTUP_TOPIC "sessionstore-windows-restored"
+
+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;
+
+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 : IterHash(mCachedURLs)) {
+ 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);
+ }
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+
+ MOZ_TRY(obs->AddObserver(this, DELAYED_STARTUP_TOPIC, false));
+
+ MOZ_TRY(NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(mProfD)));
+
+ return Ok();
+}
+
+URLPreloader& URLPreloader::ReInitialize() {
+ MOZ_ASSERT(sSingleton);
+ 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[] = "mozURLcachev002";
+
+Result<nsCOMPtr<nsIFile>, nsresult> URLPreloader::FindCacheFile() {
+ 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());
+
+ // 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 fd;
+ MOZ_TRY(cacheFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE, 0644,
+ &fd.rwget()));
+
+ nsTArray<URLEntry*> entries;
+ for (auto& entry : IterHash(mCachedURLs)) {
+ if (entry->mReadTime) {
+ entries.AppendElement(entry);
+ }
+ }
+
+ entries.Sort(URLEntry::Comparator());
+
+ OutputBuffer buf;
+ for (auto entry : entries) {
+ entry->Code(buf);
+ }
+
+ uint8_t headerSize[4];
+ LittleEndian::writeUint32(headerSize, buf.cursor());
+
+ MOZ_TRY(Write(fd, URL_MAGIC, sizeof(URL_MAGIC)));
+ MOZ_TRY(Write(fd, headerSize, sizeof(headerSize)));
+ 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;
+ if (size < sizeof(URL_MAGIC) + sizeof(headerSize)) {
+ 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);
+
+ if (data + headerSize > end) {
+ 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());
+
+ auto entry = mCachedURLs.LookupOrAdd(key, key);
+ entry->mResultCode = NS_ERROR_NOT_INITIALIZED;
+
+ pendingURLs.insertBack(entry);
+ }
+
+ if (buf.error()) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ 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) {
+ URLEntry entry(key);
+
+ return entry.Read();
+ }
+
+ auto entry = mCachedURLs.LookupOrAdd(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); }
+
+nsresult URLPreloader::Observe(nsISupports* subject, const char* topic,
+ const char16_t* data) {
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (!strcmp(topic, DELAYED_STARTUP_TOPIC)) {
+ obs->RemoveObserver(this, DELAYED_STARTUP_TOPIC);
+ mStartupFinished = true;
+ }
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(URLPreloader, nsIObserver, 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..77daa680b7
--- /dev/null
+++ b/js/xpconnect/loader/URLPreloader.h
@@ -0,0 +1,316 @@
+/* -*- 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 "nsIObserver.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 nsIObserver, public nsIMemoryReporter {
+ MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf)
+
+ URLPreloader() = default;
+
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ 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);
+
+ 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));
+ 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);
+ }
+
+ 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() {
+ 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{"[URLPreloader::mMutex]"};
+};
+
+} // namespace mozilla
+
+#endif // URLPreloader_h
diff --git a/js/xpconnect/loader/XPCOMUtils.jsm b/js/xpconnect/loader/XPCOMUtils.jsm
new file mode 100644
index 0000000000..1a18c7a69b
--- /dev/null
+++ b/js/xpconnect/loader/XPCOMUtils.jsm
@@ -0,0 +1,547 @@
+/* -*- 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/. */
+
+var EXPORTED_SYMBOLS = ["XPCOMUtils"];
+
+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 = {
+ Headers: "fetch",
+ MessagePort: "MessageChannel",
+ Request: "fetch",
+ Response: "fetch",
+};
+
+/**
+ * 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;
+}
+
+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: function XPCU_defineLazyGetter(aObject, aName, aLambda)
+ {
+ let redefining = false;
+ Object.defineProperty(aObject, aName, {
+ get: function () {
+ if (!redefining) {
+ // Make sure we don't get into an infinite recursion loop if
+ // the getter lambda does something shady.
+ redefining = true;
+ return redefine(aObject, aName, aLambda.apply(aObject));
+ }
+ },
+ configurable: true,
+ enumerable: true
+ });
+ },
+
+ /**
+ * 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: function XPCU_defineLazyScriptGetter(aObject, aNames,
+ aResource)
+ {
+ if (!Array.isArray(aNames)) {
+ aNames = [aNames];
+ }
+ for (let name of aNames) {
+ Object.defineProperty(aObject, name, {
+ get: function() {
+ Services.scriptloader.loadSubScript(aResource, aObject);
+ return aObject[name];
+ },
+ set(value) {
+ redefine(aObject, name, value);
+ },
+ configurable: true,
+ enumerable: true
+ });
+ }
+ },
+
+ /**
+ * 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) {
+ this.defineLazyGetter(aObject, name, () => {
+ if (!(name in global)) {
+ let importName = EXTRA_GLOBAL_NAME_TO_IMPORT_NAME[name] || name;
+ 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: function XPCU_defineLazyServiceGetter(aObject, aName,
+ aContract,
+ aInterfaceName)
+ {
+ this.defineLazyGetter(aObject, aName, function XPCU_serviceLambda() {
+ 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: function XPCU_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 getter on a specified object for a module. The module will not
+ * be imported until first use. The getter allows to execute setup and
+ * teardown code (e.g. to register/unregister to services) and accepts
+ * a proxy object which acts on behalf of the module until it is imported.
+ *
+ * @param aObject
+ * The object to define the lazy getter on.
+ * @param aName
+ * The name of the getter to define on aObject for the module.
+ * @param aResource
+ * The URL used to obtain the module.
+ * @param aSymbol
+ * The name of the symbol exported by the module.
+ * This parameter is optional and defaults to aName.
+ * @param aPreLambda
+ * A function that is executed when the proxy is set up.
+ * This will only ever be called once.
+ * @param aPostLambda
+ * A function that is executed when the module has been imported to
+ * run optional teardown procedures on the proxy object.
+ * This will only ever be called once.
+ * @param aProxy
+ * An object which acts on behalf of the module to be imported until
+ * the module has been imported.
+ */
+ defineLazyModuleGetter: function XPCU_defineLazyModuleGetter(
+ aObject, aName, aResource, aSymbol,
+ aPreLambda, aPostLambda, aProxy)
+ {
+ if (arguments.length == 3) {
+ return ChromeUtils.defineModuleGetter(aObject, aName, aResource);
+ }
+
+ let proxy = aProxy || {};
+
+ if (typeof(aPreLambda) === "function") {
+ aPreLambda.apply(proxy);
+ }
+
+ this.defineLazyGetter(aObject, aName, function XPCU_moduleLambda() {
+ var temp = {};
+ try {
+ ChromeUtils.import(aResource, temp);
+
+ if (typeof(aPostLambda) === "function") {
+ aPostLambda.apply(proxy);
+ }
+ } catch (ex) {
+ Cu.reportError("Failed to load module " + aResource + ".");
+ throw ex;
+ }
+ return temp[aSymbol || aName];
+ });
+ },
+
+ /**
+ * 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: function XPCU_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 aDefaultValue
+ * The default value to use, if the preference is not defined.
+ * @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: function XPCU_defineLazyPreferenceGetter(
+ aObject, aName, aPreference,
+ aDefaultValue = null,
+ aOnUpdate = null,
+ aTransform = val => val)
+ {
+ // 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 = aDefaultValue;
+ 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: function XPCOMUtils__defineConstant(aObj, aName, aValue) {
+ Object.defineProperty(aObj, aName, {
+ value: aValue,
+ enumerable: true,
+ writable: false
+ });
+ },
+
+ /**
+ * Defines a proxy which acts as a lazy object getter that can be passed
+ * around as a reference, and will only be evaluated when something in
+ * that object gets accessed.
+ *
+ * The evaluation can be triggered by a function call, by getting or
+ * setting a property, calling this as a constructor, or enumerating
+ * the properties of this object (e.g. during an iteration).
+ *
+ * Please note that, even after evaluated, the object given to you
+ * remains being the proxy object (which forwards everything to the
+ * real object). This is important to correctly use these objects
+ * in pairs of add+remove listeners, for example.
+ * If your use case requires access to the direct object, you can
+ * get it through the untrap callback.
+ *
+ * @param aObject
+ * The object to define the lazy getter on.
+ *
+ * You can pass null to aObject if you just want to get this
+ * proxy through the return value.
+ *
+ * @param aName
+ * The name of the getter to define on aObject.
+ *
+ * @param aInitFuncOrResource
+ * A function or a module that defines what this object actually
+ * should be when it gets evaluated. This will only ever be called once.
+ *
+ * Short-hand: If you pass a string to this parameter, it will be treated
+ * as the URI of a module to be imported, and aName will be used as
+ * the symbol to retrieve from the module.
+ *
+ * @param aStubProperties
+ * In this parameter, you can provide an object which contains
+ * properties from the original object that, when accessed, will still
+ * prevent the entire object from being evaluated.
+ *
+ * These can be copies or simplified versions of the original properties.
+ *
+ * One example is to provide an alternative QueryInterface implementation
+ * to avoid the entire object from being evaluated when it's added as an
+ * observer (as addObserver calls object.QueryInterface(Ci.nsIObserver)).
+ *
+ * Once the object has been evaluated, the properties from the real
+ * object will be used instead of the ones provided here.
+ *
+ * @param aUntrapCallback
+ * A function that gets called once when the object has just been evaluated.
+ * You can use this to do some work (e.g. setting properties) that you need
+ * to do on this object but that can wait until it gets evaluated.
+ *
+ * Another use case for this is to use during code development to log when
+ * this object gets evaluated, to make sure you're not accidentally triggering
+ * it earlier than expected.
+ */
+ defineLazyProxy: function XPCOMUtils__defineLazyProxy(aObject, aName, aInitFuncOrResource,
+ aStubProperties, aUntrapCallback) {
+ let initFunc = aInitFuncOrResource;
+
+ if (typeof(aInitFuncOrResource) == "string") {
+ initFunc = function () {
+ let tmp = {};
+ ChromeUtils.import(aInitFuncOrResource, tmp);
+ return tmp[aName];
+ };
+ }
+
+ let handler = new LazyProxyHandler(aName, initFunc,
+ aStubProperties, aUntrapCallback);
+
+ /*
+ * We cannot simply create a lazy getter for the underlying
+ * object and pass it as the target of the proxy, because
+ * just passing it in `new Proxy` means it would get
+ * evaluated. Becase of this, a full handler needs to be
+ * implemented (the LazyProxyHandler).
+ *
+ * So, an empty object is used as the target, and the handler
+ * replaces it on every call with the real object.
+ */
+ let proxy = new Proxy({}, handler);
+
+ if (aObject) {
+ Object.defineProperty(aObject, aName, {
+ value: proxy,
+ enumerable: true,
+ writable: true,
+ });
+ }
+
+ return proxy;
+ },
+};
+
+/**
+ * LazyProxyHandler
+ * This class implements the handler used
+ * in the proxy from defineLazyProxy.
+ *
+ * This handler forwards all calls to an underlying object,
+ * stored as `this.realObject`, which is obtained as the returned
+ * value from aInitFunc, which will be called on the first time
+ * time that it needs to be used (with an exception in the get() trap
+ * for the properties provided in the `aStubProperties` parameter).
+ */
+
+class LazyProxyHandler {
+ constructor(aName, aInitFunc, aStubProperties, aUntrapCallback) {
+ this.pending = true;
+ this.name = aName;
+ this.initFuncOrResource = aInitFunc;
+ this.stubProperties = aStubProperties;
+ this.untrapCallback = aUntrapCallback;
+ }
+
+ getObject() {
+ if (this.pending) {
+ this.realObject = this.initFuncOrResource.call(null);
+
+ if (this.untrapCallback) {
+ this.untrapCallback.call(null, this.realObject);
+ this.untrapCallback = null;
+ }
+
+ this.pending = false;
+ this.stubProperties = null;
+ }
+ return this.realObject;
+ }
+
+ getPrototypeOf(target) {
+ return Reflect.getPrototypeOf(this.getObject());
+ }
+
+ setPrototypeOf(target, prototype) {
+ return Reflect.setPrototypeOf(this.getObject(), prototype);
+ }
+
+ isExtensible(target) {
+ return Reflect.isExtensible(this.getObject());
+ }
+
+ preventExtensions(target) {
+ return Reflect.preventExtensions(this.getObject());
+ }
+
+ getOwnPropertyDescriptor(target, prop) {
+ return Reflect.getOwnPropertyDescriptor(this.getObject(), prop);
+ }
+
+ defineProperty(target, prop, descriptor) {
+ return Reflect.defineProperty(this.getObject(), prop, descriptor);
+ }
+
+ has(target, prop) {
+ return Reflect.has(this.getObject(), prop);
+ }
+
+ get(target, prop, receiver) {
+ if (this.pending &&
+ this.stubProperties &&
+ Object.prototype.hasOwnProperty.call(this.stubProperties, prop)) {
+ return this.stubProperties[prop];
+ }
+ return Reflect.get(this.getObject(), prop, receiver);
+ }
+
+ set(target, prop, value, receiver) {
+ return Reflect.set(this.getObject(), prop, value, receiver);
+ }
+
+ deleteProperty(target, prop) {
+ return Reflect.deleteProperty(this.getObject(), prop);
+ }
+
+ ownKeys(target) {
+ return Reflect.ownKeys(this.getObject());
+ }
+}
+
+var XPCU_lazyPreferenceObserverQI = ChromeUtils.generateQI(["nsIObserver", "nsISupportsWeakReference"]);
+
+ChromeUtils.defineModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
diff --git a/js/xpconnect/loader/moz.build b/js/xpconnect/loader/moz.build
new file mode 100644
index 0000000000..926d3ce0de
--- /dev/null
+++ b/js/xpconnect/loader/moz.build
@@ -0,0 +1,63 @@
+# -*- 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",
+ "mozJSLoaderUtils.cpp",
+ "mozJSSubScriptLoader.cpp",
+ "nsImportModule.cpp",
+ "ScriptCacheActors.cpp",
+ "ScriptPreloader.cpp",
+ "URLPreloader.cpp",
+]
+
+# mozJSComponentLoader.cpp cannot be built in unified mode because it uses
+# windows.h
+SOURCES += [
+ "mozJSComponentLoader.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",
+]
+
+EXTRA_JS_MODULES += [
+ "ComponentUtils.jsm",
+ "XPCOMUtils.jsm",
+]
+
+FINAL_LIBRARY = "xul"
+
+LOCAL_INCLUDES += [
+ "../src",
+ "../wrappers",
+ "/dom/base",
+ "/xpcom/base/",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+if CONFIG["CC_TYPE"] in ("clang", "gcc"):
+ CXXFLAGS += ["-Wno-shadow"]
diff --git a/js/xpconnect/loader/mozJSComponentLoader.cpp b/js/xpconnect/loader/mozJSComponentLoader.cpp
new file mode 100644
index 0000000000..23c5e1d0c3
--- /dev/null
+++ b/js/xpconnect/loader/mozJSComponentLoader.cpp
@@ -0,0 +1,1356 @@
+/* -*- 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/Attributes.h"
+#include "mozilla/Utf8.h" // mozilla::Utf8Unit
+
+#include <cstdarg>
+
+#include "mozilla/Logging.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/friend/JSMEnvironment.h" // JS::ExecuteInJSMEnvironment, JS::GetJSMEnvironmentOfScriptedCaller, JS::NewJSMEnvironment
+#include "js/Object.h" // JS::GetCompartment
+#include "js/Printf.h"
+#include "js/PropertySpec.h"
+#include "js/SourceText.h" // JS::SourceText
+#include "nsCOMPtr.h"
+#include "nsExceptionHandler.h"
+#include "nsIComponentManager.h"
+#include "mozilla/Module.h"
+#include "nsIFile.h"
+#include "mozJSComponentLoader.h"
+#include "mozJSLoaderUtils.h"
+#include "nsIFileURL.h"
+#include "nsIJARURI.h"
+#include "nsIChannel.h"
+#include "nsNetUtil.h"
+#include "nsJSPrincipals.h"
+#include "nsJSUtils.h"
+#include "xpcprivate.h"
+#include "xpcpublic.h"
+#include "nsContentUtils.h"
+#include "nsReadableUtils.h"
+#include "nsXULAppAPI.h"
+#include "GeckoProfiler.h"
+#include "WrapperFactory.h"
+
+#include "AutoMemMap.h"
+#include "ScriptPreloader-inl.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/ResultExtensions.h"
+#include "mozilla/ScriptPreloader.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/UniquePtrExtensions.h"
+#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(aType) "jsloader/" aType
+
+/**
+ * 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=JSComponentLoader:5
+static LazyLogModule gJSCLLog("JSComponentLoader");
+
+#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 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;
+ }
+
+#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;
+}
+
+mozJSComponentLoader::mozJSComponentLoader()
+ : mModules(16),
+ mImports(16),
+ mInProgressImports(16),
+ mLocations(16),
+ mInitialized(false),
+ mLoaderGlobal(dom::RootingCx()) {
+ MOZ_ASSERT(!sSelf, "mozJSComponentLoader should be a singleton");
+}
+
+#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 ComponentLoaderInfo {
+ public:
+ explicit ComponentLoaderInfo(const nsACString& aLocation)
+ : mLocation(aLocation) {}
+
+ 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);
+ return mIOService->NewURI(mLocation, nullptr, nullptr,
+ getter_AddRefs(mURI));
+ }
+
+ nsIChannel* ScriptChannel() {
+ MOZ_ASSERT(mScriptChannel);
+ return mScriptChannel;
+ }
+ nsresult EnsureScriptChannel() {
+ BEGIN_ENSURE(ScriptChannel, IOService, URI);
+ return NS_NewChannel(
+ getter_AddRefs(mScriptChannel), mURI,
+ nsContentUtils::GetSystemPrincipal(),
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_SCRIPT,
+ nullptr, // nsICookieJarSettings
+ nullptr, // aPerformanceStorage
+ nullptr, // aLoadGroup
+ nullptr, // aCallbacks
+ nsIRequest::LOAD_NORMAL, mIOService);
+ }
+
+ nsIURI* ResolvedURI() {
+ MOZ_ASSERT(mResolvedURI);
+ return mResolvedURI;
+ }
+ nsresult EnsureResolvedURI() {
+ BEGIN_ENSURE(ResolvedURI, URI);
+ return ResolveURI(mURI, getter_AddRefs(mResolvedURI));
+ }
+
+ const nsACString& Key() { return mLocation; }
+
+ MOZ_MUST_USE nsresult GetLocation(nsCString& aLocation) {
+ nsresult rv = EnsureURI();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mURI->GetSpec(aLocation);
+ }
+
+ private:
+ const nsACString& mLocation;
+ nsCOMPtr<nsIIOService> mIOService;
+ nsCOMPtr<nsIURI> mURI;
+ nsCOMPtr<nsIChannel> mScriptChannel;
+ nsCOMPtr<nsIURI> mResolvedURI;
+};
+
+template <typename... Args>
+static nsresult ReportOnCallerUTF8(JSCLContextHelper& helper,
+ const char* format,
+ ComponentLoaderInfo& 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
+
+mozJSComponentLoader::~mozJSComponentLoader() {
+ MOZ_ASSERT(!mInitialized,
+ "UnloadModules() was not explicitly called before cleaning up "
+ "mozJSComponentLoader");
+
+ if (mInitialized) {
+ UnloadModules();
+ }
+
+ sSelf = nullptr;
+}
+
+StaticRefPtr<mozJSComponentLoader> mozJSComponentLoader::sSelf;
+
+// For terrible compatibility reasons, we need to consider both the global
+// lexical environment and the global of modules when searching for exported
+// symbols.
+static JSObject* ResolveModuleObjectProperty(JSContext* aCx,
+ HandleObject aModObj,
+ const char* name) {
+ if (JS_HasExtensibleLexicalEnvironment(aModObj)) {
+ RootedObject lexical(aCx, JS_ExtensibleLexicalEnvironment(aModObj));
+ bool found;
+ if (!JS_HasOwnProperty(aCx, lexical, name, &found)) {
+ return nullptr;
+ }
+ if (found) {
+ return lexical;
+ }
+ }
+ return aModObj;
+}
+
+static mozilla::Result<nsCString, nsresult> ReadScript(
+ ComponentLoaderInfo& aInfo);
+
+static nsresult AnnotateScriptContents(CrashReporter::Annotation aName,
+ const nsACString& aURI) {
+ ComponentLoaderInfo info(aURI);
+
+ nsCString str;
+ MOZ_TRY_VAR(str, ReadScript(info));
+
+ // The crash reporter won't accept any strings with embedded nuls. We
+ // shouldn't have any here, but if we do because of data corruption, we
+ // still want the annotation. So replace any embedded nuls before
+ // annotating.
+ str.ReplaceSubstring("\0"_ns, "\\0"_ns);
+
+ CrashReporter::AnnotateCrashReport(aName, str);
+
+ return NS_OK;
+}
+
+nsresult mozJSComponentLoader::AnnotateCrashReport() {
+ Unused << AnnotateScriptContents(
+ CrashReporter::Annotation::nsAsyncShutdownComponent,
+ "resource://gre/components/nsAsyncShutdown.js"_ns);
+
+ Unused << AnnotateScriptContents(
+ CrashReporter::Annotation::AsyncShutdownModule,
+ "resource://gre/modules/AsyncShutdown.jsm"_ns);
+
+ return NS_OK;
+}
+
+const mozilla::Module* mozJSComponentLoader::LoadModule(FileLocation& aFile) {
+ if (!NS_IsMainThread()) {
+ MOZ_ASSERT(false, "Don't use JS components off the main thread");
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIFile> file = aFile.GetBaseFile();
+
+ nsCString spec;
+ aFile.GetURIString(spec);
+ ComponentLoaderInfo info(spec);
+ nsresult rv = info.EnsureURI();
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ mInitialized = true;
+
+ AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING("mozJSComponentLoader::LoadModule",
+ OTHER, spec);
+ AUTO_PROFILER_MARKER_TEXT("JS XPCOM", JS, MarkerStack::Capture(), spec);
+
+ ModuleEntry* mod;
+ if (mModules.Get(spec, &mod)) {
+ return mod;
+ }
+
+ dom::AutoJSAPI jsapi;
+ jsapi.Init();
+ JSContext* cx = jsapi.cx();
+
+ bool isCriticalModule = StringEndsWith(spec, "/nsAsyncShutdown.js"_ns);
+
+ auto entry = MakeUnique<ModuleEntry>(RootingContext::get(cx));
+ RootedValue exn(cx);
+ rv = ObjectForLocation(info, file, &entry->obj, &entry->thisObjectKey,
+ &entry->location, isCriticalModule, &exn);
+ if (NS_FAILED(rv)) {
+ // Temporary debugging assertion for bug 1403348:
+ if (isCriticalModule && !exn.isUndefined()) {
+ AnnotateCrashReport();
+
+ JSAutoRealm ar(cx, xpc::PrivilegedJunkScope());
+ JS_WrapValue(cx, &exn);
+
+ nsAutoCString file;
+ uint32_t line;
+ uint32_t column;
+ nsAutoString msg;
+ nsContentUtils::ExtractErrorValues(cx, exn, file, &line, &column, msg);
+
+ NS_ConvertUTF16toUTF8 cMsg(msg);
+ MOZ_CRASH_UNSAFE_PRINTF(
+ "Failed to load module \"%s\": "
+ "[\"%s\" {file: \"%s\", line: %u}]",
+ spec.get(), cMsg.get(), file.get(), line);
+ }
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIComponentManager> cm;
+ rv = NS_GetComponentManager(getter_AddRefs(cm));
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+
+ JSAutoRealm ar(cx, entry->obj);
+ RootedObject entryObj(cx, entry->obj);
+
+ RootedObject NSGetFactoryHolder(
+ cx, ResolveModuleObjectProperty(cx, entryObj, "NSGetFactory"));
+ RootedValue NSGetFactory_val(cx);
+ if (!NSGetFactoryHolder ||
+ !JS_GetProperty(cx, NSGetFactoryHolder, "NSGetFactory",
+ &NSGetFactory_val) ||
+ NSGetFactory_val.isUndefined()) {
+ return nullptr;
+ }
+
+ if (JS_TypeOfValue(cx, NSGetFactory_val) != JSTYPE_FUNCTION) {
+ /*
+ * spec's encoding is ASCII unless it's zip file, otherwise it's
+ * random encoding. Latin1 variant is safe for random encoding.
+ */
+ JS_ReportErrorLatin1(
+ cx, "%s has NSGetFactory property that is not a function", spec.get());
+ return nullptr;
+ }
+
+ RootedObject jsGetFactoryObj(cx);
+ if (!JS_ValueToObject(cx, NSGetFactory_val, &jsGetFactoryObj) ||
+ !jsGetFactoryObj) {
+ /* XXX report error properly */
+ return nullptr;
+ }
+
+ rv = nsXPConnect::XPConnect()->WrapJS(cx, jsGetFactoryObj,
+ NS_GET_IID(xpcIJSGetFactory),
+ getter_AddRefs(entry->getfactoryobj));
+ if (NS_FAILED(rv)) {
+ /* XXX report error properly */
+#ifdef DEBUG
+ fprintf(stderr, "mJCL: couldn't get nsIModule from jsval\n");
+#endif
+ return nullptr;
+ }
+
+#if defined(NIGHTLY_BUILD) || defined(DEBUG)
+ if (Preferences::GetBool("browser.startup.record", false)) {
+ entry->importStack = xpc_PrintJSStack(cx, false, false, false).get();
+ }
+#endif
+
+ // Cache this module for later
+ mModules.Put(spec, entry.get());
+
+ // The hash owns the ModuleEntry now, forget about it
+ return entry.release();
+}
+
+void mozJSComponentLoader::FindTargetObject(JSContext* aCx,
+ MutableHandleObject aTargetObject) {
+ aTargetObject.set(JS::GetJSMEnvironmentOfScriptedCaller(aCx));
+
+ // The above could fail if the scripted caller is not a component/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 mozJSComponentLoader::InitStatics() {
+ MOZ_ASSERT(!sSelf);
+ sSelf = new mozJSComponentLoader();
+}
+
+void mozJSComponentLoader::Unload() {
+ if (sSelf) {
+ sSelf->UnloadModules();
+ }
+}
+
+void mozJSComponentLoader::Shutdown() {
+ MOZ_ASSERT(sSelf);
+ sSelf = nullptr;
+}
+
+// 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 (auto iter = aTable.ConstIter(); !iter.Done(); iter.Next()) {
+ n += iter.Key().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ n += iter.Data()->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ return n;
+}
+
+size_t mozJSComponentLoader::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) {
+ size_t n = aMallocSizeOf(this);
+ n += SizeOfTableExcludingThis(mModules, aMallocSizeOf);
+ n += SizeOfTableExcludingThis(mImports, aMallocSizeOf);
+ n += mLocations.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ n += SizeOfTableExcludingThis(mInProgressImports, aMallocSizeOf);
+ return n;
+}
+
+void mozJSComponentLoader::CreateLoaderGlobal(JSContext* aCx,
+ const nsACString& aLocation,
+ MutableHandleObject aGlobal) {
+ auto backstagePass = MakeRefPtr<BackstagePass>();
+ RealmOptions options;
+
+ options.creationOptions().setNewCompartmentInSystemZone();
+ 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);
+ nsresult rv = xpc::InitClassesWithNewWrappedGlobal(
+ aCx, static_cast<nsIGlobalObject*>(backstagePass),
+ nsContentUtils::GetSystemPrincipal(), xpc::DONT_FIRE_ONNEWGLOBALHOOK,
+ options, &global);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ NS_ENSURE_TRUE_VOID(global);
+
+ backstagePass->SetGlobalObject(global);
+
+ JSAutoRealm ar(aCx, global);
+ if (!JS_DefineFunctions(aCx, global, gGlobalFun)) {
+ return;
+ }
+
+ // Set the location information for the new global, so that tools like
+ // about:memory may use that information
+ xpc::SetLocationForGlobal(global, aLocation);
+
+ aGlobal.set(global);
+}
+
+JSObject* mozJSComponentLoader::GetSharedGlobal(JSContext* aCx) {
+ if (!mLoaderGlobal) {
+ JS::RootedObject globalObj(aCx);
+ CreateLoaderGlobal(aCx, "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, "component loader report global");
+ JS_FireOnNewGlobalObject(aes.cx(), globalObj);
+ }
+
+ return mLoaderGlobal;
+}
+
+JSObject* mozJSComponentLoader::PrepareObjectForLocation(
+ JSContext* aCx, nsIFile* aComponentFile, nsIURI* aURI, bool* aRealFile) {
+ nsAutoCString nativePath;
+ NS_ENSURE_SUCCESS(aURI->GetSpec(nativePath), nullptr);
+
+ RootedObject globalObj(aCx, GetSharedGlobal(aCx));
+
+ // |thisObj| is the object we set properties on for a particular .jsm.
+ RootedObject thisObj(aCx, globalObj);
+ NS_ENSURE_TRUE(thisObj, nullptr);
+
+ JSAutoRealm ar(aCx, thisObj);
+
+ thisObj = JS::NewJSMEnvironment(aCx);
+ NS_ENSURE_TRUE(thisObj, nullptr);
+
+ *aRealFile = false;
+
+ // 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
+ {
+ // Create an extra scope so that ~nsCOMPtr will run before the returned
+ // JSObject* is placed on the stack, since otherwise a GC in the destructor
+ // would invalidate the return value.
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(aURI, &rv);
+ nsCOMPtr<nsIFile> testFile;
+ if (NS_SUCCEEDED(rv)) {
+ fileURL->GetFile(getter_AddRefs(testFile));
+ }
+
+ if (testFile) {
+ *aRealFile = true;
+
+ if (XRE_IsParentProcess()) {
+ RootedObject locationObj(aCx);
+
+ rv = nsXPConnect::XPConnect()->WrapNative(aCx, thisObj, aComponentFile,
+ 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.
+ 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(
+ ComponentLoaderInfo& aInfo) {
+ MOZ_TRY(aInfo.EnsureScriptChannel());
+
+ nsCOMPtr<nsIInputStream> scriptStream;
+ MOZ_TRY(NS_MaybeOpenChannelUsingOpen(aInfo.ScriptChannel(),
+ 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 mozJSComponentLoader::ObjectForLocation(
+ ComponentLoaderInfo& aInfo, nsIFile* aComponentFile,
+ 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();
+
+ bool realFile = false;
+ nsresult rv = aInfo.EnsureURI();
+ NS_ENSURE_SUCCESS(rv, rv);
+ RootedObject obj(
+ cx, PrepareObjectForLocation(cx, aComponentFile, aInfo.URI(), &realFile));
+ NS_ENSURE_TRUE(obj, NS_ERROR_FAILURE);
+ MOZ_ASSERT(!JS_IsGlobalObject(obj));
+
+ JSAutoRealm ar(cx, obj);
+
+ RootedScript script(cx);
+
+ nsAutoCString nativePath;
+ rv = aInfo.URI()->GetSpec(nativePath);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Before compiling the script, first check to see if we have it in
+ // the startupcache. Note: as a rule, startupcache errors are not fatal
+ // to loading the script, since we can always slow-load.
+
+ bool writeToCache = false;
+ StartupCache* cache = StartupCache::GetSingleton();
+
+ aInfo.EnsureResolvedURI();
+
+ nsAutoCString cachePath(JS_CACHE_PREFIX("non-syntactic"));
+ rv = PathifyURI(aInfo.ResolvedURI(), cachePath);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ CompileOptions options(cx);
+ ScriptPreloader::FillCompileOptionsForCachedScript(options);
+ options.setForceStrictMode()
+ .setFileAndLine(nativePath.get(), 1)
+ .setSourceIsLazy(true)
+ .setNonSyntacticScope(true);
+
+ script =
+ ScriptPreloader::GetSingleton().GetCachedScript(cx, options, cachePath);
+ if (!script && cache) {
+ ReadCachedScript(cache, cachePath, cx, options, &script);
+ }
+
+ if (script) {
+ LOG(("Successfully loaded %s from startupcache\n", nativePath.get()));
+ } else if (cache) {
+ // This is ok, it just means the script is not yet in the
+ // cache. Could mean that the cache was corrupted and got removed,
+ // but either way we're going to write this out.
+ writeToCache = true;
+ // ReadCachedScript may have set a pending exception.
+ JS_ClearPendingException(cx);
+ }
+
+ if (!script) {
+ // The script wasn't in the cache , so compile it now.
+ LOG(("Slow loading %s\n", nativePath.get()));
+
+ // 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 (!cache && !ScriptPreloader::GetSingleton().Active()) {
+ options.setSourceIsLazy(false);
+ }
+
+ if (realFile) {
+ AutoMemMap map;
+ MOZ_TRY(map.init(aComponentFile));
+
+ // 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(cx, buf.get(), map.size(),
+ JS::SourceOwnership::Borrowed)) {
+ script = Compile(cx, options, srcBuf);
+ } else {
+ MOZ_ASSERT(!script);
+ }
+ } else {
+ nsCString str;
+ MOZ_TRY_VAR(str, ReadScript(aInfo));
+
+ JS::SourceText<mozilla::Utf8Unit> srcBuf;
+ if (srcBuf.init(cx, str.get(), str.Length(),
+ JS::SourceOwnership::Borrowed)) {
+ script = Compile(cx, options, srcBuf);
+ } else {
+ MOZ_ASSERT(!script);
+ }
+ }
+ // Propagate the exception, if one exists. Also, don't leave the stale
+ // exception on this context.
+ if (!script && aPropagateExceptions && jsapi.HasException()) {
+ if (!jsapi.StealException(aException)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+ }
+
+ if (!script) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT_IF(ScriptPreloader::GetSingleton().Active(), options.sourceIsLazy);
+ ScriptPreloader::GetSingleton().NoteScript(nativePath, cachePath, script);
+
+ if (writeToCache) {
+ MOZ_ASSERT(options.sourceIsLazy);
+
+ // We successfully compiled the script, so cache it.
+ rv = WriteCachedScript(cache, cachePath, cx, script);
+
+ // 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"));
+ }
+ }
+
+ // 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),
+ "component loader load module");
+ JSContext* aescx = aes.cx();
+
+ bool executeOk = false;
+ if (JS_IsGlobalObject(obj)) {
+ JS::RootedValue rval(cx);
+ executeOk = JS::CloneAndExecuteScript(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;
+ }
+ }
+
+ /* Freed when we remove from the table. */
+ *aLocation = ToNewCString(nativePath, mozilla::fallible);
+ if (!*aLocation) {
+ aObject.set(nullptr);
+ aTableScript.set(nullptr);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ return NS_OK;
+}
+
+void mozJSComponentLoader::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;
+ }
+
+ mInProgressImports.Clear();
+ mImports.Clear();
+ mLocations.Clear();
+
+ for (auto iter = mModules.Iter(); !iter.Done(); iter.Next()) {
+ iter.Data()->Clear();
+ iter.Remove();
+ }
+}
+
+nsresult mozJSComponentLoader::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 mozJSComponentLoader::IsModuleLoaded(const nsACString& aLocation,
+ bool* retval) {
+ MOZ_ASSERT(nsContentUtils::IsCallerChrome());
+
+ mInitialized = true;
+ ComponentLoaderInfo info(aLocation);
+ *retval = !!mImports.Get(info.Key());
+ return NS_OK;
+}
+
+void mozJSComponentLoader::GetLoadedModules(
+ nsTArray<nsCString>& aLoadedModules) {
+ aLoadedModules.SetCapacity(mImports.Count());
+ for (auto iter = mImports.Iter(); !iter.Done(); iter.Next()) {
+ aLoadedModules.AppendElement(iter.Data()->location);
+ }
+}
+
+void mozJSComponentLoader::GetLoadedComponents(
+ nsTArray<nsCString>& aLoadedComponents) {
+ aLoadedComponents.SetCapacity(mModules.Count());
+ for (auto iter = mModules.Iter(); !iter.Done(); iter.Next()) {
+ aLoadedComponents.AppendElement(iter.Data()->location);
+ }
+}
+
+nsresult mozJSComponentLoader::GetModuleImportStack(const nsACString& aLocation,
+ nsACString& retval) {
+#ifdef STARTUP_RECORDER_ENABLED
+ MOZ_ASSERT(nsContentUtils::IsCallerChrome());
+ MOZ_ASSERT(mInitialized);
+
+ ComponentLoaderInfo info(aLocation);
+
+ ModuleEntry* mod;
+ if (!mImports.Get(info.Key(), &mod)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ retval = mod->importStack;
+ return NS_OK;
+#else
+ return NS_ERROR_NOT_IMPLEMENTED;
+#endif
+}
+
+nsresult mozJSComponentLoader::GetComponentLoadStack(
+ const nsACString& aLocation, nsACString& retval) {
+#ifdef STARTUP_RECORDER_ENABLED
+ MOZ_ASSERT(nsContentUtils::IsCallerChrome());
+ MOZ_ASSERT(mInitialized);
+
+ ComponentLoaderInfo info(aLocation);
+ nsresult rv = info.EnsureURI();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ModuleEntry* mod;
+ if (!mModules.Get(info.Key(), &mod)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ retval = mod->importStack;
+ return NS_OK;
+#else
+ return NS_ERROR_NOT_IMPLEMENTED;
+#endif
+}
+
+static JSObject* ResolveModuleObjectPropertyById(JSContext* aCx,
+ HandleObject aModObj,
+ HandleId id) {
+ if (JS_HasExtensibleLexicalEnvironment(aModObj)) {
+ RootedObject lexical(aCx, JS_ExtensibleLexicalEnvironment(aModObj));
+ bool found;
+ if (!JS_HasOwnPropertyById(aCx, lexical, id, &found)) {
+ return nullptr;
+ }
+ if (found) {
+ return lexical;
+ }
+ }
+ return aModObj;
+}
+
+nsresult mozJSComponentLoader::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 mozJSComponentLoader::ExtractExports(
+ JSContext* aCx, ComponentLoaderInfo& 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, JSID_TO_STRING(symbolId));
+ 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, JSID_TO_STRING(symbolId));
+ 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, JSID_TO_STRING(symbolId));
+ 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, JSID_TO_STRING(symbolId));
+ 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;
+}
+
+nsresult mozJSComponentLoader::Import(JSContext* aCx,
+ const nsACString& aLocation,
+ JS::MutableHandleObject aModuleGlobal,
+ JS::MutableHandleObject aModuleExports,
+ bool aIgnoreExports) {
+ mInitialized = true;
+
+ AUTO_PROFILER_MARKER_TEXT("ChromeUtils.import", JS, MarkerStack::Capture(),
+ aLocation);
+
+ ComponentLoaderInfo info(aLocation);
+
+ 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::ShutdownFinal)) {
+ return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
+ }
+
+ newEntry = MakeUnique<ModuleEntry>(RootingContext::get(aCx));
+
+ // Note: This implies EnsureURI().
+ MOZ_TRY(info.EnsureResolvedURI());
+
+ // get the JAR if there is one
+ nsCOMPtr<nsIJARURI> jarURI;
+ jarURI = do_QueryInterface(info.ResolvedURI(), &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(info.ResolvedURI(), &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIFile> sourceFile;
+ rv = baseFileURL->GetFile(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.Put(newEntry->resolvedURL, new nsCString(info.Key()));
+
+ RootedValue exception(aCx);
+ {
+ mInProgressImports.Put(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)) {
+ if (!exception.isUndefined()) {
+ // An exception was thrown during compilation. Propagate it
+ // out to our caller so they can report it.
+ if (!JS_WrapValue(aCx, &exception)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ JS_SetPendingException(aCx, exception);
+ return NS_ERROR_FAILURE;
+ }
+
+ // Something failed, but we don't know what it is, guess.
+ return NS_ERROR_FILE_NOT_FOUND;
+ }
+
+#ifdef STARTUP_RECORDER_ENABLED
+ if (Preferences::GetBool("browser.startup.record", false)) {
+ newEntry->importStack = xpc_PrintJSStack(aCx, false, false, false).get();
+ }
+#endif
+
+ mod = newEntry.get();
+ }
+
+ MOZ_ASSERT(mod->obj, "Import table contains entry with no object");
+ aModuleGlobal.set(mod->obj);
+
+ JS::RootedObject exports(aCx, mod->exports);
+ if (!exports && !aIgnoreExports) {
+ MOZ_TRY(ExtractExports(aCx, info, mod, &exports));
+ }
+
+ if (exports && !JS_WrapObject(aCx, &exports)) {
+ return NS_ERROR_FAILURE;
+ }
+ aModuleExports.set(exports);
+
+ // Cache this module for later
+ if (newEntry) {
+ mImports.Put(info.Key(), newEntry.release());
+ }
+
+ return NS_OK;
+}
+
+nsresult mozJSComponentLoader::Unload(const nsACString& aLocation) {
+ if (!mInitialized) {
+ return NS_OK;
+ }
+
+ ComponentLoaderInfo 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;
+}
+
+size_t mozJSComponentLoader::ModuleEntry::SizeOfIncludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ size_t n = aMallocSizeOf(this);
+ n += aMallocSizeOf(location);
+
+ return n;
+}
+
+/* static */
+already_AddRefed<nsIFactory> mozJSComponentLoader::ModuleEntry::GetFactory(
+ const mozilla::Module& module, const mozilla::Module::CIDEntry& entry) {
+ const ModuleEntry& self = static_cast<const ModuleEntry&>(module);
+ MOZ_ASSERT(self.getfactoryobj, "Handing out an uninitialized module?");
+
+ nsCOMPtr<nsIFactory> f;
+ nsresult rv = self.getfactoryobj->Get(*entry.cid, getter_AddRefs(f));
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+
+ return f.forget();
+}
+
+//----------------------------------------------------------------------
+
+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/mozJSComponentLoader.h b/js/xpconnect/loader/mozJSComponentLoader.h
new file mode 100644
index 0000000000..920fe5163d
--- /dev/null
+++ b/js/xpconnect/loader/mozJSComponentLoader.h
@@ -0,0 +1,194 @@
+/* -*- 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 mozJSComponentLoader_h
+#define mozJSComponentLoader_h
+
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/FileLocation.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/Module.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "nsISupports.h"
+#include "nsIURI.h"
+#include "nsClassHashtable.h"
+#include "nsDataHashtable.h"
+#include "jsapi.h"
+
+#include "xpcIJSGetFactory.h"
+#include "xpcpublic.h"
+
+class nsIFile;
+class ComponentLoaderInfo;
+
+namespace mozilla {
+class ScriptPreloader;
+} // namespace mozilla
+
+#if defined(NIGHTLY_BUILD) || defined(MOZ_DEV_EDITION) || defined(DEBUG)
+# define STARTUP_RECORDER_ENABLED
+#endif
+
+class mozJSComponentLoader final {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(mozJSComponentLoader);
+
+ void GetLoadedModules(nsTArray<nsCString>& aLoadedModules);
+ void GetLoadedComponents(nsTArray<nsCString>& aLoadedComponents);
+ nsresult GetModuleImportStack(const nsACString& aLocation,
+ nsACString& aRetval);
+ nsresult GetComponentLoadStack(const nsACString& aLocation,
+ nsACString& aRetval);
+
+ const mozilla::Module* LoadModule(mozilla::FileLocation& aFile);
+
+ void FindTargetObject(JSContext* aCx, JS::MutableHandleObject aTargetObject);
+
+ static void InitStatics();
+ static void Unload();
+ static void Shutdown();
+
+ static mozJSComponentLoader* Get() {
+ MOZ_ASSERT(sSelf, "Should have already created the component loader");
+ return sSelf;
+ }
+
+ nsresult ImportInto(const nsACString& aResourceURI,
+ JS::HandleValue aTargetObj, JSContext* aCx, uint8_t aArgc,
+ JS::MutableHandleValue aRetval);
+
+ nsresult Import(JSContext* aCx, const nsACString& aResourceURI,
+ JS::MutableHandleObject aModuleGlobal,
+ JS::MutableHandleObject aModuleExports,
+ bool aIgnoreExports = false);
+
+ nsresult Unload(const nsACString& aResourceURI);
+ nsresult IsModuleLoaded(const nsACString& aResourceURI, bool* aRetval);
+ bool IsLoaderGlobal(JSObject* aObj) { return mLoaderGlobal == aObj; }
+
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+
+ /**
+ * Temporary diagnostic function for startup crashes in bug 1403348:
+ *
+ * Annotate the crash report with the contents of the async shutdown
+ * module/component scripts.
+ */
+ nsresult AnnotateCrashReport();
+
+ protected:
+ mozJSComponentLoader();
+ ~mozJSComponentLoader();
+
+ friend class XPCJSRuntime;
+
+ private:
+ static mozilla::StaticRefPtr<mozJSComponentLoader> sSelf;
+
+ void UnloadModules();
+
+ void CreateLoaderGlobal(JSContext* aCx, const nsACString& aLocation,
+ JS::MutableHandleObject aGlobal);
+
+ JSObject* GetSharedGlobal(JSContext* aCx);
+
+ JSObject* PrepareObjectForLocation(JSContext* aCx, nsIFile* aComponentFile,
+ nsIURI* aComponent, bool* aRealFile);
+
+ nsresult ObjectForLocation(ComponentLoaderInfo& aInfo,
+ nsIFile* aComponentFile,
+ JS::MutableHandleObject aObject,
+ JS::MutableHandleScript aTableScript,
+ char** location, bool aCatchException,
+ JS::MutableHandleValue aException);
+
+ nsresult ImportInto(const nsACString& aLocation, JS::HandleObject targetObj,
+ JSContext* callercx, JS::MutableHandleObject vp);
+
+ nsCOMPtr<nsIComponentManager> mCompMgr;
+
+ class ModuleEntry : public mozilla::Module {
+ public:
+ explicit ModuleEntry(JS::RootingContext* aRootingCx)
+ : mozilla::Module(),
+ obj(aRootingCx),
+ exports(aRootingCx),
+ thisObjectKey(aRootingCx) {
+ mVersion = mozilla::Module::kVersion;
+ mCIDs = nullptr;
+ mContractIDs = nullptr;
+ mCategoryEntries = nullptr;
+ getFactoryProc = GetFactory;
+ loadProc = nullptr;
+ unloadProc = nullptr;
+
+ location = nullptr;
+ }
+
+ ~ModuleEntry() { Clear(); }
+
+ void Clear() {
+ getfactoryobj = nullptr;
+
+ 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;
+#ifdef STARTUP_RECORDER_ENABLED
+ importStack.Truncate();
+#endif
+ }
+
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ static already_AddRefed<nsIFactory> GetFactory(
+ const mozilla::Module& module, const mozilla::Module::CIDEntry& entry);
+
+ nsCOMPtr<xpcIJSGetFactory> getfactoryobj;
+ JS::PersistentRootedObject obj;
+ JS::PersistentRootedObject exports;
+ JS::PersistentRootedScript thisObjectKey;
+ char* location;
+ nsCString resolvedURL;
+#ifdef STARTUP_RECORDER_ENABLED
+ nsCString importStack;
+#endif
+ };
+
+ nsresult ExtractExports(JSContext* aCx, ComponentLoaderInfo& aInfo,
+ ModuleEntry* aMod, JS::MutableHandleObject aExports);
+
+ // Modules are intentionally leaked, but still cleared.
+ nsDataHashtable<nsCStringHashKey, ModuleEntry*> mModules;
+
+ nsClassHashtable<nsCStringHashKey, ModuleEntry> mImports;
+ nsDataHashtable<nsCStringHashKey, ModuleEntry*> mInProgressImports;
+
+ // 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;
+ JS::PersistentRooted<JSObject*> mLoaderGlobal;
+};
+
+#endif
diff --git a/js/xpconnect/loader/mozJSLoaderUtils.cpp b/js/xpconnect/loader/mozJSLoaderUtils.cpp
new file mode 100644
index 0000000000..522d85c304
--- /dev/null
+++ b/js/xpconnect/loader/mozJSLoaderUtils.cpp
@@ -0,0 +1,78 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/scache/StartupCache.h"
+
+#include "jsapi.h"
+#include "jsfriendapi.h"
+
+#include "mozilla/BasePrincipal.h"
+
+using namespace JS;
+using namespace mozilla::scache;
+using mozilla::UniquePtr;
+
+// We only serialize scripts with system principals. So we don't serialize the
+// principals when writing a script. Instead, when reading it back, we set the
+// principals to the system principals.
+nsresult ReadCachedScript(StartupCache* cache, nsACString& uri, JSContext* cx,
+ const JS::ReadOnlyCompileOptions& options,
+ MutableHandleScript scriptp) {
+ const char* buf;
+ uint32_t len;
+ nsresult rv = cache->GetBuffer(PromiseFlatCString(uri).get(), &buf, &len);
+ if (NS_FAILED(rv)) {
+ return rv; // don't warn since NOT_AVAILABLE is an ok error
+ }
+ void* copy = malloc(len);
+ if (!copy) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ memcpy(copy, buf, len);
+ JS::TranscodeBuffer buffer;
+ buffer.replaceRawBuffer(reinterpret_cast<uint8_t*>(copy), len);
+ JS::TranscodeResult code = JS::DecodeScript(cx, options, buffer, scriptp);
+ if (code == JS::TranscodeResult_Ok) {
+ return NS_OK;
+ }
+
+ if ((code & JS::TranscodeResult_Failure) != 0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT((code & JS::TranscodeResult_Throw) != 0);
+ JS_ClearPendingException(cx);
+ return NS_ERROR_OUT_OF_MEMORY;
+}
+
+nsresult WriteCachedScript(StartupCache* cache, nsACString& uri, JSContext* cx,
+ HandleScript script) {
+ MOZ_ASSERT(
+ nsJSPrincipals::get(JS_GetScriptPrincipals(script))->IsSystemPrincipal());
+
+ JS::TranscodeBuffer buffer;
+ JS::TranscodeResult code = JS::EncodeScript(cx, buffer, script);
+ if (code != JS::TranscodeResult_Ok) {
+ if ((code & JS::TranscodeResult_Failure) != 0) {
+ return NS_ERROR_FAILURE;
+ }
+ MOZ_ASSERT((code & JS::TranscodeResult_Throw) != 0);
+ JS_ClearPendingException(cx);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ size_t size = buffer.length();
+ if (size > UINT32_MAX) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Move the vector buffer into a unique pointer buffer.
+ UniquePtr<char[]> buf(
+ reinterpret_cast<char*>(buffer.extractOrCopyRawBuffer()));
+ nsresult rv =
+ cache->PutBuffer(PromiseFlatCString(uri).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..74097c39c6
--- /dev/null
+++ b/js/xpconnect/loader/mozJSLoaderUtils.h
@@ -0,0 +1,29 @@
+/* -*- 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/CompileOptions.h" // JS::ReadOnlyCompileOptions
+
+namespace mozilla {
+namespace scache {
+class StartupCache;
+} // namespace scache
+} // namespace mozilla
+
+nsresult ReadCachedScript(mozilla::scache::StartupCache* cache, nsACString& uri,
+ JSContext* cx,
+ const JS::ReadOnlyCompileOptions& options,
+ JS::MutableHandleScript scriptp);
+
+nsresult WriteCachedScript(mozilla::scache::StartupCache* cache,
+ nsACString& uri, JSContext* cx,
+ JS::HandleScript script);
+
+#endif /* mozJSLoaderUtils_h */
diff --git a/js/xpconnect/loader/mozJSSubScriptLoader.cpp b/js/xpconnect/loader/mozJSSubScriptLoader.cpp
new file mode 100644
index 0000000000..00577f378d
--- /dev/null
+++ b/js/xpconnect/loader/mozJSSubScriptLoader.cpp
@@ -0,0 +1,507 @@
+/* -*- 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 "mozJSComponentLoader.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
+#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/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"
+#include "GeckoProfiler.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(aType) "jssubloader/" aType
+
+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)) {
+ cachePath.AssignLiteral(JSSUB_CACHE_PREFIX("non-syntactic"));
+ } else {
+ cachePath.AssignLiteral(JSSUB_CACHE_PREFIX("global"));
+ }
+ PathifyURI(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 void FillCompileOptions(JS::CompileOptions& options, const char* uriStr,
+ bool wantGlobalScript, bool wantReturnValue) {
+ options.setFileAndLine(uriStr, 1).setNoScriptRval(!wantReturnValue);
+
+ // This presumes that no one else might be compiling a script for this
+ // (URL, syntactic-or-not) key *not* using UTF-8. Seeing as JS source can
+ // only be compiled as UTF-8 or UTF-16 now -- there isn't a JSAPI function to
+ // compile Latin-1 now -- this presumption seems relatively safe.
+ //
+ // This also presumes that lazy parsing is disabled, for the sake of the
+ // startup cache. If lazy parsing is ever enabled for pertinent scripts that
+ // pass through here, we may need to disable lazy source for them.
+ options.setSourceIsLazy(true);
+
+ if (!wantGlobalScript) {
+ options.setNonSyntacticScope(true);
+ }
+}
+
+static JSScript* PrepareScript(nsIURI* uri, JSContext* cx,
+ const JS::ReadOnlyCompileOptions& options,
+ const char* buf, int64_t len) {
+ JS::SourceText<Utf8Unit> srcBuf;
+ if (!srcBuf.init(cx, buf, len, JS::SourceOwnership::Borrowed)) {
+ return nullptr;
+ }
+
+ return JS::Compile(cx, options, srcBuf);
+}
+
+static bool EvalScript(JSContext* cx, HandleObject targetObj,
+ HandleObject loadScope, MutableHandleValue retval,
+ nsIURI* uri, bool startupCache, bool preloadCache,
+ MutableHandleScript script) {
+ MOZ_ASSERT(!js::IsWrapper(targetObj));
+
+ if (JS_IsGlobalObject(targetObj)) {
+ if (!JS::CloneAndExecuteScript(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(
+ !mozJSComponentLoader::Get()->IsLoaderGlobal(targetGlobal),
+ "Don't load subscript into target in a shared-global JSM");
+#endif
+ if (!JS::CloneAndExecuteScript(cx, envChain, script, retval)) {
+ return false;
+ }
+ } else if (JS_IsGlobalObject(loadScope)) {
+ if (!JS::CloneAndExecuteScript(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 && (startupCache || preloadCache)) {
+ nsAutoCString cachePath;
+ SubscriptCachePath(cx, uri, targetObj, cachePath);
+
+ nsCString uriStr;
+ if (preloadCache && NS_SUCCEEDED(uri->GetSpec(uriStr))) {
+ // Note that, when called during startup, this will keep the
+ // original JSScript object alive for an indefinite amount of time.
+ // This has the side-effect of keeping the global that the script
+ // was compiled for alive, too.
+ //
+ // For most startups, the global in question will be the
+ // CompilationScope, since we pre-compile any scripts that were
+ // needed during the last startup in that scope. But for startups
+ // when a non-cached script is used (e.g., after add-on
+ // installation), this may be a Sandbox global, which may be
+ // nuked but held alive by the JSScript. We can avoid this problem
+ // by using a different scope when compiling the script. See
+ // useCompilationScope in ReadScript().
+ //
+ // In general, this isn't a problem, since add-on Sandboxes which
+ // use the script preloader are not destroyed until add-on shutdown,
+ // and when add-ons are uninstalled or upgraded, the preloader cache
+ // is immediately flushed after shutdown. But it's possible to
+ // disable and reenable an add-on without uninstalling it, leading
+ // to cached scripts being held alive, and tied to nuked Sandbox
+ // globals. Given the unusual circumstances required to trigger
+ // this, it's not a major concern. But it should be kept in mind.
+ ScriptPreloader::GetSingleton().NoteScript(uriStr, cachePath, script);
+ }
+
+ if (startupCache) {
+ JSAutoRealm ar(cx, script);
+ WriteCachedScript(StartupCache::GetSingleton(), cachePath, cx, script);
+ }
+ }
+
+ return true;
+}
+
+bool mozJSSubScriptLoader::ReadScript(JS::MutableHandle<JSScript*> script,
+ 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();
+ }
+
+#ifdef DEBUG
+ int64_t currentLength = -1;
+ // if getting content length succeeded above, it should not fail now
+ MOZ_ASSERT(chan->GetContentLength(&currentLength) == NS_OK);
+ // if content length was not known when GetContentLength() was called before,
+ // 'len' would be set to -1 until NS_ReadInputStreamToString() set its correct
+ // value. Every subsequent call to GetContentLength() should return the same
+ // length as that value.
+ MOZ_ASSERT(currentLength == len);
+#endif
+
+ 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());
+ }
+
+ JSScript* ret = PrepareScript(uri, cx, options, buf.get(), len);
+ if (!ret) {
+ return false;
+ }
+
+ script.set(ret);
+ return true;
+}
+
+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);
+ mozJSComponentLoader* loader = mozJSComponentLoader::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);
+ AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING_NONSENSITIVE(
+ "mozJSSubScriptLoader::DoLoadSubScriptWithOptions", OTHER, asciiUrl);
+ AUTO_PROFILER_MARKER_TEXT("SubScript", JS, MarkerStack::Capture(), asciiUrl);
+
+ // 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>()) {
+ auto* content = principal->As<ContentPrincipal>();
+
+ nsAutoCString scheme;
+ content->mURI->GetScheme(scheme);
+
+ // We want to enable caching for scripts with Activity Stream's
+ // codebase URLs.
+ if (scheme.EqualsLiteral("about")) {
+ nsAutoCString filePath;
+ content->mURI->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::CompileOptions compileOptions(cx);
+ FillCompileOptions(compileOptions, uriStr.get(), JS_IsGlobalObject(targetObj),
+ options.wantReturnValue);
+
+ RootedScript script(cx);
+ if (!options.ignoreCache) {
+ if (!options.wantReturnValue) {
+ script = ScriptPreloader::GetSingleton().GetCachedScript(
+ cx, compileOptions, cachePath);
+ }
+ if (!script && cache) {
+ rv = ReadCachedScript(cache, cachePath, cx, compileOptions, &script);
+ }
+ if (NS_FAILED(rv) || !script) {
+ // ReadCachedScript may have set a pending exception.
+ JS_ClearPendingException(cx);
+ }
+ }
+
+ if (script) {
+ // |script| came from the cache, so don't bother writing it
+ // |back there.
+ cache = nullptr;
+ } else {
+ if (!ReadScript(&script, uri, cx, compileOptions, serv,
+ useCompilationScope)) {
+ return NS_OK;
+ }
+ }
+
+ Unused << EvalScript(cx, targetObj, loadScope, retval, uri, !!cache,
+ !ignoreCache && !options.wantReturnValue, &script);
+ return NS_OK;
+}
diff --git a/js/xpconnect/loader/mozJSSubScriptLoader.h b/js/xpconnect/loader/mozJSSubScriptLoader.h
new file mode 100644
index 0000000000..085e089539
--- /dev/null
+++ b/js/xpconnect/loader/mozJSSubScriptLoader.h
@@ -0,0 +1,49 @@
+/* -*- 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/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 ReadScript(JS::MutableHandle<JSScript*> script, 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..372ca6de75
--- /dev/null
+++ b/js/xpconnect/loader/nsImportModule.cpp
@@ -0,0 +1,46 @@
+/* -*- 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/ResultExtensions.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozJSComponentLoader.h"
+#include "xpcpublic.h"
+#include "xpcprivate.h"
+
+using mozilla::dom::AutoJSAPI;
+
+namespace mozilla {
+namespace loader {
+
+nsresult ImportModule(const char* aURI, const char* aExportName,
+ const nsIID& aIID, void** aResult) {
+ AutoJSAPI jsapi;
+ MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
+ JSContext* cx = jsapi.cx();
+
+ JS::RootedObject global(cx);
+ JS::RootedObject exports(cx);
+ MOZ_TRY(mozJSComponentLoader::Get()->Import(cx, nsDependentCString(aURI),
+ &global, &exports));
+
+ 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);
+}
+
+} // namespace loader
+} // namespace mozilla
diff --git a/js/xpconnect/loader/nsImportModule.h b/js/xpconnect/loader/nsImportModule.h
new file mode 100644
index 0000000000..b1105278d3
--- /dev/null
+++ b/js/xpconnect/loader/nsImportModule.h
@@ -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/. */
+
+#ifndef nsImportModule_h
+#define nsImportModule_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);
+
+} // namespace loader
+} // namespace mozilla
+
+class MOZ_STACK_CLASS nsImportModule final : public nsCOMPtr_helper {
+ public:
+ nsImportModule(const char* aURI, const char* aExportName, nsresult* aErrorPtr)
+ : mURI(aURI), mExportName(aExportName), mErrorPtr(aErrorPtr) {}
+
+ virtual nsresult NS_FASTCALL operator()(const nsIID& aIID,
+ void** aResult) const override {
+ nsresult rv =
+ ::mozilla::loader::ImportModule(mURI, mExportName, aIID, aResult);
+ if (mErrorPtr) {
+ *mErrorPtr = rv;
+ }
+ return rv;
+ }
+
+ private:
+ const char* mURI;
+ const char* mExportName;
+ nsresult* mErrorPtr;
+};
+
+/**
+ * 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};
+}
+
+template <size_t N>
+inline nsImportModule do_ImportModule(const char (&aURI)[N], nsresult* aRv) {
+ return {aURI, nullptr, aRv};
+}
+
+template <size_t N, size_t N2>
+inline nsImportModule do_ImportModule(const char (&aURI)[N],
+ const char (&aExportName)[N2]) {
+ return {aURI, aExportName, nullptr};
+}
+
+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};
+}
+
+#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..ecf5d8ae2e
--- /dev/null
+++ b/js/xpconnect/loader/script_cache.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python
+
+from __future__ import absolute_import, print_function
+
+# 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("")