From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- js/loader/ImportMap.cpp | 697 +++++++++++++++++++ js/loader/ImportMap.h | 118 ++++ js/loader/LoadContextBase.cpp | 60 ++ js/loader/LoadContextBase.h | 73 ++ js/loader/LoadedScript.cpp | 289 ++++++++ js/loader/LoadedScript.h | 384 +++++++++++ js/loader/ModuleLoadRequest.cpp | 265 ++++++++ js/loader/ModuleLoadRequest.h | 178 +++++ js/loader/ModuleLoaderBase.cpp | 1431 +++++++++++++++++++++++++++++++++++++++ js/loader/ModuleLoaderBase.h | 503 ++++++++++++++ js/loader/ResolveResult.h | 55 ++ js/loader/ScriptFetchOptions.h | 103 +++ js/loader/ScriptKind.h | 16 + js/loader/ScriptLoadRequest.cpp | 241 +++++++ js/loader/ScriptLoadRequest.h | 323 +++++++++ js/loader/moz.build | 30 + 16 files changed, 4766 insertions(+) create mode 100644 js/loader/ImportMap.cpp create mode 100644 js/loader/ImportMap.h create mode 100644 js/loader/LoadContextBase.cpp create mode 100644 js/loader/LoadContextBase.h create mode 100644 js/loader/LoadedScript.cpp create mode 100644 js/loader/LoadedScript.h create mode 100644 js/loader/ModuleLoadRequest.cpp create mode 100644 js/loader/ModuleLoadRequest.h create mode 100644 js/loader/ModuleLoaderBase.cpp create mode 100644 js/loader/ModuleLoaderBase.h create mode 100644 js/loader/ResolveResult.h create mode 100644 js/loader/ScriptFetchOptions.h create mode 100644 js/loader/ScriptKind.h create mode 100644 js/loader/ScriptLoadRequest.cpp create mode 100644 js/loader/ScriptLoadRequest.h create mode 100644 js/loader/moz.build (limited to 'js/loader') diff --git a/js/loader/ImportMap.cpp b/js/loader/ImportMap.cpp new file mode 100644 index 0000000000..51543511a7 --- /dev/null +++ b/js/loader/ImportMap.cpp @@ -0,0 +1,697 @@ +/* -*- 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 "ImportMap.h" + +#include "js/Array.h" // IsArrayObject +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/JSON.h" // JS_ParseJSON +#include "LoadedScript.h" +#include "ModuleLoaderBase.h" // ScriptLoaderInterface +#include "nsContentUtils.h" +#include "nsIScriptElement.h" +#include "nsIScriptError.h" +#include "nsJSUtils.h" // nsAutoJSString +#include "nsNetUtil.h" // NS_NewURI +#include "ScriptLoadRequest.h" + +using JS::SourceText; +using mozilla::Err; +using mozilla::LazyLogModule; +using mozilla::MakeUnique; +using mozilla::UniquePtr; +using mozilla::WrapNotNull; + +namespace JS::loader { + +LazyLogModule ImportMap::gImportMapLog("ImportMap"); + +#undef LOG +#define LOG(args) \ + MOZ_LOG(ImportMap::gImportMapLog, mozilla::LogLevel::Debug, args) + +#define LOG_ENABLED() \ + MOZ_LOG_TEST(ImportMap::gImportMapLog, mozilla::LogLevel::Debug) + +void ReportWarningHelper::Report(const char* aMessageName, + const nsTArray& aParams) const { + mLoader->ReportWarningToConsole(mRequest, aMessageName, aParams); +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#resolving-a-url-like-module-specifier +static ResolveResult ResolveURLLikeModuleSpecifier(const nsAString& aSpecifier, + nsIURI* aBaseURL) { + nsCOMPtr uri; + nsresult rv; + + // Step 1. If specifier starts with "/", "./", or "../", then: + if (StringBeginsWith(aSpecifier, u"/"_ns) || + StringBeginsWith(aSpecifier, u"./"_ns) || + StringBeginsWith(aSpecifier, u"../"_ns)) { + // Step 1.1. Let url be the result of parsing specifier with baseURL as the + // base URL. + rv = NS_NewURI(getter_AddRefs(uri), aSpecifier, nullptr, aBaseURL); + // Step 1.2. If url is failure, then return null. + if (NS_FAILED(rv)) { + return Err(ResolveError::Failure); + } + + // Step 1.3. Return url. + return WrapNotNull(uri); + } + + // Step 2. Let url be the result of parsing specifier (with no base URL). + rv = NS_NewURI(getter_AddRefs(uri), aSpecifier); + // Step 3. If url is failure, then return null. + if (NS_FAILED(rv)) { + return Err(ResolveError::FailureMayBeBare); + } + + // Step 4. Return url. + return WrapNotNull(uri); +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#normalizing-a-specifier-key +static void NormalizeSpecifierKey(const nsAString& aSpecifierKey, + nsIURI* aBaseURL, + const ReportWarningHelper& aWarning, + nsAString& aRetVal) { + // Step 1. If specifierKey is the empty string, then: + if (aSpecifierKey.IsEmpty()) { + // Step 1.1. Report a warning to the console that specifier keys cannot be + // the empty string. + aWarning.Report("ImportMapEmptySpecifierKeys"); + + // Step 1.2. Return null. + aRetVal = EmptyString(); + return; + } + + // Step 2. Let url be the result of resolving a URL-like module specifier, + // given specifierKey and baseURL. + auto parseResult = ResolveURLLikeModuleSpecifier(aSpecifierKey, aBaseURL); + + // Step 3. If url is not null, then return the serialization of url. + if (parseResult.isOk()) { + nsCOMPtr url = parseResult.unwrap(); + aRetVal = NS_ConvertUTF8toUTF16(url->GetSpecOrDefault()); + return; + } + + // Step 4. Return specifierKey. + aRetVal = aSpecifierKey; +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#sorting-and-normalizing-a-module-specifier-map +static UniquePtr SortAndNormalizeSpecifierMap( + JSContext* aCx, JS::HandleObject aOriginalMap, nsIURI* aBaseURL, + const ReportWarningHelper& aWarning) { + // Step 1. Let normalized be an empty ordered map. + UniquePtr normalized = MakeUnique(); + + JS::Rooted specifierKeys(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, aOriginalMap, &specifierKeys)) { + return nullptr; + } + + // Step 2. For each specifierKey → value of originalMap, + for (size_t i = 0; i < specifierKeys.length(); i++) { + const JS::RootedId specifierId(aCx, specifierKeys[i]); + nsAutoJSString specifierKey; + NS_ENSURE_TRUE(specifierKey.init(aCx, specifierId), nullptr); + + // Step 2.1. Let normalizedSpecifierKey be the result of normalizing a + // specifier key given specifierKey and baseURL. + nsString normalizedSpecifierKey; + NormalizeSpecifierKey(specifierKey, aBaseURL, aWarning, + normalizedSpecifierKey); + + // Step 2.2. If normalizedSpecifierKey is null, then continue. + if (normalizedSpecifierKey.IsEmpty()) { + continue; + } + + JS::RootedValue idVal(aCx); + NS_ENSURE_TRUE(JS_GetPropertyById(aCx, aOriginalMap, specifierId, &idVal), + nullptr); + // Step 2.3. If value is not a string, then: + if (!idVal.isString()) { + // Step 2.3.1. The user agent may report a warning to the console + // indicating that addresses need to be strings. + aWarning.Report("ImportMapAddressesNotStrings"); + + // Step 2.3.2. Set normalized[normalizedSpecifierKey] to null. + normalized->insert_or_assign(normalizedSpecifierKey, nullptr); + + // Step 2.3.3. Continue. + continue; + } + + nsAutoJSString value; + NS_ENSURE_TRUE(value.init(aCx, idVal), nullptr); + + // Step 2.4. Let addressURL be the result of resolving a URL-like module + // specifier given value and baseURL. + auto parseResult = ResolveURLLikeModuleSpecifier(value, aBaseURL); + + // Step 2.5. If addressURL is null, then: + if (parseResult.isErr()) { + // Step 2.5.1. The user agent may report a warning to the console + // indicating that the address was invalid. + AutoTArray params; + params.AppendElement(value); + aWarning.Report("ImportMapInvalidAddress", params); + + // Step 2.5.2. Set normalized[normalizedSpecifierKey] to null. + normalized->insert_or_assign(normalizedSpecifierKey, nullptr); + + // Step 2.5.3. Continue. + continue; + } + + nsCOMPtr addressURL = parseResult.unwrap(); + nsCString address = addressURL->GetSpecOrDefault(); + // Step 2.6. If specifierKey ends with U+002F (/), and the serialization + // of addressURL does not end with U+002F (/), then: + if (StringEndsWith(specifierKey, u"/"_ns) && + !StringEndsWith(address, "/"_ns)) { + // Step 2.6.1. The user agent may report a warning to the console + // indicating that an invalid address was given for the specifier key + // specifierKey; since specifierKey ends with a slash, the address needs + // to as well. + AutoTArray params; + params.AppendElement(specifierKey); + params.AppendElement(NS_ConvertUTF8toUTF16(address)); + aWarning.Report("ImportMapAddressNotEndsWithSlash", params); + + // Step 2.6.2. Set normalized[normalizedSpecifierKey] to null. + normalized->insert_or_assign(normalizedSpecifierKey, nullptr); + + // Step 2.6.3. Continue. + continue; + } + + LOG(("ImportMap::SortAndNormalizeSpecifierMap {%s, %s}", + NS_ConvertUTF16toUTF8(normalizedSpecifierKey).get(), + addressURL->GetSpecOrDefault().get())); + + // Step 2.7. Set normalized[normalizedSpecifierKey] to addressURL. + normalized->insert_or_assign(normalizedSpecifierKey, addressURL); + } + + // Step 3: Return the result of sorting normalized, with an entry a being + // less than an entry b if b’s key is code unit less than a’s key. + // + // Impl note: The sorting is done when inserting the entry. + return normalized; +} + +// Check if it's a map defined in +// https://infra.spec.whatwg.org/#ordered-map +// +// If it is, *aIsMap will be set to true. +static bool IsMapObject(JSContext* aCx, JS::HandleValue aMapVal, bool* aIsMap) { + MOZ_ASSERT(aIsMap); + + *aIsMap = false; + if (!aMapVal.isObject()) { + return true; + } + + bool isArray; + if (!IsArrayObject(aCx, aMapVal, &isArray)) { + return false; + } + + *aIsMap = !isArray; + return true; +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#sorting-and-normalizing-scopes +static UniquePtr SortAndNormalizeScopes( + JSContext* aCx, JS::HandleObject aOriginalMap, nsIURI* aBaseURL, + const ReportWarningHelper& aWarning) { + JS::Rooted scopeKeys(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, aOriginalMap, &scopeKeys)) { + return nullptr; + } + + // Step 1. Let normalized be an empty map. + UniquePtr normalized = MakeUnique(); + + // Step 2. For each scopePrefix → potentialSpecifierMap of originalMap, + for (size_t i = 0; i < scopeKeys.length(); i++) { + const JS::RootedId scopeKey(aCx, scopeKeys[i]); + nsAutoJSString scopePrefix; + NS_ENSURE_TRUE(scopePrefix.init(aCx, scopeKey), nullptr); + + // Step 2.1. If potentialSpecifierMap is not an ordered map, then throw a + // TypeError indicating that the value of the scope with prefix scopePrefix + // needs to be a JSON object. + JS::RootedValue mapVal(aCx); + NS_ENSURE_TRUE(JS_GetPropertyById(aCx, aOriginalMap, scopeKey, &mapVal), + nullptr); + + bool isMap; + if (!IsMapObject(aCx, mapVal, &isMap)) { + return nullptr; + } + if (!isMap) { + const char16_t* scope = scopePrefix.get(); + JS_ReportErrorNumberUC(aCx, js::GetErrorMessage, nullptr, + JSMSG_IMPORT_MAPS_SCOPE_VALUE_NOT_A_MAP, scope); + return nullptr; + } + + // Step 2.2. Let scopePrefixURL be the result of URL parsing scopePrefix + // with baseURL. + nsCOMPtr scopePrefixURL; + nsresult rv = NS_NewURI(getter_AddRefs(scopePrefixURL), scopePrefix, + nullptr, aBaseURL); + + // Step 2.3. If scopePrefixURL is failure, then: + if (NS_FAILED(rv)) { + // Step 2.3.1. The user agent may report a warning to the console that + // the scope prefix URL was not parseable. + AutoTArray params; + params.AppendElement(scopePrefix); + aWarning.Report("ImportMapScopePrefixNotParseable", params); + + // Step 2.3.2. Continue. + continue; + } + + // Step 2.4. Let normalizedScopePrefix be the serialization of + // scopePrefixURL. + nsCString normalizedScopePrefix = scopePrefixURL->GetSpecOrDefault(); + + // Step 2.5. Set normalized[normalizedScopePrefix] to the result of sorting + // and normalizing a specifier map given potentialSpecifierMap and baseURL. + JS::RootedObject potentialSpecifierMap(aCx, &mapVal.toObject()); + UniquePtr specifierMap = SortAndNormalizeSpecifierMap( + aCx, potentialSpecifierMap, aBaseURL, aWarning); + if (!specifierMap) { + return nullptr; + } + + normalized->insert_or_assign(normalizedScopePrefix, + std::move(specifierMap)); + } + + // Step 3. Return the result of sorting in descending order normalized, with + // an entry a being less than an entry b if a's key is code unit less than b's + // key. + // + // Impl note: The sorting is done when inserting the entry. + return normalized; +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#parse-an-import-map-string +// static +UniquePtr ImportMap::ParseString( + JSContext* aCx, SourceText& aInput, nsIURI* aBaseURL, + const ReportWarningHelper& aWarning) { + // Step 1. Let parsed be the result of parsing JSON into Infra values given + // input. + JS::Rooted parsedVal(aCx); + if (!JS_ParseJSON(aCx, aInput.get(), aInput.length(), &parsedVal)) { + NS_WARNING("Parsing Import map string failed"); + + // If JS_ParseJSON fails we check if it throws a SyntaxError. + // If so we update the error message from JSON parser to make it more clear + // that the parsing of import map has failed. + MOZ_ASSERT(JS_IsExceptionPending(aCx)); + JS::Rooted exn(aCx); + if (!JS_GetPendingException(aCx, &exn)) { + return nullptr; + } + MOZ_ASSERT(exn.isObject()); + JS::Rooted obj(aCx, &exn.toObject()); + JSErrorReport* err = JS_ErrorFromException(aCx, obj); + if (err->exnType == JSEXN_SYNTAXERR) { + JS_ClearPendingException(aCx); + JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr, + JSMSG_IMPORT_MAPS_PARSE_FAILED, + err->message().c_str()); + } + + return nullptr; + } + + // Step 2. If parsed is not an ordered map, then throw a TypeError indicating + // that the top-level value needs to be a JSON object. + bool isMap; + if (!IsMapObject(aCx, parsedVal, &isMap)) { + return nullptr; + } + if (!isMap) { + JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr, + JSMSG_IMPORT_MAPS_NOT_A_MAP); + return nullptr; + } + + JS::RootedObject parsedObj(aCx, &parsedVal.toObject()); + JS::RootedValue importsVal(aCx); + if (!JS_GetProperty(aCx, parsedObj, "imports", &importsVal)) { + return nullptr; + } + + // Step 3. Let sortedAndNormalizedImports be an empty ordered map. + // + // Impl note: If parsed["imports"] doesn't exist, we will allocate + // sortedAndNormalizedImports to an empty map in Step 8 below. + UniquePtr sortedAndNormalizedImports = nullptr; + + // Step 4. If parsed["imports"] exists, then: + if (!importsVal.isUndefined()) { + // Step 4.1. If parsed["imports"] is not an ordered map, then throw a + // TypeError indicating that the "imports" top-level key needs to be a JSON + // object. + bool isMap; + if (!IsMapObject(aCx, importsVal, &isMap)) { + return nullptr; + } + if (!isMap) { + JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr, + JSMSG_IMPORT_MAPS_IMPORTS_NOT_A_MAP); + return nullptr; + } + + // Step 4.2. Set sortedAndNormalizedImports to the result of sorting and + // normalizing a module specifier map given parsed["imports"] and baseURL. + JS::RootedObject importsObj(aCx, &importsVal.toObject()); + sortedAndNormalizedImports = + SortAndNormalizeSpecifierMap(aCx, importsObj, aBaseURL, aWarning); + if (!sortedAndNormalizedImports) { + return nullptr; + } + } + + JS::RootedValue scopesVal(aCx); + if (!JS_GetProperty(aCx, parsedObj, "scopes", &scopesVal)) { + return nullptr; + } + + // Step 5. Let sortedAndNormalizedScopes be an empty ordered map. + // + // Impl note: If parsed["scopes"] doesn't exist, we will allocate + // sortedAndNormalizedScopes to an empty map in Step 8 below. + UniquePtr sortedAndNormalizedScopes = nullptr; + + // Step 6. If parsed["scopes"] exists, then: + if (!scopesVal.isUndefined()) { + // Step 6.1. If parsed["scopes"] is not an ordered map, then throw a + // TypeError indicating that the "scopes" top-level key needs to be a JSON + // object. + bool isMap; + if (!IsMapObject(aCx, scopesVal, &isMap)) { + return nullptr; + } + if (!isMap) { + JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr, + JSMSG_IMPORT_MAPS_SCOPES_NOT_A_MAP); + return nullptr; + } + + // Step 6.2. Set sortedAndNormalizedScopes to the result of sorting and + // normalizing scopes given parsed["scopes"] and baseURL. + JS::RootedObject scopesObj(aCx, &scopesVal.toObject()); + sortedAndNormalizedScopes = + SortAndNormalizeScopes(aCx, scopesObj, aBaseURL, aWarning); + if (!sortedAndNormalizedScopes) { + return nullptr; + } + } + + // Step 7. If parsed’s keys contains any items besides "imports" or + // "scopes", then the user agent should report a warning to the console + // indicating that an invalid top-level key was present in the import map. + JS::Rooted keys(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, parsedObj, &keys)) { + return nullptr; + } + + for (size_t i = 0; i < keys.length(); i++) { + const JS::RootedId key(aCx, keys[i]); + nsAutoJSString val; + NS_ENSURE_TRUE(val.init(aCx, key), nullptr); + if (val.EqualsLiteral("imports") || val.EqualsLiteral("scopes")) { + continue; + } + + AutoTArray params; + params.AppendElement(val); + aWarning.Report("ImportMapInvalidTopLevelKey", params); + } + + // Impl note: Create empty maps for sortedAndNormalizedImports and + // sortedAndNormalizedImports if they aren't allocated. + if (!sortedAndNormalizedImports) { + sortedAndNormalizedImports = MakeUnique(); + } + if (!sortedAndNormalizedScopes) { + sortedAndNormalizedScopes = MakeUnique(); + } + + // Step 8. Return an import map whose imports are + // sortedAndNormalizedImports and whose scopes scopes are + // sortedAndNormalizedScopes. + return MakeUnique(std::move(sortedAndNormalizedImports), + std::move(sortedAndNormalizedScopes)); +} + +// https://url.spec.whatwg.org/#is-special +static bool IsSpecialScheme(nsIURI* aURI) { + nsAutoCString scheme; + aURI->GetScheme(scheme); + return scheme.EqualsLiteral("ftp") || scheme.EqualsLiteral("file") || + scheme.EqualsLiteral("http") || scheme.EqualsLiteral("https") || + scheme.EqualsLiteral("ws") || scheme.EqualsLiteral("wss"); +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#resolving-an-imports-match +static mozilla::Result, ResolveError> ResolveImportsMatch( + nsString& aNormalizedSpecifier, nsIURI* aAsURL, + const SpecifierMap* aSpecifierMap) { + // Step 1. For each specifierKey → resolutionResult of specifierMap, + for (auto&& [specifierKey, resolutionResult] : *aSpecifierMap) { + nsAutoString specifier{aNormalizedSpecifier}; + nsCString asURL = aAsURL ? aAsURL->GetSpecOrDefault() : EmptyCString(); + + // Step 1.1. If specifierKey is normalizedSpecifier, then: + if (specifierKey.Equals(aNormalizedSpecifier)) { + // Step 1.1.1. If resolutionResult is null, then throw a TypeError + // indicating that resolution of specifierKey was blocked by a null entry. + // This will terminate the entire resolve a module specifier algorithm, + // without any further fallbacks. + if (!resolutionResult) { + LOG( + ("ImportMap::ResolveImportsMatch normalizedSpecifier: %s, " + "specifierKey: %s, but resolution is null.", + NS_ConvertUTF16toUTF8(aNormalizedSpecifier).get(), + NS_ConvertUTF16toUTF8(specifierKey).get())); + return Err(ResolveError::BlockedByNullEntry); + } + + // Step 1.1.2. Assert: resolutionResult is a URL. + MOZ_ASSERT(resolutionResult); + + // Step 1.1.3. Return resolutionResult. + return resolutionResult; + } + + // Step 1.2. If all of the following are true: + // specifierKey ends with U+002F (/), + // specifierKey is a code unit prefix of normalizedSpecifier, and + // either asURL is null, or asURL is special + if (StringEndsWith(specifierKey, u"/"_ns) && + StringBeginsWith(aNormalizedSpecifier, specifierKey) && + (!aAsURL || IsSpecialScheme(aAsURL))) { + // Step 1.2.1. If resolutionResult is null, then throw a TypeError + // indicating that resolution of specifierKey was blocked by a null entry. + // This will terminate the entire resolve a module specifier algorithm, + // without any further fallbacks. + if (!resolutionResult) { + LOG( + ("ImportMap::ResolveImportsMatch normalizedSpecifier: %s, " + "specifierKey: %s, but resolution is null.", + NS_ConvertUTF16toUTF8(aNormalizedSpecifier).get(), + NS_ConvertUTF16toUTF8(specifierKey).get())); + return Err(ResolveError::BlockedByNullEntry); + } + + // Step 1.2.2. Assert: resolutionResult is a URL. + MOZ_ASSERT(resolutionResult); + + // Step 1.2.3. Let afterPrefix be the portion of normalizedSpecifier after + // the initial specifierKey prefix. + nsAutoString afterPrefix( + Substring(aNormalizedSpecifier, specifierKey.Length())); + + // Step 1.2.4. Assert: resolutionResult, serialized, ends with U+002F (/), + // as enforced during parsing + MOZ_ASSERT(StringEndsWith(resolutionResult->GetSpecOrDefault(), "/"_ns)); + + // Step 1.2.5. Let url be the result of URL parsing afterPrefix with + // resolutionResult. + nsCOMPtr url; + nsresult rv = NS_NewURI(getter_AddRefs(url), afterPrefix, nullptr, + resolutionResult); + + // Step 1.2.6. If url is failure, then throw a TypeError indicating that + // resolution of normalizedSpecifier was blocked since the afterPrefix + // portion could not be URL-parsed relative to the resolutionResult mapped + // to by the specifierKey prefix. + // + // This will terminate the entire resolve a module specifier algorithm, + // without any further fallbacks. + if (NS_FAILED(rv)) { + LOG( + ("ImportMap::ResolveImportsMatch normalizedSpecifier: %s, " + "specifierKey: %s, resolutionResult: %s, afterPrefix: %s, " + "but URL is not parsable.", + NS_ConvertUTF16toUTF8(aNormalizedSpecifier).get(), + NS_ConvertUTF16toUTF8(specifierKey).get(), + resolutionResult->GetSpecOrDefault().get(), + NS_ConvertUTF16toUTF8(afterPrefix).get())); + return Err(ResolveError::BlockedByAfterPrefix); + } + + // Step 1.2.7. Assert: url is a URL. + MOZ_ASSERT(url); + + // Step 1.2.8. If the serialization of resolutionResult is not a code unit + // prefix of the serialization of url, then throw a TypeError indicating + // that resolution of normalizedSpecifier was blocked due to it + // backtracking above its prefix specifierKey. + // + // This will terminate the entire resolve a module specifier algorithm, + // without any further fallbacks. + if (!StringBeginsWith(url->GetSpecOrDefault(), + resolutionResult->GetSpecOrDefault())) { + LOG( + ("ImportMap::ResolveImportsMatch normalizedSpecifier: %s, " + "specifierKey: %s, " + "url %s does not start with resolutionResult %s.", + NS_ConvertUTF16toUTF8(aNormalizedSpecifier).get(), + NS_ConvertUTF16toUTF8(specifierKey).get(), + url->GetSpecOrDefault().get(), + resolutionResult->GetSpecOrDefault().get())); + return Err(ResolveError::BlockedByBacktrackingPrefix); + } + + // Step 1.2.9. Return url. + return std::move(url); + } + } + + // Step 2. Return null. + return nsCOMPtr(nullptr); +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier +// static +ResolveResult ImportMap::ResolveModuleSpecifier(ImportMap* aImportMap, + ScriptLoaderInterface* aLoader, + LoadedScript* aScript, + const nsAString& aSpecifier) { + LOG(("ImportMap::ResolveModuleSpecifier specifier: %s", + NS_ConvertUTF16toUTF8(aSpecifier).get())); + nsCOMPtr baseURL; + if (aScript && !aScript->IsEventScript()) { + baseURL = aScript->BaseURL(); + } else { + baseURL = aLoader->GetBaseURI(); + } + + // Step 7. Let asURL be the result of resolving a URL-like module specifier + // given specifier and baseURL. + // + // Impl note: Step 6 is done below if aImportMap exists. + auto parseResult = ResolveURLLikeModuleSpecifier(aSpecifier, baseURL); + nsCOMPtr asURL; + if (parseResult.isOk()) { + asURL = parseResult.unwrap(); + } + + if (aImportMap) { + // Step 6. Let baseURLString be baseURL, serialized. + nsCString baseURLString = baseURL->GetSpecOrDefault(); + + // Step 8. Let normalizedSpecifier be the serialization of asURL, if asURL + // is non-null; otherwise, specifier. + nsAutoString normalizedSpecifier = + asURL ? NS_ConvertUTF8toUTF16(asURL->GetSpecOrDefault()) + : nsAutoString{aSpecifier}; + + // Step 9. For each scopePrefix → scopeImports of importMap’s scopes, + for (auto&& [scopePrefix, scopeImports] : *aImportMap->mScopes) { + // Step 9.1. If scopePrefix is baseURLString, or if scopePrefix ends with + // U+002F (/) and scopePrefix is a code unit prefix of baseURLString, + // then: + if (scopePrefix.Equals(baseURLString) || + (StringEndsWith(scopePrefix, "/"_ns) && + StringBeginsWith(baseURLString, scopePrefix))) { + // Step 9.1.1. Let scopeImportsMatch be the result of resolving an + // imports match given normalizedSpecifier, asURL, and scopeImports. + auto result = + ResolveImportsMatch(normalizedSpecifier, asURL, scopeImports.get()); + if (result.isErr()) { + return result.propagateErr(); + } + + nsCOMPtr scopeImportsMatch = result.unwrap(); + // Step 9.1.2. If scopeImportsMatch is not null, then return + // scopeImportsMatch. + if (scopeImportsMatch) { + LOG(( + "ImportMap::ResolveModuleSpecifier returns scopeImportsMatch: %s", + scopeImportsMatch->GetSpecOrDefault().get())); + return WrapNotNull(scopeImportsMatch); + } + } + } + + // Step 10. Let topLevelImportsMatch be the result of resolving an imports + // match given normalizedSpecifier, asURL, and importMap’s imports. + auto result = ResolveImportsMatch(normalizedSpecifier, asURL, + aImportMap->mImports.get()); + if (result.isErr()) { + return result.propagateErr(); + } + nsCOMPtr topLevelImportsMatch = result.unwrap(); + + // Step 11. If topLevelImportsMatch is not null, then return + // topLevelImportsMatch. + if (topLevelImportsMatch) { + LOG(("ImportMap::ResolveModuleSpecifier returns topLevelImportsMatch: %s", + topLevelImportsMatch->GetSpecOrDefault().get())); + return WrapNotNull(topLevelImportsMatch); + } + } + + // Step 12. At this point, the specifier was able to be turned in to a URL, + // but it wasn’t remapped to anything by importMap. If asURL is not null, then + // return asURL. + if (asURL) { + LOG(("ImportMap::ResolveModuleSpecifier returns asURL: %s", + asURL->GetSpecOrDefault().get())); + return WrapNotNull(asURL); + } + + // Step 13. Throw a TypeError indicating that specifier was a bare specifier, + // but was not remapped to anything by importMap. + if (parseResult.unwrapErr() != ResolveError::FailureMayBeBare) { + // We may have failed to parse a non-bare specifier for another reason. + return Err(ResolveError::Failure); + } + + return Err(ResolveError::InvalidBareSpecifier); +} + +#undef LOG +#undef LOG_ENABLED +} // namespace JS::loader diff --git a/js/loader/ImportMap.h b/js/loader/ImportMap.h new file mode 100644 index 0000000000..d304a52856 --- /dev/null +++ b/js/loader/ImportMap.h @@ -0,0 +1,118 @@ +/* -*- 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 js_loader_ImportMap_h +#define js_loader_ImportMap_h + +#include +#include + +#include "js/SourceText.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Logging.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" +#include "nsStringFwd.h" +#include "nsTArray.h" +#include "ResolveResult.h" + +struct JSContext; +class nsIScriptElement; +class nsIURI; + +namespace JS::loader { +class LoadedScript; +class ScriptLoaderInterface; +class ScriptLoadRequest; + +/** + * A helper class to report warning to ScriptLoaderInterface. + */ +class ReportWarningHelper { + public: + ReportWarningHelper(ScriptLoaderInterface* aLoader, + ScriptLoadRequest* aRequest) + : mLoader(aLoader), mRequest(aRequest) {} + + void Report(const char* aMessageName, + const nsTArray& aParams = nsTArray()) const; + + private: + RefPtr mLoader; + ScriptLoadRequest* mRequest; +}; + +// Specifier map from import maps. +// https://html.spec.whatwg.org/multipage/webappapis.html#module-specifier-map +using SpecifierMap = + std::map, std::greater>; + +// Scope map from import maps. +// https://html.spec.whatwg.org/multipage/webappapis.html#concept-import-map-scopes +using ScopeMap = std::map, + std::greater>; + +/** + * Implementation of Import maps. + * https://html.spec.whatwg.org/multipage/webappapis.html#import-maps + */ +class ImportMap { + public: + ImportMap(mozilla::UniquePtr aImports, + mozilla::UniquePtr aScopes) + : mImports(std::move(aImports)), mScopes(std::move(aScopes)) {} + + /** + * Parse the JSON string from the Import map script. + * This function will throw a TypeError if there's any invalid key or value in + * the JSON text according to the spec. + * + * https://html.spec.whatwg.org/multipage/webappapis.html#parse-an-import-map-string + */ + static mozilla::UniquePtr ParseString( + JSContext* aCx, JS::SourceText& aInput, nsIURI* aBaseURL, + const ReportWarningHelper& aWarning); + + /** + * This implements "Resolve a module specifier" algorithm defined in the + * Import maps spec. + * + * See + * https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier + * + * Impl note: According to the spec, if the specifier cannot be resolved, this + * method will throw a TypeError(Step 13). But the tricky part is when + * creating a module script, + * see + * https://html.spec.whatwg.org/multipage/webappapis.html#validate-requested-module-specifiers + * If the resolving failed, it shall catch the exception and set to the + * script's parse error. + * For implementation we return a ResolveResult here, and the callers will + * need to convert the result to a TypeError if it fails. + */ + static ResolveResult ResolveModuleSpecifier(ImportMap* aImportMap, + ScriptLoaderInterface* aLoader, + LoadedScript* aScript, + const nsAString& aSpecifier); + + // Logging + static mozilla::LazyLogModule gImportMapLog; + + private: + /** + * https://html.spec.whatwg.org/multipage/webappapis.html#import-map-processing-model + * + * Formally, an import map is a struct with two items: + * 1. imports, a module specifier map, and + * 2. scopes, an ordered map of URLs to module specifier maps. + */ + mozilla::UniquePtr mImports; + mozilla::UniquePtr mScopes; +}; + +} // namespace JS::loader + +#endif // js_loader_ImportMap_h diff --git a/js/loader/LoadContextBase.cpp b/js/loader/LoadContextBase.cpp new file mode 100644 index 0000000000..d0d79e8554 --- /dev/null +++ b/js/loader/LoadContextBase.cpp @@ -0,0 +1,60 @@ +/* -*- 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/dom/ScriptLoadContext.h" +#include "mozilla/loader/SyncModuleLoader.h" +#include "mozilla/dom/WorkerLoadContext.h" +#include "mozilla/dom/worklet/WorkletModuleLoader.h" // WorkletLoadContext +#include "js/loader/LoadContextBase.h" +#include "js/loader/ScriptLoadRequest.h" + +namespace JS::loader { + +//////////////////////////////////////////////////////////////// +// LoadContextBase +//////////////////////////////////////////////////////////////// + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LoadContextBase) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(LoadContextBase) +NS_IMPL_CYCLE_COLLECTING_RELEASE(LoadContextBase) + +NS_IMPL_CYCLE_COLLECTION(LoadContextBase, mRequest) + +LoadContextBase::LoadContextBase(ContextKind kind) : mKind(kind) {} + +void LoadContextBase::SetRequest(ScriptLoadRequest* aRequest) { + MOZ_ASSERT(!mRequest); + mRequest = aRequest; +} + +void LoadContextBase::GetProfilerLabel(nsACString& aOutString) { + aOutString.Append("Unknown Script Element"); +} + +mozilla::dom::ScriptLoadContext* LoadContextBase::AsWindowContext() { + MOZ_ASSERT(IsWindowContext()); + return static_cast(this); +} + +mozilla::loader::SyncLoadContext* LoadContextBase::AsSyncContext() { + MOZ_ASSERT(IsSyncContext()); + return static_cast(this); +} + +mozilla::dom::WorkerLoadContext* LoadContextBase::AsWorkerContext() { + MOZ_ASSERT(IsWorkerContext()); + return static_cast(this); +} + +mozilla::dom::WorkletLoadContext* LoadContextBase::AsWorkletContext() { + MOZ_ASSERT(IsWorkletContext()); + return static_cast(this); +} + +} // namespace JS::loader diff --git a/js/loader/LoadContextBase.h b/js/loader/LoadContextBase.h new file mode 100644 index 0000000000..a8ce045546 --- /dev/null +++ b/js/loader/LoadContextBase.h @@ -0,0 +1,73 @@ +/* -*- 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 js_loader_BaseLoadContext_h +#define js_loader_BaseLoadContext_h + +#include "js/loader/ScriptLoadRequest.h" +#include "nsIStringBundle.h" + +namespace mozilla::dom { +class ScriptLoadContext; +class WorkerLoadContext; +class WorkletLoadContext; +} // namespace mozilla::dom + +namespace mozilla::loader { +class SyncLoadContext; +} + +namespace JS::loader { + +class ScriptLoadRequest; + +/* + * LoadContextBase + * + * LoadContexts augment the loading of a ScriptLoadRequest. This class + * is used as a base for all LoadContexts, and provides shared functionality. + * + */ + +enum class ContextKind { Window, Sync, Worker, Worklet }; + +class LoadContextBase : public nsISupports { + private: + ContextKind mKind; + + protected: + virtual ~LoadContextBase() = default; + + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(LoadContextBase) + + explicit LoadContextBase(ContextKind kind); + + void SetRequest(JS::loader::ScriptLoadRequest* aRequest); + + // Used to output a string for the Gecko Profiler. + virtual void GetProfilerLabel(nsACString& aOutString); + + // Casting to the different contexts + bool IsWindowContext() const { return mKind == ContextKind::Window; } + mozilla::dom::ScriptLoadContext* AsWindowContext(); + + bool IsSyncContext() const { return mKind == ContextKind::Sync; } + mozilla::loader::SyncLoadContext* AsSyncContext(); + + bool IsWorkerContext() const { return mKind == ContextKind::Worker; } + mozilla::dom::WorkerLoadContext* AsWorkerContext(); + + bool IsWorkletContext() const { return mKind == ContextKind::Worklet; } + mozilla::dom::WorkletLoadContext* AsWorkletContext(); + + RefPtr mRequest; +}; + +} // namespace JS::loader + +#endif // js_loader_BaseLoadContext_h diff --git a/js/loader/LoadedScript.cpp b/js/loader/LoadedScript.cpp new file mode 100644 index 0000000000..7ddd04168e --- /dev/null +++ b/js/loader/LoadedScript.cpp @@ -0,0 +1,289 @@ +/* -*- 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 "LoadedScript.h" + +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/UniquePtr.h" // mozilla::UniquePtr + +#include "mozilla/dom/ScriptLoadContext.h" // ScriptLoadContext +#include "jsfriendapi.h" +#include "js/Modules.h" // JS::{Get,Set}ModulePrivate +#include "LoadContextBase.h" // LoadContextBase + +namespace JS::loader { + +////////////////////////////////////////////////////////////// +// LoadedScript +////////////////////////////////////////////////////////////// + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LoadedScript) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_CLASS(LoadedScript) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(LoadedScript) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFetchOptions) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mBaseURL) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(LoadedScript) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFetchOptions) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(LoadedScript) +NS_IMPL_CYCLE_COLLECTING_RELEASE(LoadedScript) + +LoadedScript::LoadedScript(ScriptKind aKind, + mozilla::dom::ReferrerPolicy aReferrerPolicy, + ScriptFetchOptions* aFetchOptions, nsIURI* aURI) + : mKind(aKind), + mReferrerPolicy(aReferrerPolicy), + mFetchOptions(aFetchOptions), + mURI(aURI), + mDataType(DataType::eUnknown), + mReceivedScriptTextLength(0), + mBytecodeOffset(0) { + MOZ_ASSERT(mFetchOptions); + MOZ_ASSERT(mURI); +} + +LoadedScript::~LoadedScript() { mozilla::DropJSObjects(this); } + +void LoadedScript::AssociateWithScript(JSScript* aScript) { + // Verify that the rewritten URL is available when manipulating LoadedScript. + MOZ_ASSERT(mBaseURL); + + // Set a JSScript's private value to point to this object. The JS engine will + // increment our reference count by calling HostAddRefTopLevelScript(). This + // is decremented by HostReleaseTopLevelScript() below when the JSScript dies. + + MOZ_ASSERT(JS::GetScriptPrivate(aScript).isUndefined()); + JS::SetScriptPrivate(aScript, JS::PrivateValue(this)); +} + +nsresult LoadedScript::GetScriptSource(JSContext* aCx, + MaybeSourceText* aMaybeSource, + LoadContextBase* aMaybeLoadContext) { + // If there's no script text, we try to get it from the element + bool isWindowContext = + aMaybeLoadContext && aMaybeLoadContext->IsWindowContext(); + if (isWindowContext && aMaybeLoadContext->AsWindowContext()->mIsInline) { + nsAutoString inlineData; + auto* scriptLoadContext = aMaybeLoadContext->AsWindowContext(); + scriptLoadContext->GetScriptElement()->GetScriptText(inlineData); + + size_t nbytes = inlineData.Length() * sizeof(char16_t); + JS::UniqueTwoByteChars chars( + static_cast(JS_malloc(aCx, nbytes))); + if (!chars) { + return NS_ERROR_OUT_OF_MEMORY; + } + + memcpy(chars.get(), inlineData.get(), nbytes); + + SourceText srcBuf; + if (!srcBuf.init(aCx, std::move(chars), inlineData.Length())) { + return NS_ERROR_OUT_OF_MEMORY; + } + + aMaybeSource->construct>(std::move(srcBuf)); + return NS_OK; + } + + size_t length = ScriptTextLength(); + if (IsUTF16Text()) { + JS::UniqueTwoByteChars chars; + chars.reset(ScriptText().extractOrCopyRawBuffer()); + if (!chars) { + JS_ReportOutOfMemory(aCx); + return NS_ERROR_OUT_OF_MEMORY; + } + + SourceText srcBuf; + if (!srcBuf.init(aCx, std::move(chars), length)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + aMaybeSource->construct>(std::move(srcBuf)); + return NS_OK; + } + + MOZ_ASSERT(IsUTF8Text()); + mozilla::UniquePtr chars; + chars.reset(ScriptText().extractOrCopyRawBuffer()); + if (!chars) { + JS_ReportOutOfMemory(aCx); + return NS_ERROR_OUT_OF_MEMORY; + } + + SourceText srcBuf; + if (!srcBuf.init(aCx, std::move(chars), length)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + aMaybeSource->construct>(std::move(srcBuf)); + return NS_OK; +} + +inline void CheckModuleScriptPrivate(LoadedScript* script, + const JS::Value& aPrivate) { +#ifdef DEBUG + if (script->IsModuleScript()) { + JSObject* module = script->AsModuleScript()->mModuleRecord.unbarrieredGet(); + MOZ_ASSERT_IF(module, JS::GetModulePrivate(module) == aPrivate); + } +#endif +} + +void HostAddRefTopLevelScript(const JS::Value& aPrivate) { + // Increment the reference count of a LoadedScript object that is now pointed + // to by a JSScript. The reference count is decremented by + // HostReleaseTopLevelScript() below. + + auto script = static_cast(aPrivate.toPrivate()); + CheckModuleScriptPrivate(script, aPrivate); + script->AddRef(); +} + +void HostReleaseTopLevelScript(const JS::Value& aPrivate) { + // Decrement the reference count of a LoadedScript object that was pointed to + // by a JSScript. The reference count was originally incremented by + // HostAddRefTopLevelScript() above. + + auto script = static_cast(aPrivate.toPrivate()); + CheckModuleScriptPrivate(script, aPrivate); + script->Release(); +} + +////////////////////////////////////////////////////////////// +// EventScript +////////////////////////////////////////////////////////////// + +EventScript::EventScript(mozilla::dom::ReferrerPolicy aReferrerPolicy, + ScriptFetchOptions* aFetchOptions, nsIURI* aURI) + : LoadedScript(ScriptKind::eEvent, aReferrerPolicy, aFetchOptions, aURI) { + // EventScripts are not using ScriptLoadRequest, and mBaseURL and mURI are + // the same thing. + SetBaseURL(aURI); +} + +////////////////////////////////////////////////////////////// +// ClassicScript +////////////////////////////////////////////////////////////// + +ClassicScript::ClassicScript(mozilla::dom::ReferrerPolicy aReferrerPolicy, + ScriptFetchOptions* aFetchOptions, nsIURI* aURI) + : LoadedScript(ScriptKind::eClassic, aReferrerPolicy, aFetchOptions, aURI) { +} + +////////////////////////////////////////////////////////////// +// ModuleScript +////////////////////////////////////////////////////////////// + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(ModuleScript, LoadedScript) + +NS_IMPL_CYCLE_COLLECTION_CLASS(ModuleScript) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(ModuleScript, LoadedScript) + tmp->UnlinkModuleRecord(); + tmp->mParseError.setUndefined(); + tmp->mErrorToRethrow.setUndefined(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(ModuleScript, LoadedScript) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(ModuleScript, LoadedScript) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mModuleRecord) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mParseError) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mErrorToRethrow) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +ModuleScript::ModuleScript(mozilla::dom::ReferrerPolicy aReferrerPolicy, + ScriptFetchOptions* aFetchOptions, nsIURI* aURI) + : LoadedScript(ScriptKind::eModule, aReferrerPolicy, aFetchOptions, aURI), + mDebuggerDataInitialized(false) { + MOZ_ASSERT(!ModuleRecord()); + MOZ_ASSERT(!HasParseError()); + MOZ_ASSERT(!HasErrorToRethrow()); +} + +void ModuleScript::Shutdown() { + if (mModuleRecord) { + JS::ClearModuleEnvironment(mModuleRecord); + } + + UnlinkModuleRecord(); +} + +void ModuleScript::UnlinkModuleRecord() { + // Remove the module record's pointer to this object if present and decrement + // our reference count. The reference is added by SetModuleRecord() below. + // + // This takes care not to trigger gray unmarking because this takes a lot of + // time when we're tearing down the entire page. This is safe because we are + // only writing undefined into the module private, so it won't create any + // black-gray edges. + if (mModuleRecord) { + JSObject* module = mModuleRecord.unbarrieredGet(); + MOZ_ASSERT(JS::GetModulePrivate(module).toPrivate() == this); + JS::ClearModulePrivate(module); + mModuleRecord = nullptr; + } +} + +ModuleScript::~ModuleScript() { + // The object may be destroyed without being unlinked first. + UnlinkModuleRecord(); +} + +void ModuleScript::SetModuleRecord(JS::Handle aModuleRecord) { + MOZ_ASSERT(!mModuleRecord); + MOZ_ASSERT_IF(IsModuleScript(), !AsModuleScript()->HasParseError()); + MOZ_ASSERT_IF(IsModuleScript(), !AsModuleScript()->HasErrorToRethrow()); + + mModuleRecord = aModuleRecord; + + // Make module's host defined field point to this object. The JS engine will + // increment our reference count by calling HostAddRefTopLevelScript(). This + // is decremented when the field is cleared in UnlinkModuleRecord() above or + // when the module record dies. + MOZ_ASSERT(JS::GetModulePrivate(mModuleRecord).isUndefined()); + JS::SetModulePrivate(mModuleRecord, JS::PrivateValue(this)); + + mozilla::HoldJSObjects(this); +} + +void ModuleScript::SetParseError(const JS::Value& aError) { + MOZ_ASSERT(!aError.isUndefined()); + MOZ_ASSERT(!HasParseError()); + MOZ_ASSERT(!HasErrorToRethrow()); + + UnlinkModuleRecord(); + mParseError = aError; + mozilla::HoldJSObjects(this); +} + +void ModuleScript::SetErrorToRethrow(const JS::Value& aError) { + MOZ_ASSERT(!aError.isUndefined()); + + // This is only called after SetModuleRecord() or SetParseError() so we don't + // need to call HoldJSObjects() here. + MOZ_ASSERT(ModuleRecord() || HasParseError()); + + mErrorToRethrow = aError; +} + +void ModuleScript::SetDebuggerDataInitialized() { + MOZ_ASSERT(ModuleRecord()); + MOZ_ASSERT(!mDebuggerDataInitialized); + + mDebuggerDataInitialized = true; +} + +} // namespace JS::loader diff --git a/js/loader/LoadedScript.h b/js/loader/LoadedScript.h new file mode 100644 index 0000000000..ca6d1fc179 --- /dev/null +++ b/js/loader/LoadedScript.h @@ -0,0 +1,384 @@ +/* -*- 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 js_loader_LoadedScript_h +#define js_loader_LoadedScript_h + +#include "js/AllocPolicy.h" +#include "js/Transcoding.h" + +#include "mozilla/Maybe.h" +#include "mozilla/MaybeOneOf.h" +#include "mozilla/Utf8.h" // mozilla::Utf8Unit +#include "mozilla/Variant.h" +#include "mozilla/Vector.h" + +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" + +#include "jsapi.h" +#include "ScriptKind.h" +#include "ScriptFetchOptions.h" + +class nsIURI; + +namespace JS::loader { + +class ScriptLoadRequest; + +using Utf8Unit = mozilla::Utf8Unit; + +void HostAddRefTopLevelScript(const JS::Value& aPrivate); +void HostReleaseTopLevelScript(const JS::Value& aPrivate); + +class ClassicScript; +class ModuleScript; +class EventScript; +class LoadContextBase; + +class LoadedScript : public nsISupports { + ScriptKind mKind; + const mozilla::dom::ReferrerPolicy mReferrerPolicy; + RefPtr mFetchOptions; + nsCOMPtr mURI; + nsCOMPtr mBaseURL; + + protected: + LoadedScript(ScriptKind aKind, mozilla::dom::ReferrerPolicy aReferrerPolicy, + ScriptFetchOptions* aFetchOptions, nsIURI* aURI); + + virtual ~LoadedScript(); + + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(LoadedScript) + + bool IsClassicScript() const { return mKind == ScriptKind::eClassic; } + bool IsModuleScript() const { return mKind == ScriptKind::eModule; } + bool IsEventScript() const { return mKind == ScriptKind::eEvent; } + + inline ClassicScript* AsClassicScript(); + inline ModuleScript* AsModuleScript(); + inline EventScript* AsEventScript(); + + // Used to propagate Fetch Options to child modules + ScriptFetchOptions* GetFetchOptions() const { return mFetchOptions; } + + mozilla::dom::ReferrerPolicy ReferrerPolicy() const { + return mReferrerPolicy; + } + + nsIURI* GetURI() const { return mURI; } + void SetBaseURL(nsIURI* aBaseURL) { + MOZ_ASSERT(!mBaseURL); + mBaseURL = aBaseURL; + } + nsIURI* BaseURL() const { return mBaseURL; } + + void AssociateWithScript(JSScript* aScript); + + public: + // =========================================================================== + // Encoding of the content provided by the network, or refined by the JS + // engine. + template + using Variant = mozilla::Variant; + + template + using VariantType = mozilla::VariantType; + + // Type of data provided by the nsChannel. + enum class DataType : uint8_t { eUnknown, eTextSource, eBytecode }; + + // Use a vector backed by the JS allocator for script text so that contents + // can be transferred in constant time to the JS engine, not copied in linear + // time. + template + using ScriptTextBuffer = mozilla::Vector; + + using MaybeSourceText = + mozilla::MaybeOneOf, JS::SourceText>; + + bool IsUnknownDataType() const { return mDataType == DataType::eUnknown; } + bool IsTextSource() const { return mDataType == DataType::eTextSource; } + bool IsSource() const { return IsTextSource(); } + bool IsBytecode() const { return mDataType == DataType::eBytecode; } + + void SetUnknownDataType() { + mDataType = DataType::eUnknown; + mScriptData.reset(); + } + + void SetTextSource(LoadContextBase* maybeLoadContext) { + MOZ_ASSERT(IsUnknownDataType()); + mDataType = DataType::eTextSource; + mScriptData.emplace(VariantType>()); + } + + void SetBytecode() { + MOZ_ASSERT(IsUnknownDataType()); + mDataType = DataType::eBytecode; + } + + bool IsUTF16Text() const { + return mScriptData->is>(); + } + bool IsUTF8Text() const { + return mScriptData->is>(); + } + + template + const ScriptTextBuffer& ScriptText() const { + MOZ_ASSERT(IsTextSource()); + return mScriptData->as>(); + } + template + ScriptTextBuffer& ScriptText() { + MOZ_ASSERT(IsTextSource()); + return mScriptData->as>(); + } + + size_t ScriptTextLength() const { + MOZ_ASSERT(IsTextSource()); + return IsUTF16Text() ? ScriptText().length() + : ScriptText().length(); + } + + // Get source text. On success |aMaybeSource| will contain either UTF-8 or + // UTF-16 source; on failure it will remain in its initial state. + nsresult GetScriptSource(JSContext* aCx, MaybeSourceText* aMaybeSource, + LoadContextBase* aMaybeLoadContext); + + void ClearScriptSource() { + if (IsTextSource()) { + ClearScriptText(); + } + } + + void ClearScriptText() { + MOZ_ASSERT(IsTextSource()); + return IsUTF16Text() ? ScriptText().clearAndFree() + : ScriptText().clearAndFree(); + } + + size_t ReceivedScriptTextLength() const { return mReceivedScriptTextLength; } + + void SetReceivedScriptTextLength(size_t aLength) { + mReceivedScriptTextLength = aLength; + } + + JS::TranscodeBuffer& SRIAndBytecode() { + // Note: SRIAndBytecode might be called even if the IsSource() returns true, + // as we want to be able to save the bytecode content when we are loading + // from source. + MOZ_ASSERT(IsBytecode() || IsSource()); + return mScriptBytecode; + } + JS::TranscodeRange Bytecode() const { + MOZ_ASSERT(IsBytecode()); + const auto& bytecode = mScriptBytecode; + auto offset = mBytecodeOffset; + return JS::TranscodeRange(bytecode.begin() + offset, + bytecode.length() - offset); + } + + size_t GetSRILength() const { + MOZ_ASSERT(IsBytecode() || IsSource()); + return mBytecodeOffset; + } + void SetSRILength(size_t sriLength) { + MOZ_ASSERT(IsBytecode() || IsSource()); + mBytecodeOffset = JS::AlignTranscodingBytecodeOffset(sriLength); + } + + void DropBytecode() { + MOZ_ASSERT(IsBytecode() || IsSource()); + mScriptBytecode.clearAndFree(); + } + + // Determine whether the mScriptData or mScriptBytecode is used. + DataType mDataType; + + // Holds script source data for non-inline scripts. + mozilla::Maybe< + Variant, ScriptTextBuffer>> + mScriptData; + + // The length of script source text, set when reading completes. This is used + // since mScriptData is cleared when the source is passed to the JS engine. + size_t mReceivedScriptTextLength; + + // Holds the SRI serialized hash and the script bytecode for non-inline + // scripts. The data is laid out according to ScriptBytecodeDataLayout + // or, if compression is enabled, ScriptBytecodeCompressedDataLayout. + JS::TranscodeBuffer mScriptBytecode; + uint32_t mBytecodeOffset; // Offset of the bytecode in mScriptBytecode +}; + +// Provide accessors for any classes `Derived` which is providing the +// `getLoadedScript` function as interface. The accessors are meant to be +// inherited by the `Derived` class. +template +class LoadedScriptDelegate { + private: + // Use a static_cast instead of declaring virtual functions. This is + // meant to avoid relying on virtual table, and improve inlining for non-final + // classes. + const LoadedScript* GetLoadedScript() const { + return static_cast(this)->getLoadedScript(); + } + LoadedScript* GetLoadedScript() { + return static_cast(this)->getLoadedScript(); + } + + public: + template + using ScriptTextBuffer = LoadedScript::ScriptTextBuffer; + using MaybeSourceText = LoadedScript::MaybeSourceText; + + bool IsModuleScript() const { return GetLoadedScript()->IsModuleScript(); } + bool IsEventScript() const { return GetLoadedScript()->IsEventScript(); } + + bool IsUnknownDataType() const { + return GetLoadedScript()->IsUnknownDataType(); + } + bool IsTextSource() const { return GetLoadedScript()->IsTextSource(); } + bool IsSource() const { return GetLoadedScript()->IsSource(); } + bool IsBytecode() const { return GetLoadedScript()->IsBytecode(); } + + void SetUnknownDataType() { GetLoadedScript()->SetUnknownDataType(); } + + void SetTextSource(LoadContextBase* maybeLoadContext) { + GetLoadedScript()->SetTextSource(maybeLoadContext); + } + + void SetBytecode() { GetLoadedScript()->SetBytecode(); } + + bool IsUTF16Text() const { return GetLoadedScript()->IsUTF16Text(); } + bool IsUTF8Text() const { return GetLoadedScript()->IsUTF8Text(); } + + template + const ScriptTextBuffer& ScriptText() const { + const LoadedScript* loader = GetLoadedScript(); + return loader->ScriptText(); + } + template + ScriptTextBuffer& ScriptText() { + LoadedScript* loader = GetLoadedScript(); + return loader->ScriptText(); + } + + size_t ScriptTextLength() const { + return GetLoadedScript()->ScriptTextLength(); + } + + size_t ReceivedScriptTextLength() const { + return GetLoadedScript()->ReceivedScriptTextLength(); + } + + void SetReceivedScriptTextLength(size_t aLength) { + GetLoadedScript()->SetReceivedScriptTextLength(aLength); + } + + // Get source text. On success |aMaybeSource| will contain either UTF-8 or + // UTF-16 source; on failure it will remain in its initial state. + nsresult GetScriptSource(JSContext* aCx, MaybeSourceText* aMaybeSource, + LoadContextBase* aLoadContext) { + return GetLoadedScript()->GetScriptSource(aCx, aMaybeSource, aLoadContext); + } + + void ClearScriptSource() { GetLoadedScript()->ClearScriptSource(); } + + void ClearScriptText() { GetLoadedScript()->ClearScriptText(); } + + JS::TranscodeBuffer& SRIAndBytecode() { + return GetLoadedScript()->SRIAndBytecode(); + } + JS::TranscodeRange Bytecode() const { return GetLoadedScript()->Bytecode(); } + + size_t GetSRILength() const { return GetLoadedScript()->GetSRILength(); } + void SetSRILength(size_t sriLength) { + GetLoadedScript()->SetSRILength(sriLength); + } + + void DropBytecode() { GetLoadedScript()->DropBytecode(); } +}; + +class ClassicScript final : public LoadedScript { + ~ClassicScript() = default; + + private: + // Scripts can be created only by ScriptLoadRequest::NoCacheEntryFound. + ClassicScript(mozilla::dom::ReferrerPolicy aReferrerPolicy, + ScriptFetchOptions* aFetchOptions, nsIURI* aURI); + + friend class ScriptLoadRequest; +}; + +class EventScript final : public LoadedScript { + ~EventScript() = default; + + public: + EventScript(mozilla::dom::ReferrerPolicy aReferrerPolicy, + ScriptFetchOptions* aFetchOptions, nsIURI* aURI); +}; + +// A single module script. May be used to satisfy multiple load requests. + +class ModuleScript final : public LoadedScript { + JS::Heap mModuleRecord; + JS::Heap mParseError; + JS::Heap mErrorToRethrow; + bool mDebuggerDataInitialized; + + ~ModuleScript(); + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(ModuleScript, + LoadedScript) + + private: + // Scripts can be created only by ScriptLoadRequest::NoCacheEntryFound. + ModuleScript(mozilla::dom::ReferrerPolicy aReferrerPolicy, + ScriptFetchOptions* aFetchOptions, nsIURI* aURI); + + friend class ScriptLoadRequest; + + public: + void SetModuleRecord(JS::Handle aModuleRecord); + void SetParseError(const JS::Value& aError); + void SetErrorToRethrow(const JS::Value& aError); + void SetDebuggerDataInitialized(); + + JSObject* ModuleRecord() const { return mModuleRecord; } + + JS::Value ParseError() const { return mParseError; } + JS::Value ErrorToRethrow() const { return mErrorToRethrow; } + bool HasParseError() const { return !mParseError.isUndefined(); } + bool HasErrorToRethrow() const { return !mErrorToRethrow.isUndefined(); } + bool DebuggerDataInitialized() const { return mDebuggerDataInitialized; } + + void Shutdown(); + + void UnlinkModuleRecord(); + + friend void CheckModuleScriptPrivate(LoadedScript*, const JS::Value&); +}; + +ClassicScript* LoadedScript::AsClassicScript() { + MOZ_ASSERT(!IsModuleScript()); + return static_cast(this); +} + +ModuleScript* LoadedScript::AsModuleScript() { + MOZ_ASSERT(IsModuleScript()); + return static_cast(this); +} + +} // namespace JS::loader + +#endif // js_loader_LoadedScript_h diff --git a/js/loader/ModuleLoadRequest.cpp b/js/loader/ModuleLoadRequest.cpp new file mode 100644 index 0000000000..d90d41da58 --- /dev/null +++ b/js/loader/ModuleLoadRequest.cpp @@ -0,0 +1,265 @@ +/* -*- 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 "ModuleLoadRequest.h" + +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/dom/ScriptLoadContext.h" + +#include "LoadedScript.h" +#include "LoadContextBase.h" +#include "ModuleLoaderBase.h" + +namespace JS::loader { + +#undef LOG +#define LOG(args) \ + MOZ_LOG(ModuleLoaderBase::gModuleLoaderBaseLog, mozilla::LogLevel::Debug, \ + args) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(ModuleLoadRequest, + ScriptLoadRequest) + +NS_IMPL_CYCLE_COLLECTION_CLASS(ModuleLoadRequest) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(ModuleLoadRequest, + ScriptLoadRequest) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mLoader, mRootModule, mModuleScript, mImports, + mWaitingParentRequest, + mDynamicReferencingScript) + tmp->ClearDynamicImport(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(ModuleLoadRequest, + ScriptLoadRequest) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLoader, mRootModule, mModuleScript, + mImports, mWaitingParentRequest, + mDynamicReferencingScript) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(ModuleLoadRequest, + ScriptLoadRequest) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mDynamicSpecifier) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mDynamicPromise) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +/* static */ +VisitedURLSet* ModuleLoadRequest::NewVisitedSetForTopLevelImport(nsIURI* aURI) { + auto set = new VisitedURLSet(); + set->PutEntry(aURI); + return set; +} + +ModuleLoadRequest::ModuleLoadRequest( + nsIURI* aURI, mozilla::dom::ReferrerPolicy aReferrerPolicy, + ScriptFetchOptions* aFetchOptions, + const mozilla::dom::SRIMetadata& aIntegrity, nsIURI* aReferrer, + LoadContextBase* aContext, bool aIsTopLevel, bool aIsDynamicImport, + ModuleLoaderBase* aLoader, VisitedURLSet* aVisitedSet, + ModuleLoadRequest* aRootModule) + : ScriptLoadRequest(ScriptKind::eModule, aURI, aReferrerPolicy, + aFetchOptions, aIntegrity, aReferrer, aContext), + mIsTopLevel(aIsTopLevel), + mIsDynamicImport(aIsDynamicImport), + mLoader(aLoader), + mRootModule(aRootModule), + mVisitedSet(aVisitedSet) { + MOZ_ASSERT(mLoader); +} + +nsIGlobalObject* ModuleLoadRequest::GetGlobalObject() { + return mLoader->GetGlobalObject(); +} + +bool ModuleLoadRequest::IsErrored() const { + return !mModuleScript || mModuleScript->HasParseError(); +} + +void ModuleLoadRequest::Cancel() { + if (IsCanceled()) { + AssertAllImportsCancelled(); + return; + } + + if (IsFinished()) { + return; + } + + ScriptLoadRequest::Cancel(); + + mModuleScript = nullptr; + CancelImports(); + + if (mWaitingParentRequest) { + ChildLoadComplete(false); + } +} + +void ModuleLoadRequest::SetReady() { + MOZ_ASSERT(!IsFinished()); + + // Mark a module as ready to execute. This means that this module and all it + // dependencies have had their source loaded, parsed as a module and the + // modules instantiated. + + AssertAllImportsFinished(); + + ScriptLoadRequest::SetReady(); + + if (mWaitingParentRequest) { + ChildLoadComplete(true); + } +} + +void ModuleLoadRequest::ModuleLoaded() { + // A module that was found to be marked as fetching in the module map has now + // been loaded. + + LOG(("ScriptLoadRequest (%p): Module loaded", this)); + + if (IsCanceled()) { + AssertAllImportsCancelled(); + return; + } + + MOZ_ASSERT(IsFetching() || IsPendingFetchingError()); + + mModuleScript = mLoader->GetFetchedModule(mURI); + if (IsErrored()) { + ModuleErrored(); + return; + } + + mLoader->StartFetchingModuleDependencies(this); +} + +void ModuleLoadRequest::LoadFailed() { + // We failed to load the source text or an error occurred unrelated to the + // content of the module (e.g. OOM). + + LOG(("ScriptLoadRequest (%p): Module load failed", this)); + + if (IsCanceled()) { + AssertAllImportsCancelled(); + return; + } + + MOZ_ASSERT(IsFetching() || IsPendingFetchingError()); + MOZ_ASSERT(!mModuleScript); + + Cancel(); + LoadFinished(); +} + +void ModuleLoadRequest::ModuleErrored() { + // Parse error, failure to resolve imported modules or error loading import. + + LOG(("ScriptLoadRequest (%p): Module errored", this)); + + if (IsCanceled()) { + return; + } + + MOZ_ASSERT(!IsFinished()); + + CheckModuleDependenciesLoaded(); + MOZ_ASSERT(IsErrored()); + + CancelImports(); + if (IsFinished()) { + // Cancelling an outstanding import will error this request. + return; + } + + SetReady(); + LoadFinished(); +} + +void ModuleLoadRequest::DependenciesLoaded() { + // The module and all of its dependencies have been successfully fetched and + // compiled. + + LOG(("ScriptLoadRequest (%p): Module dependencies loaded", this)); + + if (IsCanceled()) { + return; + } + + MOZ_ASSERT(IsLoadingImports()); + MOZ_ASSERT(!IsErrored()); + + CheckModuleDependenciesLoaded(); + SetReady(); + LoadFinished(); +} + +void ModuleLoadRequest::CheckModuleDependenciesLoaded() { + LOG(("ScriptLoadRequest (%p): Check dependencies loaded", this)); + + if (!mModuleScript || mModuleScript->HasParseError()) { + return; + } + for (const auto& childRequest : mImports) { + ModuleScript* childScript = childRequest->mModuleScript; + if (!childScript) { + mModuleScript = nullptr; + LOG(("ScriptLoadRequest (%p): %p failed (load error)", this, + childRequest.get())); + return; + } + } + + LOG(("ScriptLoadRequest (%p): all ok", this)); +} + +void ModuleLoadRequest::CancelImports() { + for (size_t i = 0; i < mImports.Length(); i++) { + mImports[i]->Cancel(); + } +} + +void ModuleLoadRequest::LoadFinished() { + RefPtr request(this); + if (IsTopLevel() && IsDynamicImport()) { + mLoader->RemoveDynamicImport(request); + } + + mLoader->OnModuleLoadComplete(request); +} + +void ModuleLoadRequest::SetDynamicImport(LoadedScript* aReferencingScript, + JS::Handle aSpecifier, + JS::Handle aPromise) { + mDynamicReferencingScript = aReferencingScript; + mDynamicSpecifier = aSpecifier; + mDynamicPromise = aPromise; + + mozilla::HoldJSObjects(this); +} + +void ModuleLoadRequest::ClearDynamicImport() { + mDynamicReferencingScript = nullptr; + mDynamicSpecifier = nullptr; + mDynamicPromise = nullptr; +} + +inline void ModuleLoadRequest::AssertAllImportsFinished() const { +#ifdef DEBUG + for (const auto& request : mImports) { + MOZ_ASSERT(request->IsFinished()); + } +#endif +} + +inline void ModuleLoadRequest::AssertAllImportsCancelled() const { +#ifdef DEBUG + for (const auto& request : mImports) { + MOZ_ASSERT(request->IsCanceled()); + } +#endif +} + +} // namespace JS::loader diff --git a/js/loader/ModuleLoadRequest.h b/js/loader/ModuleLoadRequest.h new file mode 100644 index 0000000000..cb33c532fc --- /dev/null +++ b/js/loader/ModuleLoadRequest.h @@ -0,0 +1,178 @@ +/* -*- 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 js_loader_ModuleLoadRequest_h +#define js_loader_ModuleLoadRequest_h + +#include "LoadContextBase.h" +#include "ScriptLoadRequest.h" +#include "ModuleLoaderBase.h" +#include "mozilla/Assertions.h" +#include "js/RootingAPI.h" +#include "js/Value.h" +#include "nsURIHashKey.h" +#include "nsTHashtable.h" + +namespace JS::loader { + +class LoadedScript; +class ModuleScript; +class ModuleLoaderBase; + +// A reference counted set of URLs we have visited in the process of loading a +// module graph. +class VisitedURLSet : public nsTHashtable { + NS_INLINE_DECL_REFCOUNTING(VisitedURLSet) + + private: + ~VisitedURLSet() = default; +}; + +// A load request for a module, created for every top level module script and +// every module import. Load request can share an ModuleScript if there are +// multiple imports of the same module. + +class ModuleLoadRequest final : public ScriptLoadRequest { + ~ModuleLoadRequest() { + MOZ_ASSERT(!mWaitingParentRequest); + MOZ_ASSERT(mAwaitingImports == 0); + } + + ModuleLoadRequest(const ModuleLoadRequest& aOther) = delete; + ModuleLoadRequest(ModuleLoadRequest&& aOther) = delete; + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(ModuleLoadRequest, + ScriptLoadRequest) + using SRIMetadata = mozilla::dom::SRIMetadata; + + ModuleLoadRequest(nsIURI* aURI, mozilla::dom::ReferrerPolicy aReferrerPolicy, + ScriptFetchOptions* aFetchOptions, + const SRIMetadata& aIntegrity, nsIURI* aReferrer, + LoadContextBase* aContext, bool aIsTopLevel, + bool aIsDynamicImport, ModuleLoaderBase* aLoader, + VisitedURLSet* aVisitedSet, ModuleLoadRequest* aRootModule); + + static VisitedURLSet* NewVisitedSetForTopLevelImport(nsIURI* aURI); + + bool IsTopLevel() const override { return mIsTopLevel; } + + bool IsDynamicImport() const { return mIsDynamicImport; } + + bool IsErrored() const; + + nsIGlobalObject* GetGlobalObject(); + + void SetReady() override; + void Cancel() override; + + void SetDynamicImport(LoadedScript* aReferencingScript, + JS::Handle aSpecifier, + JS::Handle aPromise); + void ClearDynamicImport(); + + void ModuleLoaded(); + void ModuleErrored(); + void DependenciesLoaded(); + void LoadFailed(); + + ModuleLoadRequest* GetRootModule() { + if (!mRootModule) { + return this; + } + return mRootModule; + } + + bool IsModuleMarkedForBytecodeEncoding() const { + return mIsMarkedForBytecodeEncoding; + } + void MarkModuleForBytecodeEncoding() { + MOZ_ASSERT(!IsModuleMarkedForBytecodeEncoding()); + mIsMarkedForBytecodeEncoding = true; + } + + // Convenience methods to call into the module loader for this request. + + void CancelDynamicImport(nsresult aResult) { + MOZ_ASSERT(IsDynamicImport()); + mLoader->CancelDynamicImport(this, aResult); + } +#ifdef DEBUG + bool IsRegisteredDynamicImport() const { + return IsDynamicImport() && mLoader->HasDynamicImport(this); + } +#endif + nsresult StartModuleLoad() { return mLoader->StartModuleLoad(this); } + nsresult RestartModuleLoad() { return mLoader->RestartModuleLoad(this); } + nsresult OnFetchComplete(nsresult aRv) { + return mLoader->OnFetchComplete(this, aRv); + } + bool InstantiateModuleGraph() { + return mLoader->InstantiateModuleGraph(this); + } + nsresult EvaluateModule() { return mLoader->EvaluateModule(this); } + void StartDynamicImport() { mLoader->StartDynamicImport(this); } + void ProcessDynamicImport() { mLoader->ProcessDynamicImport(this); } + + void ChildLoadComplete(bool aSuccess); + + private: + void LoadFinished(); + void CancelImports(); + void CheckModuleDependenciesLoaded(); + + void AssertAllImportsFinished() const; + void AssertAllImportsCancelled() const; + + public: + // Is this a request for a top level module script or an import? + const bool mIsTopLevel; + + // Is this the top level request for a dynamic module import? + const bool mIsDynamicImport; + + // True if this module is planned to be saved in the bytecode cache. + // ModuleLoadRequest doesn't use ScriptLoadRequest::mScriptForBytecodeEncoding + // field because the JSScript reference isn't always avaialble for module. + bool mIsMarkedForBytecodeEncoding = false; + + // Pointer to the script loader, used to trigger actions when the module load + // finishes. + RefPtr mLoader; + + // Pointer to the top level module of this module graph, nullptr if this is a + // top level module + RefPtr mRootModule; + + // Set to a module script object after a successful load or nullptr on + // failure. + RefPtr mModuleScript; + + // Array of imported modules. + nsTArray> mImports; + + // Parent module (i.e. importer of this module) that is waiting for this + // module and its dependencies to load, or null. + RefPtr mWaitingParentRequest; + + // Number of child modules (i.e. imported modules) that this module is waiting + // for. + size_t mAwaitingImports = 0; + + // Set of module URLs visited while fetching the module graph this request is + // part of. + RefPtr mVisitedSet; + + // For dynamic imports, the details to pass to FinishDynamicImport. + RefPtr mDynamicReferencingScript; + JS::Heap mDynamicSpecifier; + JS::Heap mDynamicPromise; +}; + +} // namespace JS::loader + +#endif // js_loader_ModuleLoadRequest_h diff --git a/js/loader/ModuleLoaderBase.cpp b/js/loader/ModuleLoaderBase.cpp new file mode 100644 index 0000000000..59e77b2d9c --- /dev/null +++ b/js/loader/ModuleLoaderBase.cpp @@ -0,0 +1,1431 @@ +/* -*- 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 "GeckoProfiler.h" +#include "LoadedScript.h" +#include "ModuleLoadRequest.h" +#include "ScriptLoadRequest.h" +#include "mozilla/dom/ScriptSettings.h" // AutoJSAPI +#include "mozilla/dom/ScriptTrace.h" + +#include "js/Array.h" // JS::GetArrayLength +#include "js/CompilationAndEvaluation.h" +#include "js/ColumnNumber.h" // JS::ColumnNumberOneOrigin +#include "js/ContextOptions.h" // JS::ContextOptionsRef +#include "js/ErrorReport.h" // JSErrorBase +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/Modules.h" // JS::FinishDynamicModuleImport, JS::{G,S}etModuleResolveHook, JS::Get{ModulePrivate,ModuleScript,RequestedModule{s,Specifier,SourcePos}}, JS::SetModule{DynamicImport,Metadata}Hook +#include "js/PropertyAndElement.h" // JS_DefineProperty, JS_GetElement +#include "js/SourceText.h" +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/BasePrincipal.h" +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/ScriptLoadContext.h" +#include "mozilla/CycleCollectedJSContext.h" // nsAutoMicroTask +#include "mozilla/Preferences.h" +#include "mozilla/RefPtr.h" // mozilla::StaticRefPtr +#include "mozilla/StaticPrefs_dom.h" +#include "nsContentUtils.h" +#include "nsICacheInfoChannel.h" // nsICacheInfoChannel +#include "nsNetUtil.h" // NS_NewURI +#include "xpcpublic.h" + +using mozilla::CycleCollectedJSContext; +using mozilla::Err; +using mozilla::Preferences; +using mozilla::UniquePtr; +using mozilla::WrapNotNull; +using mozilla::dom::AutoJSAPI; + +namespace JS::loader { + +mozilla::LazyLogModule ModuleLoaderBase::gCspPRLog("CSP"); +mozilla::LazyLogModule ModuleLoaderBase::gModuleLoaderBaseLog( + "ModuleLoaderBase"); + +#undef LOG +#define LOG(args) \ + MOZ_LOG(ModuleLoaderBase::gModuleLoaderBaseLog, mozilla::LogLevel::Debug, \ + args) + +#define LOG_ENABLED() \ + MOZ_LOG_TEST(ModuleLoaderBase::gModuleLoaderBaseLog, mozilla::LogLevel::Debug) + +////////////////////////////////////////////////////////////// +// ModuleLoaderBase::WaitingRequests +////////////////////////////////////////////////////////////// + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ModuleLoaderBase::WaitingRequests) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION(ModuleLoaderBase::WaitingRequests, mWaiting) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(ModuleLoaderBase::WaitingRequests) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ModuleLoaderBase::WaitingRequests) + +////////////////////////////////////////////////////////////// +// ModuleLoaderBase +////////////////////////////////////////////////////////////// + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ModuleLoaderBase) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION(ModuleLoaderBase, mFetchingModules, mFetchedModules, + mDynamicImportRequests, mGlobalObject, mLoader) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(ModuleLoaderBase) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ModuleLoaderBase) + +// static +void ModuleLoaderBase::EnsureModuleHooksInitialized() { + AutoJSAPI jsapi; + jsapi.Init(); + JSRuntime* rt = JS_GetRuntime(jsapi.cx()); + if (JS::GetModuleResolveHook(rt)) { + return; + } + + JS::SetModuleResolveHook(rt, HostResolveImportedModule); + JS::SetModuleMetadataHook(rt, HostPopulateImportMeta); + JS::SetScriptPrivateReferenceHooks(rt, HostAddRefTopLevelScript, + HostReleaseTopLevelScript); + JS::SetModuleDynamicImportHook(rt, HostImportModuleDynamically); +} + +// 8.1.3.8.1 HostResolveImportedModule(referencingModule, moduleRequest) +/** + * Implement the HostResolveImportedModule abstract operation. + * + * Resolve a module specifier string and look this up in the module + * map, returning the result. This is only called for previously + * loaded modules and always succeeds. + * + * @param aReferencingPrivate A JS::Value which is either undefined + * or contains a LoadedScript private pointer. + * @param aModuleRequest A module request object. + * @returns module This is set to the module found. + */ +// static +JSObject* ModuleLoaderBase::HostResolveImportedModule( + JSContext* aCx, JS::Handle aReferencingPrivate, + JS::Handle aModuleRequest) { + JS::Rooted module(aCx); + + { + // LoadedScript should only live in this block, otherwise it will be a GC + // hazard + RefPtr script( + GetLoadedScriptOrNull(aCx, aReferencingPrivate)); + + JS::Rooted specifierString( + aCx, JS::GetModuleRequestSpecifier(aCx, aModuleRequest)); + if (!specifierString) { + return nullptr; + } + + // Let url be the result of resolving a module specifier given referencing + // module script and specifier. + nsAutoJSString string; + if (!string.init(aCx, specifierString)) { + return nullptr; + } + + RefPtr loader = GetCurrentModuleLoader(aCx); + if (!loader) { + return nullptr; + } + + auto result = loader->ResolveModuleSpecifier(script, string); + // This cannot fail because resolving a module specifier must have been + // previously successful with these same two arguments. + MOZ_ASSERT(result.isOk()); + nsCOMPtr uri = result.unwrap(); + MOZ_ASSERT(uri, "Failed to resolve previously-resolved module specifier"); + + // Let resolved module script be moduleMap[url]. (This entry must exist for + // us to have gotten to this point.) + ModuleScript* ms = loader->GetFetchedModule(uri); + MOZ_ASSERT(ms, "Resolved module not found in module map"); + MOZ_ASSERT(!ms->HasParseError()); + MOZ_ASSERT(ms->ModuleRecord()); + + module.set(ms->ModuleRecord()); + } + return module; +} + +// static +bool ModuleLoaderBase::ImportMetaResolve(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedValue modulePrivate( + cx, js::GetFunctionNativeReserved(&args.callee(), ModulePrivateSlot)); + + // https://html.spec.whatwg.org/#hostgetimportmetaproperties + // Step 4.1. Set specifier to ? ToString(specifier). + // + // https://tc39.es/ecma262/#sec-tostring + RootedValue v(cx, args.get(ImportMetaResolveSpecifierArg)); + RootedString specifier(cx, JS::ToString(cx, v)); + if (!specifier) { + return false; + } + + // Step 4.2, 4.3 are implemented in ImportMetaResolveImpl. + RootedString url(cx, ImportMetaResolveImpl(cx, modulePrivate, specifier)); + if (!url) { + return false; + } + + // Step 4.4. Return the serialization of url. + args.rval().setString(url); + return true; +} + +// static +JSString* ModuleLoaderBase::ImportMetaResolveImpl( + JSContext* aCx, JS::Handle aReferencingPrivate, + JS::Handle aSpecifier) { + RootedString urlString(aCx); + + { + // ModuleScript should only live in this block, otherwise it will be a GC + // hazard + RefPtr script = + static_cast(aReferencingPrivate.toPrivate()); + MOZ_ASSERT(script->IsModuleScript()); + MOZ_ASSERT(JS::GetModulePrivate(script->ModuleRecord()) == + aReferencingPrivate); + + RefPtr loader = GetCurrentModuleLoader(aCx); + if (!loader) { + return nullptr; + } + + nsAutoJSString specifier; + if (!specifier.init(aCx, aSpecifier)) { + return nullptr; + } + + auto result = loader->ResolveModuleSpecifier(script, specifier); + if (result.isErr()) { + JS::Rooted error(aCx); + nsresult rv = loader->HandleResolveFailure( + aCx, script, specifier, result.unwrapErr(), 0, + JS::ColumnNumberOneOrigin(), &error); + if (NS_FAILED(rv)) { + JS_ReportOutOfMemory(aCx); + return nullptr; + } + + JS_SetPendingException(aCx, error); + + return nullptr; + } + + nsCOMPtr uri = result.unwrap(); + nsAutoCString url; + MOZ_ALWAYS_SUCCEEDS(uri->GetAsciiSpec(url)); + + urlString.set(JS_NewStringCopyZ(aCx, url.get())); + } + + return urlString; +} + +// static +bool ModuleLoaderBase::HostPopulateImportMeta( + JSContext* aCx, JS::Handle aReferencingPrivate, + JS::Handle aMetaObject) { + RefPtr script = + static_cast(aReferencingPrivate.toPrivate()); + MOZ_ASSERT(script->IsModuleScript()); + MOZ_ASSERT(JS::GetModulePrivate(script->ModuleRecord()) == + aReferencingPrivate); + + nsAutoCString url; + MOZ_DIAGNOSTIC_ASSERT(script->BaseURL()); + MOZ_ALWAYS_SUCCEEDS(script->BaseURL()->GetAsciiSpec(url)); + + JS::Rooted urlString(aCx, JS_NewStringCopyZ(aCx, url.get())); + if (!urlString) { + JS_ReportOutOfMemory(aCx); + return false; + } + + // https://html.spec.whatwg.org/#import-meta-url + if (!JS_DefineProperty(aCx, aMetaObject, "url", urlString, + JSPROP_ENUMERATE)) { + return false; + } + + // https://html.spec.whatwg.org/#import-meta-resolve + // Define 'resolve' function on the import.meta object. + JSFunction* resolveFunc = js::DefineFunctionWithReserved( + aCx, aMetaObject, "resolve", ImportMetaResolve, ImportMetaResolveNumArgs, + JSPROP_ENUMERATE); + if (!resolveFunc) { + return false; + } + + // Store the 'active script' of the meta object into the function slot. + // https://html.spec.whatwg.org/#active-script + RootedObject resolveFuncObj(aCx, JS_GetFunctionObject(resolveFunc)); + js::SetFunctionNativeReserved(resolveFuncObj, ModulePrivateSlot, + aReferencingPrivate); + + return true; +} + +// static +bool ModuleLoaderBase::HostImportModuleDynamically( + JSContext* aCx, JS::Handle aReferencingPrivate, + JS::Handle aModuleRequest, JS::Handle aPromise) { + MOZ_DIAGNOSTIC_ASSERT(aModuleRequest); + MOZ_DIAGNOSTIC_ASSERT(aPromise); + + RefPtr script(GetLoadedScriptOrNull(aCx, aReferencingPrivate)); + + JS::Rooted specifierString( + aCx, JS::GetModuleRequestSpecifier(aCx, aModuleRequest)); + if (!specifierString) { + return false; + } + + // Attempt to resolve the module specifier. + nsAutoJSString specifier; + if (!specifier.init(aCx, specifierString)) { + return false; + } + + RefPtr loader = GetCurrentModuleLoader(aCx); + if (!loader) { + return false; + } + + auto result = loader->ResolveModuleSpecifier(script, specifier); + if (result.isErr()) { + JS::Rooted error(aCx); + nsresult rv = + loader->HandleResolveFailure(aCx, script, specifier, result.unwrapErr(), + 0, JS::ColumnNumberOneOrigin(), &error); + if (NS_FAILED(rv)) { + JS_ReportOutOfMemory(aCx); + return false; + } + + JS_SetPendingException(aCx, error); + return false; + } + + // Create a new top-level load request. + nsCOMPtr uri = result.unwrap(); + RefPtr request = + loader->CreateDynamicImport(aCx, uri, script, specifierString, aPromise); + + if (!request) { + // Throws TypeError if CreateDynamicImport returns nullptr. + JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr, + JSMSG_DYNAMIC_IMPORT_NOT_SUPPORTED); + + return false; + } + + nsresult rv = loader->StartDynamicImport(request); + if (NS_SUCCEEDED(rv)) { + loader->OnDynamicImportStarted(request); + } + + return true; +} + +AutoOverrideModuleLoader::AutoOverrideModuleLoader(ModuleLoaderBase* aTarget, + ModuleLoaderBase* aLoader) + : mTarget(aTarget) { + mTarget->SetOverride(aLoader); +} + +AutoOverrideModuleLoader::~AutoOverrideModuleLoader() { + mTarget->ResetOverride(); +} + +void ModuleLoaderBase::SetOverride(ModuleLoaderBase* aLoader) { + MOZ_ASSERT(!mOverriddenBy); + MOZ_ASSERT(!aLoader->mOverriddenBy); + MOZ_ASSERT(mGlobalObject == aLoader->mGlobalObject); + mOverriddenBy = aLoader; +} + +bool ModuleLoaderBase::IsOverridden() { return !!mOverriddenBy; } + +bool ModuleLoaderBase::IsOverriddenBy(ModuleLoaderBase* aLoader) { + return mOverriddenBy == aLoader; +} + +void ModuleLoaderBase::ResetOverride() { + MOZ_ASSERT(mOverriddenBy); + mOverriddenBy = nullptr; +} + +// static +ModuleLoaderBase* ModuleLoaderBase::GetCurrentModuleLoader(JSContext* aCx) { + auto reportError = mozilla::MakeScopeExit([aCx]() { + JS_ReportErrorASCII(aCx, "No ScriptLoader found for the current context"); + }); + + JS::Rooted object(aCx, JS::CurrentGlobalOrNull(aCx)); + if (!object) { + return nullptr; + } + + nsIGlobalObject* global = xpc::NativeGlobal(object); + if (!global) { + return nullptr; + } + + ModuleLoaderBase* loader = global->GetModuleLoader(aCx); + if (!loader) { + return nullptr; + } + + MOZ_ASSERT(loader->mGlobalObject == global); + + reportError.release(); + + if (loader->mOverriddenBy) { + MOZ_ASSERT(loader->mOverriddenBy->mGlobalObject == global); + return loader->mOverriddenBy; + } + return loader; +} + +// static +LoadedScript* ModuleLoaderBase::GetLoadedScriptOrNull( + JSContext* aCx, JS::Handle aReferencingPrivate) { + if (aReferencingPrivate.isUndefined()) { + return nullptr; + } + + auto* script = static_cast(aReferencingPrivate.toPrivate()); + + MOZ_ASSERT_IF( + script->IsModuleScript(), + JS::GetModulePrivate(script->AsModuleScript()->ModuleRecord()) == + aReferencingPrivate); + + return script; +} + +JS::Value PrivateFromLoadedScript(LoadedScript* aScript) { + if (!aScript) { + return JS::UndefinedValue(); + } + + return JS::PrivateValue(aScript); +} + +nsresult ModuleLoaderBase::StartModuleLoad(ModuleLoadRequest* aRequest) { + return StartOrRestartModuleLoad(aRequest, RestartRequest::No); +} + +nsresult ModuleLoaderBase::RestartModuleLoad(ModuleLoadRequest* aRequest) { + return StartOrRestartModuleLoad(aRequest, RestartRequest::Yes); +} + +nsresult ModuleLoaderBase::StartOrRestartModuleLoad(ModuleLoadRequest* aRequest, + RestartRequest aRestart) { + MOZ_ASSERT(aRequest->mLoader == this); + MOZ_ASSERT(aRequest->IsFetching() || aRequest->IsPendingFetchingError()); + + aRequest->SetUnknownDataType(); + + // If we're restarting the request, the module should already be in the + // "fetching" map. + MOZ_ASSERT_IF(aRestart == RestartRequest::Yes, + IsModuleFetching(aRequest->mURI)); + + // Check with the derived class whether we should load this module. + nsresult rv = NS_OK; + if (!CanStartLoad(aRequest, &rv)) { + return rv; + } + + // Check whether the module has been fetched or is currently being fetched, + // and if so wait for it rather than starting a new fetch. + ModuleLoadRequest* request = aRequest->AsModuleRequest(); + + if (aRestart == RestartRequest::No && ModuleMapContainsURL(request->mURI)) { + LOG(("ScriptLoadRequest (%p): Waiting for module fetch", aRequest)); + WaitForModuleFetch(request); + return NS_OK; + } + + rv = StartFetch(aRequest); + NS_ENSURE_SUCCESS(rv, rv); + + // We successfully started fetching a module so put its URL in the module + // map and mark it as fetching. + if (aRestart == RestartRequest::No) { + SetModuleFetchStarted(aRequest->AsModuleRequest()); + } + + return NS_OK; +} + +bool ModuleLoaderBase::ModuleMapContainsURL(nsIURI* aURL) const { + return IsModuleFetching(aURL) || IsModuleFetched(aURL); +} + +bool ModuleLoaderBase::IsModuleFetching(nsIURI* aURL) const { + return mFetchingModules.Contains(aURL); +} + +bool ModuleLoaderBase::IsModuleFetched(nsIURI* aURL) const { + return mFetchedModules.Contains(aURL); +} + +nsresult ModuleLoaderBase::GetFetchedModuleURLs(nsTArray& aURLs) { + for (const auto& entry : mFetchedModules) { + nsIURI* uri = entry.GetData()->BaseURL(); + + nsAutoCString spec; + nsresult rv = uri->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + aURLs.AppendElement(spec); + } + + return NS_OK; +} + +void ModuleLoaderBase::SetModuleFetchStarted(ModuleLoadRequest* aRequest) { + // Update the module map to indicate that a module is currently being fetched. + + MOZ_ASSERT(aRequest->IsFetching() || aRequest->IsPendingFetchingError()); + MOZ_ASSERT(!ModuleMapContainsURL(aRequest->mURI)); + + mFetchingModules.InsertOrUpdate(aRequest->mURI, nullptr); +} + +void ModuleLoaderBase::SetModuleFetchFinishedAndResumeWaitingRequests( + ModuleLoadRequest* aRequest, nsresult aResult) { + // Update module map with the result of fetching a single module script. + // + // If any requests for the same URL are waiting on this one to complete, call + // ModuleLoaded or LoadFailed to resume or fail them as appropriate. + + MOZ_ASSERT(aRequest->mLoader == this); + + LOG( + ("ScriptLoadRequest (%p): Module fetch finished (script == %p, result == " + "%u)", + aRequest, aRequest->mModuleScript.get(), unsigned(aResult))); + + RefPtr waitingRequests; + if (!mFetchingModules.Remove(aRequest->mURI, + getter_AddRefs(waitingRequests))) { + LOG( + ("ScriptLoadRequest (%p): Key not found in mFetchingModules, " + "assuming we have an inline module or have finished fetching already", + aRequest)); + return; + } + + RefPtr moduleScript(aRequest->mModuleScript); + MOZ_ASSERT(NS_FAILED(aResult) == !moduleScript); + + mFetchedModules.InsertOrUpdate(aRequest->mURI, RefPtr{moduleScript}); + + if (waitingRequests) { + LOG(("ScriptLoadRequest (%p): Resuming waiting requests", aRequest)); + ResumeWaitingRequests(waitingRequests, bool(moduleScript)); + } +} + +void ModuleLoaderBase::ResumeWaitingRequests(WaitingRequests* aWaitingRequests, + bool aSuccess) { + for (ModuleLoadRequest* request : aWaitingRequests->mWaiting) { + ResumeWaitingRequest(request, aSuccess); + } +} + +void ModuleLoaderBase::ResumeWaitingRequest(ModuleLoadRequest* aRequest, + bool aSuccess) { + if (aSuccess) { + aRequest->ModuleLoaded(); + } else { + aRequest->LoadFailed(); + } +} + +void ModuleLoaderBase::WaitForModuleFetch(ModuleLoadRequest* aRequest) { + nsIURI* uri = aRequest->mURI; + MOZ_ASSERT(ModuleMapContainsURL(uri)); + + if (auto entry = mFetchingModules.Lookup(uri)) { + RefPtr waitingRequests = entry.Data(); + if (!waitingRequests) { + waitingRequests = new WaitingRequests(); + mFetchingModules.InsertOrUpdate(uri, waitingRequests); + } + + waitingRequests->mWaiting.AppendElement(aRequest); + return; + } + + RefPtr ms; + MOZ_ALWAYS_TRUE(mFetchedModules.Get(uri, getter_AddRefs(ms))); + + ResumeWaitingRequest(aRequest, bool(ms)); +} + +ModuleScript* ModuleLoaderBase::GetFetchedModule(nsIURI* aURL) const { + if (LOG_ENABLED()) { + nsAutoCString url; + aURL->GetAsciiSpec(url); + LOG(("GetFetchedModule %s", url.get())); + } + + bool found; + ModuleScript* ms = mFetchedModules.GetWeak(aURL, &found); + MOZ_ASSERT(found); + return ms; +} + +nsresult ModuleLoaderBase::OnFetchComplete(ModuleLoadRequest* aRequest, + nsresult aRv) { + MOZ_ASSERT(aRequest->mLoader == this); + MOZ_ASSERT(!aRequest->mModuleScript); + + nsresult rv = aRv; + if (NS_SUCCEEDED(rv)) { + rv = CreateModuleScript(aRequest); + + // If a module script was created, it should either have a module record + // object or a parse error. + if (ModuleScript* ms = aRequest->mModuleScript) { + MOZ_DIAGNOSTIC_ASSERT(bool(ms->ModuleRecord()) != ms->HasParseError()); + } + + aRequest->ClearScriptSource(); + + if (NS_FAILED(rv)) { + aRequest->LoadFailed(); + return rv; + } + } + + MOZ_ASSERT(NS_SUCCEEDED(rv) == bool(aRequest->mModuleScript)); + SetModuleFetchFinishedAndResumeWaitingRequests(aRequest, rv); + + if (!aRequest->IsErrored()) { + StartFetchingModuleDependencies(aRequest); + } + + return NS_OK; +} + +nsresult ModuleLoaderBase::CreateModuleScript(ModuleLoadRequest* aRequest) { + MOZ_ASSERT(!aRequest->mModuleScript); + MOZ_ASSERT(aRequest->mBaseURL); + + LOG(("ScriptLoadRequest (%p): Create module script", aRequest)); + + AutoJSAPI jsapi; + if (!jsapi.Init(mGlobalObject)) { + return NS_ERROR_FAILURE; + } + + nsresult rv; + { + JSContext* cx = jsapi.cx(); + JS::Rooted module(cx); + + JS::CompileOptions options(cx); + JS::RootedScript introductionScript(cx); + rv = mLoader->FillCompileOptionsForRequest(cx, aRequest, &options, + &introductionScript); + + if (NS_SUCCEEDED(rv)) { + JS::Rooted global(cx, mGlobalObject->GetGlobalJSObject()); + rv = CompileFetchedModule(cx, global, options, aRequest, &module); + } + + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv) == (module != nullptr)); + + if (module) { + JS::RootedValue privateValue(cx); + JS::RootedScript moduleScript(cx, JS::GetModuleScript(module)); + JS::InstantiateOptions instantiateOptions(options); + if (!JS::UpdateDebugMetadata(cx, moduleScript, instantiateOptions, + privateValue, nullptr, introductionScript, + nullptr)) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + + MOZ_ASSERT(aRequest->mLoadedScript->IsModuleScript()); + MOZ_ASSERT(aRequest->mLoadedScript->GetFetchOptions() == + aRequest->mFetchOptions); + MOZ_ASSERT(aRequest->mLoadedScript->GetURI() == aRequest->mURI); + aRequest->mLoadedScript->SetBaseURL(aRequest->mBaseURL); + RefPtr moduleScript = + aRequest->mLoadedScript->AsModuleScript(); + aRequest->mModuleScript = moduleScript; + + if (!module) { + LOG(("ScriptLoadRequest (%p): compilation failed (%d)", aRequest, + unsigned(rv))); + + JS::Rooted error(cx); + if (!jsapi.HasException() || !jsapi.StealException(&error) || + error.isUndefined()) { + aRequest->mModuleScript = nullptr; + return NS_ERROR_FAILURE; + } + + moduleScript->SetParseError(error); + aRequest->ModuleErrored(); + return NS_OK; + } + + moduleScript->SetModuleRecord(module); + + // Validate requested modules and treat failure to resolve module specifiers + // the same as a parse error. + rv = ResolveRequestedModules(aRequest, nullptr); + if (NS_FAILED(rv)) { + if (!aRequest->IsErrored()) { + aRequest->mModuleScript = nullptr; + return rv; + } + aRequest->ModuleErrored(); + return NS_OK; + } + } + + LOG(("ScriptLoadRequest (%p): module script == %p", aRequest, + aRequest->mModuleScript.get())); + + return rv; +} + +nsresult ModuleLoaderBase::GetResolveFailureMessage(ResolveError aError, + const nsAString& aSpecifier, + nsAString& aResult) { + AutoTArray errorParams; + errorParams.AppendElement(aSpecifier); + + nsresult rv = nsContentUtils::FormatLocalizedString( + nsContentUtils::eDOM_PROPERTIES, ResolveErrorInfo::GetString(aError), + errorParams, aResult); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +nsresult ModuleLoaderBase::HandleResolveFailure( + JSContext* aCx, LoadedScript* aScript, const nsAString& aSpecifier, + ResolveError aError, uint32_t aLineNumber, + JS::ColumnNumberOneOrigin aColumnNumber, + JS::MutableHandle aErrorOut) { + JS::Rooted filename(aCx); + if (aScript) { + nsAutoCString url; + aScript->BaseURL()->GetAsciiSpec(url); + filename = JS_NewStringCopyZ(aCx, url.get()); + } else { + filename = JS_NewStringCopyZ(aCx, "(unknown)"); + } + + if (!filename) { + return NS_ERROR_OUT_OF_MEMORY; + } + + nsAutoString errorText; + nsresult rv = GetResolveFailureMessage(aError, aSpecifier, errorText); + NS_ENSURE_SUCCESS(rv, rv); + + JS::Rooted string(aCx, JS_NewUCStringCopyZ(aCx, errorText.get())); + if (!string) { + return NS_ERROR_OUT_OF_MEMORY; + } + + if (!JS::CreateError(aCx, JSEXN_TYPEERR, nullptr, filename, aLineNumber, + aColumnNumber, nullptr, string, JS::NothingHandleValue, + aErrorOut)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +ResolveResult ModuleLoaderBase::ResolveModuleSpecifier( + LoadedScript* aScript, const nsAString& aSpecifier) { + // Import Maps are not supported on workers/worklets. + // See https://github.com/WICG/import-maps/issues/2 + MOZ_ASSERT_IF(!NS_IsMainThread(), mImportMap == nullptr); + // Forward to the updated 'Resolve a module specifier' algorithm defined in + // the Import Maps spec. + return ImportMap::ResolveModuleSpecifier(mImportMap.get(), mLoader, aScript, + aSpecifier); +} + +nsresult ModuleLoaderBase::ResolveRequestedModules( + ModuleLoadRequest* aRequest, nsCOMArray* aUrlsOut) { + ModuleScript* ms = aRequest->mModuleScript; + + AutoJSAPI jsapi; + if (!jsapi.Init(mGlobalObject)) { + return NS_ERROR_FAILURE; + } + + JSContext* cx = jsapi.cx(); + JS::Rooted moduleRecord(cx, ms->ModuleRecord()); + uint32_t length = JS::GetRequestedModulesCount(cx, moduleRecord); + + for (uint32_t i = 0; i < length; i++) { + JS::Rooted str( + cx, JS::GetRequestedModuleSpecifier(cx, moduleRecord, i)); + MOZ_ASSERT(str); + + nsAutoJSString specifier; + if (!specifier.init(cx, str)) { + return NS_ERROR_FAILURE; + } + + // Let url be the result of resolving a module specifier given module script + // and requested. + ModuleLoaderBase* loader = aRequest->mLoader; + auto result = loader->ResolveModuleSpecifier(ms, specifier); + if (result.isErr()) { + uint32_t lineNumber = 0; + JS::ColumnNumberOneOrigin columnNumber; + JS::GetRequestedModuleSourcePos(cx, moduleRecord, i, &lineNumber, + &columnNumber); + + JS::Rooted error(cx); + nsresult rv = + loader->HandleResolveFailure(cx, ms, specifier, result.unwrapErr(), + lineNumber, columnNumber, &error); + NS_ENSURE_SUCCESS(rv, rv); + + ms->SetParseError(error); + return NS_ERROR_FAILURE; + } + + nsCOMPtr uri = result.unwrap(); + if (aUrlsOut) { + aUrlsOut->AppendElement(uri.forget()); + } + } + + return NS_OK; +} + +void ModuleLoaderBase::StartFetchingModuleDependencies( + ModuleLoadRequest* aRequest) { + LOG(("ScriptLoadRequest (%p): Start fetching module dependencies", aRequest)); + + if (aRequest->IsCanceled()) { + return; + } + + MOZ_ASSERT(aRequest->mModuleScript); + MOZ_ASSERT(!aRequest->mModuleScript->HasParseError()); + MOZ_ASSERT(aRequest->IsFetching() || aRequest->IsCompiling()); + + auto visitedSet = aRequest->mVisitedSet; + MOZ_ASSERT(visitedSet->Contains(aRequest->mURI)); + + aRequest->mState = ModuleLoadRequest::State::LoadingImports; + + nsCOMArray urls; + nsresult rv = ResolveRequestedModules(aRequest, &urls); + if (NS_FAILED(rv)) { + aRequest->mModuleScript = nullptr; + aRequest->ModuleErrored(); + return; + } + + // Remove already visited URLs from the list. Put unvisited URLs into the + // visited set. + int32_t i = 0; + while (i < urls.Count()) { + nsIURI* url = urls[i]; + if (visitedSet->Contains(url)) { + urls.RemoveObjectAt(i); + } else { + visitedSet->PutEntry(url); + i++; + } + } + + if (urls.Count() == 0) { + // There are no descendants to load so this request is ready. + aRequest->DependenciesLoaded(); + return; + } + + MOZ_ASSERT(aRequest->mAwaitingImports == 0); + aRequest->mAwaitingImports = urls.Count(); + + // For each url in urls, fetch a module script graph given url, module + // script's CORS setting, and module script's settings object. + for (auto* url : urls) { + StartFetchingModuleAndDependencies(aRequest, url); + } +} + +void ModuleLoaderBase::StartFetchingModuleAndDependencies( + ModuleLoadRequest* aParent, nsIURI* aURI) { + MOZ_ASSERT(aURI); + + RefPtr childRequest = CreateStaticImport(aURI, aParent); + + aParent->mImports.AppendElement(childRequest); + + if (LOG_ENABLED()) { + nsAutoCString url1; + aParent->mURI->GetAsciiSpec(url1); + + nsAutoCString url2; + aURI->GetAsciiSpec(url2); + + LOG(("ScriptLoadRequest (%p): Start fetching dependency %p", aParent, + childRequest.get())); + LOG(("StartFetchingModuleAndDependencies \"%s\" -> \"%s\"", url1.get(), + url2.get())); + } + + MOZ_ASSERT(!childRequest->mWaitingParentRequest); + childRequest->mWaitingParentRequest = aParent; + + nsresult rv = StartModuleLoad(childRequest); + if (NS_FAILED(rv)) { + MOZ_ASSERT(!childRequest->mModuleScript); + LOG(("ScriptLoadRequest (%p): rejecting %p", aParent, + childRequest.get())); + + mLoader->ReportErrorToConsole(childRequest, rv); + childRequest->LoadFailed(); + } +} + +void ModuleLoadRequest::ChildLoadComplete(bool aSuccess) { + RefPtr parent = mWaitingParentRequest; + MOZ_ASSERT(parent); + MOZ_ASSERT(parent->mAwaitingImports != 0); + + mWaitingParentRequest = nullptr; + parent->mAwaitingImports--; + + if (parent->IsFinished()) { + MOZ_ASSERT_IF(!aSuccess, parent->IsErrored()); + return; + } + + if (!aSuccess) { + parent->ModuleErrored(); + } else if (parent->mAwaitingImports == 0) { + parent->DependenciesLoaded(); + } +} + +nsresult ModuleLoaderBase::StartDynamicImport(ModuleLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->mLoader == this); + + LOG(("ScriptLoadRequest (%p): Start dynamic import", aRequest)); + + mDynamicImportRequests.AppendElement(aRequest); + + nsresult rv = StartModuleLoad(aRequest); + if (NS_FAILED(rv)) { + mLoader->ReportErrorToConsole(aRequest, rv); + FinishDynamicImportAndReject(aRequest, rv); + } + return rv; +} + +void ModuleLoaderBase::FinishDynamicImportAndReject(ModuleLoadRequest* aRequest, + nsresult aResult) { + AutoJSAPI jsapi; + MOZ_ASSERT(NS_FAILED(aResult)); + if (!jsapi.Init(mGlobalObject)) { + return; + } + + FinishDynamicImport(jsapi.cx(), aRequest, aResult, nullptr); +} + +/* static */ +void ModuleLoaderBase::FinishDynamicImport( + JSContext* aCx, ModuleLoadRequest* aRequest, nsresult aResult, + JS::Handle aEvaluationPromise) { + LOG(("ScriptLoadRequest (%p): Finish dynamic import %x %d", aRequest, + unsigned(aResult), JS_IsExceptionPending(aCx))); + + MOZ_ASSERT(GetCurrentModuleLoader(aCx) == aRequest->mLoader); + + // If aResult is a failed result, we don't have an EvaluationPromise. If it + // succeeded, evaluationPromise may still be null, but in this case it will + // be handled by rejecting the dynamic module import promise in the JSAPI. + MOZ_ASSERT_IF(NS_FAILED(aResult), !aEvaluationPromise); + + // Complete the dynamic import, report failures indicated by aResult or as a + // pending exception on the context. + + if (!aRequest->mDynamicPromise) { + // Import has already been completed. + return; + } + + if (NS_FAILED(aResult) && + aResult != NS_SUCCESS_DOM_SCRIPT_EVALUATION_THREW_UNCATCHABLE) { + MOZ_ASSERT(!JS_IsExceptionPending(aCx)); + nsAutoCString url; + aRequest->mURI->GetSpec(url); + JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr, + JSMSG_DYNAMIC_IMPORT_FAILED, url.get()); + } + + JS::Rooted referencingScript( + aCx, PrivateFromLoadedScript(aRequest->mDynamicReferencingScript)); + JS::Rooted specifier(aCx, aRequest->mDynamicSpecifier); + JS::Rooted promise(aCx, aRequest->mDynamicPromise); + + JS::Rooted moduleRequest(aCx, + JS::CreateModuleRequest(aCx, specifier)); + + JS::FinishDynamicModuleImport(aCx, aEvaluationPromise, referencingScript, + moduleRequest, promise); + + // FinishDynamicModuleImport clears any pending exception. + MOZ_ASSERT(!JS_IsExceptionPending(aCx)); + + aRequest->ClearDynamicImport(); +} + +ModuleLoaderBase::ModuleLoaderBase(ScriptLoaderInterface* aLoader, + nsIGlobalObject* aGlobalObject) + : mGlobalObject(aGlobalObject), mLoader(aLoader) { + MOZ_ASSERT(mGlobalObject); + MOZ_ASSERT(mLoader); + + EnsureModuleHooksInitialized(); +} + +ModuleLoaderBase::~ModuleLoaderBase() { + mDynamicImportRequests.CancelRequestsAndClear(); + + LOG(("ModuleLoaderBase::~ModuleLoaderBase %p", this)); +} + +void ModuleLoaderBase::Shutdown() { + CancelAndClearDynamicImports(); + + for (const auto& entry : mFetchingModules) { + RefPtr waitingRequests(entry.GetData()); + if (waitingRequests) { + ResumeWaitingRequests(waitingRequests, false); + } + } + + for (const auto& entry : mFetchedModules) { + if (entry.GetData()) { + entry.GetData()->Shutdown(); + } + } + + mFetchingModules.Clear(); + mFetchedModules.Clear(); + mGlobalObject = nullptr; + mLoader = nullptr; +} + +bool ModuleLoaderBase::HasFetchingModules() const { + return !mFetchingModules.IsEmpty(); +} + +bool ModuleLoaderBase::HasPendingDynamicImports() const { + return !mDynamicImportRequests.isEmpty(); +} + +void ModuleLoaderBase::CancelDynamicImport(ModuleLoadRequest* aRequest, + nsresult aResult) { + MOZ_ASSERT(aRequest->mLoader == this); + + RefPtr req = mDynamicImportRequests.Steal(aRequest); + if (!aRequest->IsCanceled()) { + aRequest->Cancel(); + // FinishDynamicImport must happen exactly once for each dynamic import + // request. If the load is aborted we do it when we remove the request + // from mDynamicImportRequests. + FinishDynamicImportAndReject(aRequest, aResult); + } +} + +void ModuleLoaderBase::RemoveDynamicImport(ModuleLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->IsDynamicImport()); + mDynamicImportRequests.Remove(aRequest); +} + +#ifdef DEBUG +bool ModuleLoaderBase::HasDynamicImport( + const ModuleLoadRequest* aRequest) const { + MOZ_ASSERT(aRequest->mLoader == this); + return mDynamicImportRequests.Contains( + const_cast(aRequest)); +} +#endif + +JS::Value ModuleLoaderBase::FindFirstParseError(ModuleLoadRequest* aRequest) { + MOZ_ASSERT(aRequest); + + ModuleScript* moduleScript = aRequest->mModuleScript; + MOZ_ASSERT(moduleScript); + + if (moduleScript->HasParseError()) { + return moduleScript->ParseError(); + } + + for (ModuleLoadRequest* childRequest : aRequest->mImports) { + JS::Value error = FindFirstParseError(childRequest); + if (!error.isUndefined()) { + return error; + } + } + + return JS::UndefinedValue(); +} + +bool ModuleLoaderBase::InstantiateModuleGraph(ModuleLoadRequest* aRequest) { + // Instantiate a top-level module and record any error. + + MOZ_ASSERT(aRequest); + MOZ_ASSERT(aRequest->mLoader == this); + MOZ_ASSERT(aRequest->IsTopLevel()); + + LOG(("ScriptLoadRequest (%p): Instantiate module graph", aRequest)); + + AUTO_PROFILER_LABEL("ModuleLoaderBase::InstantiateModuleGraph", JS); + + ModuleScript* moduleScript = aRequest->mModuleScript; + MOZ_ASSERT(moduleScript); + + JS::Value parseError = FindFirstParseError(aRequest); + if (!parseError.isUndefined()) { + moduleScript->SetErrorToRethrow(parseError); + LOG(("ScriptLoadRequest (%p): found parse error", aRequest)); + return true; + } + + MOZ_ASSERT(moduleScript->ModuleRecord()); + + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(mGlobalObject))) { + return false; + } + + JS::Rooted module(jsapi.cx(), moduleScript->ModuleRecord()); + if (!xpc::Scriptability::AllowedIfExists(module)) { + return true; + } + + if (!JS::ModuleLink(jsapi.cx(), module)) { + LOG(("ScriptLoadRequest (%p): Instantiate failed", aRequest)); + MOZ_ASSERT(jsapi.HasException()); + JS::RootedValue exception(jsapi.cx()); + if (!jsapi.StealException(&exception)) { + return false; + } + MOZ_ASSERT(!exception.isUndefined()); + moduleScript->SetErrorToRethrow(exception); + } + + return true; +} + +nsresult ModuleLoaderBase::InitDebuggerDataForModuleGraph( + JSContext* aCx, ModuleLoadRequest* aRequest) { + // JS scripts can be associated with a DOM element for use by the debugger, + // but preloading can cause scripts to be compiled before DOM script element + // nodes have been created. This method ensures that this association takes + // place before the first time a module script is run. + + MOZ_ASSERT(aRequest); + + ModuleScript* moduleScript = aRequest->mModuleScript; + if (moduleScript->DebuggerDataInitialized()) { + return NS_OK; + } + + for (ModuleLoadRequest* childRequest : aRequest->mImports) { + nsresult rv = InitDebuggerDataForModuleGraph(aCx, childRequest); + NS_ENSURE_SUCCESS(rv, rv); + } + + JS::Rooted module(aCx, moduleScript->ModuleRecord()); + MOZ_ASSERT(module); + + // The script is now ready to be exposed to the debugger. + JS::Rooted script(aCx, JS::GetModuleScript(module)); + JS::ExposeScriptToDebugger(aCx, script); + + moduleScript->SetDebuggerDataInitialized(); + return NS_OK; +} + +void ModuleLoaderBase::ProcessDynamicImport(ModuleLoadRequest* aRequest) { + if (!aRequest->mModuleScript) { + FinishDynamicImportAndReject(aRequest, NS_ERROR_FAILURE); + return; + } + + InstantiateAndEvaluateDynamicImport(aRequest); +} + +void ModuleLoaderBase::InstantiateAndEvaluateDynamicImport( + ModuleLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->mModuleScript); + + if (!InstantiateModuleGraph(aRequest)) { + aRequest->mModuleScript = nullptr; + } + + nsresult rv = NS_ERROR_FAILURE; + if (aRequest->mModuleScript) { + rv = EvaluateModule(aRequest); + } + + if (NS_FAILED(rv)) { + FinishDynamicImportAndReject(aRequest, rv); + } +} + +nsresult ModuleLoaderBase::EvaluateModule(ModuleLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->mLoader == this); + + mozilla::nsAutoMicroTask mt; + mozilla::dom::AutoEntryScript aes(mGlobalObject, "EvaluateModule", + NS_IsMainThread()); + + return EvaluateModuleInContext(aes.cx(), aRequest, + JS::ReportModuleErrorsAsync); +} + +nsresult ModuleLoaderBase::EvaluateModuleInContext( + JSContext* aCx, ModuleLoadRequest* aRequest, + JS::ModuleErrorBehaviour errorBehaviour) { + MOZ_ASSERT(aRequest->mLoader == this); + MOZ_ASSERT_IF(!mGlobalObject->GetModuleLoader(aCx)->IsOverridden(), + mGlobalObject->GetModuleLoader(aCx) == this); + MOZ_ASSERT_IF(mGlobalObject->GetModuleLoader(aCx)->IsOverridden(), + mGlobalObject->GetModuleLoader(aCx)->IsOverriddenBy(this)); + + AUTO_PROFILER_LABEL("ModuleLoaderBase::EvaluateModule", JS); + + nsAutoCString profilerLabelString; + if (aRequest->HasScriptLoadContext()) { + aRequest->GetScriptLoadContext()->GetProfilerLabel(profilerLabelString); + } + + LOG(("ScriptLoadRequest (%p): Evaluate Module", aRequest)); + AUTO_PROFILER_MARKER_TEXT("ModuleEvaluation", JS, + MarkerInnerWindowIdFromJSContext(aCx), + profilerLabelString); + + ModuleLoadRequest* request = aRequest->AsModuleRequest(); + MOZ_ASSERT(request->mModuleScript); + MOZ_ASSERT_IF(request->HasScriptLoadContext(), + !request->GetScriptLoadContext()->mCompileOrDecodeTask); + + ModuleScript* moduleScript = request->mModuleScript; + if (moduleScript->HasErrorToRethrow()) { + LOG(("ScriptLoadRequest (%p): module has error to rethrow", aRequest)); + JS::Rooted error(aCx, moduleScript->ErrorToRethrow()); + JS_SetPendingException(aCx, error); + // For a dynamic import, the promise is rejected. Otherwise an error + // is either reported by AutoEntryScript. + if (request->IsDynamicImport()) { + FinishDynamicImport(aCx, request, NS_OK, nullptr); + } + return NS_OK; + } + + JS::Rooted module(aCx, moduleScript->ModuleRecord()); + MOZ_ASSERT(module); + MOZ_ASSERT(CurrentGlobalOrNull(aCx) == GetNonCCWObjectGlobal(module)); + + if (!xpc::Scriptability::AllowedIfExists(module)) { + return NS_OK; + } + + nsresult rv = InitDebuggerDataForModuleGraph(aCx, request); + NS_ENSURE_SUCCESS(rv, rv); + + if (request->HasScriptLoadContext()) { + TRACE_FOR_TEST(aRequest->GetScriptLoadContext()->GetScriptElement(), + "scriptloader_evaluate_module"); + } + + JS::Rooted rval(aCx); + + mLoader->MaybePrepareModuleForBytecodeEncodingBeforeExecute(aCx, request); + + bool ok = JS::ModuleEvaluate(aCx, module, &rval); + + // ModuleEvaluate will usually set a pending exception if it returns false, + // unless the user cancels execution. + MOZ_ASSERT_IF(ok, !JS_IsExceptionPending(aCx)); + + if (!ok || IsModuleEvaluationAborted(request)) { + LOG(("ScriptLoadRequest (%p): evaluation failed", aRequest)); + // For a dynamic import, the promise is rejected. Otherwise an error is + // reported by AutoEntryScript. + rv = NS_ERROR_ABORT; + } + + // ModuleEvaluate returns a promise unless the user cancels the execution in + // which case rval will be undefined. We should treat it as a failed + // evaluation, and reject appropriately. + JS::Rooted evaluationPromise(aCx); + if (rval.isObject()) { + evaluationPromise.set(&rval.toObject()); + } + + if (request->IsDynamicImport()) { + if (NS_FAILED(rv)) { + FinishDynamicImportAndReject(request, rv); + } else { + FinishDynamicImport(aCx, request, NS_OK, evaluationPromise); + } + } else { + // If this is not a dynamic import, and if the promise is rejected, + // the value is unwrapped from the promise value. + if (!JS::ThrowOnModuleEvaluationFailure(aCx, evaluationPromise, + errorBehaviour)) { + LOG(("ScriptLoadRequest (%p): evaluation failed on throw", aRequest)); + // For a dynamic import, the promise is rejected. Otherwise an error is + // reported by AutoEntryScript. + } + } + + rv = mLoader->MaybePrepareModuleForBytecodeEncodingAfterExecute(request, + NS_OK); + + mLoader->MaybeTriggerBytecodeEncoding(); + + return rv; +} + +void ModuleLoaderBase::CancelAndClearDynamicImports() { + while (ScriptLoadRequest* req = mDynamicImportRequests.getFirst()) { + // This also removes the request from the list. + CancelDynamicImport(req->AsModuleRequest(), NS_ERROR_ABORT); + } +} + +UniquePtr ModuleLoaderBase::ParseImportMap( + ScriptLoadRequest* aRequest) { + AutoJSAPI jsapi; + if (!jsapi.Init(GetGlobalObject())) { + return nullptr; + } + + MOZ_ASSERT(aRequest->IsTextSource()); + MaybeSourceText maybeSource; + nsresult rv = aRequest->GetScriptSource(jsapi.cx(), &maybeSource, + aRequest->mLoadContext.get()); + if (NS_FAILED(rv)) { + return nullptr; + } + + JS::SourceText& text = maybeSource.ref>(); + ReportWarningHelper warning{mLoader, aRequest}; + + // https://html.spec.whatwg.org/multipage/webappapis.html#create-an-import-map-parse-result + // Step 2. Parse an import map string given input and baseURL, catching any + // exceptions. If this threw an exception, then set result's error to rethrow + // to that exception. Otherwise, set result's import map to the return value. + // + // https://html.spec.whatwg.org/multipage/webappapis.html#register-an-import-map + // Step 1. If result's error to rethrow is not null, then report the exception + // given by result's error to rethrow and return. + // + // Impl note: We didn't implement 'Import map parse result' from the spec, + // https://html.spec.whatwg.org/multipage/webappapis.html#import-map-parse-result + // As the struct has another item called 'error to rethow' to store the + // exception thrown during parsing import-maps, and report that exception + // while registering an import map. Currently only inline import-maps are + // supported, therefore parsing and registering import-maps will be executed + // consecutively. To simplify the implementation, we didn't create the 'error + // to rethow' item and report the exception immediately(done in ~AutoJSAPI). + return ImportMap::ParseString(jsapi.cx(), text, aRequest->mBaseURL, warning); +} + +void ModuleLoaderBase::RegisterImportMap(UniquePtr aImportMap) { + // Check for aImportMap is done in ScriptLoader. + MOZ_ASSERT(aImportMap); + + // https://html.spec.whatwg.org/multipage/webappapis.html#register-an-import-map + // The step 1(report the exception if there's an error) is done in + // ParseImportMap. + // + // Step 2. Assert: global's import map is an empty import map. + // Impl note: The default import map from the spec is an empty import map, but + // from the implementation it defaults to nullptr, so we check if the global's + // import map is null here. + // + // Also see + // https://html.spec.whatwg.org/multipage/webappapis.html#empty-import-map + MOZ_ASSERT(!mImportMap); + + // Step 3. Set global's import map to result's import map. + mImportMap = std::move(aImportMap); +} + +void ModuleLoaderBase::CopyModulesTo(ModuleLoaderBase* aDest) { + MOZ_ASSERT(aDest->mFetchingModules.IsEmpty()); + MOZ_ASSERT(aDest->mFetchedModules.IsEmpty()); + MOZ_ASSERT(mFetchingModules.IsEmpty()); + + for (const auto& entry : mFetchedModules) { + RefPtr moduleScript = entry.GetData(); + if (!moduleScript) { + continue; + } + aDest->mFetchedModules.InsertOrUpdate(entry.GetKey(), moduleScript); + } +} + +void ModuleLoaderBase::MoveModulesTo(ModuleLoaderBase* aDest) { + MOZ_ASSERT(mFetchingModules.IsEmpty()); + MOZ_ASSERT(aDest->mFetchingModules.IsEmpty()); + + for (const auto& entry : mFetchedModules) { + RefPtr moduleScript = entry.GetData(); + if (!moduleScript) { + continue; + } + +#ifdef DEBUG + if (auto existingEntry = aDest->mFetchedModules.Lookup(entry.GetKey())) { + MOZ_ASSERT(moduleScript == existingEntry.Data()); + } +#endif + + aDest->mFetchedModules.InsertOrUpdate(entry.GetKey(), moduleScript); + } + + mFetchedModules.Clear(); +} + +#undef LOG +#undef LOG_ENABLED + +} // namespace JS::loader diff --git a/js/loader/ModuleLoaderBase.h b/js/loader/ModuleLoaderBase.h new file mode 100644 index 0000000000..2c2c385a30 --- /dev/null +++ b/js/loader/ModuleLoaderBase.h @@ -0,0 +1,503 @@ +/* -*- 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 js_loader_ModuleLoaderBase_h +#define js_loader_ModuleLoaderBase_h + +#include "LoadedScript.h" +#include "ScriptLoadRequest.h" + +#include "ImportMap.h" +#include "js/ColumnNumber.h" // JS::ColumnNumberOneOrigin +#include "js/TypeDecls.h" // JS::MutableHandle, JS::Handle, JS::Root +#include "js/Modules.h" +#include "nsRefPtrHashtable.h" +#include "nsCOMArray.h" +#include "nsCOMPtr.h" +#include "nsILoadInfo.h" // nsSecurityFlags +#include "nsINode.h" // nsIURI +#include "nsThreadUtils.h" // GetMainThreadSerialEventTarget +#include "nsURIHashKey.h" +#include "mozilla/Attributes.h" // MOZ_RAII +#include "mozilla/CORSMode.h" +#include "mozilla/dom/JSExecutionContext.h" +#include "mozilla/MaybeOneOf.h" +#include "mozilla/UniquePtr.h" +#include "ResolveResult.h" + +class nsIURI; + +namespace mozilla { + +class LazyLogModule; +union Utf8Unit; + +} // namespace mozilla + +namespace JS { + +class CompileOptions; + +template +class SourceText; + +namespace loader { + +class ModuleLoaderBase; +class ModuleLoadRequest; +class ModuleScript; + +/* + * [DOMDOC] Shared Classic/Module Script Methods + * + * The ScriptLoaderInterface defines the shared methods needed by both + * ScriptLoaders (loading classic scripts) and ModuleLoaders (loading module + * scripts). These include: + * + * * Error Logging + * * Generating the compile options + * * Optional: Bytecode Encoding + * + * ScriptLoaderInterface does not provide any implementations. + * It enables the ModuleLoaderBase to reference back to the behavior implemented + * by a given ScriptLoader. + * + * Not all methods will be used by all ModuleLoaders. For example, Bytecode + * Encoding does not apply to workers, as we only work with source text there. + * Fully virtual methods are implemented by all. + * + */ + +class ScriptLoaderInterface : public nsISupports { + public: + // alias common classes + using ScriptFetchOptions = JS::loader::ScriptFetchOptions; + using ScriptKind = JS::loader::ScriptKind; + using ScriptLoadRequest = JS::loader::ScriptLoadRequest; + using ScriptLoadRequestList = JS::loader::ScriptLoadRequestList; + using ModuleLoadRequest = JS::loader::ModuleLoadRequest; + + virtual ~ScriptLoaderInterface() = default; + + // In some environments, we will need to default to a base URI + virtual nsIURI* GetBaseURI() const = 0; + + virtual void ReportErrorToConsole(ScriptLoadRequest* aRequest, + nsresult aResult) const = 0; + + virtual void ReportWarningToConsole( + ScriptLoadRequest* aRequest, const char* aMessageName, + const nsTArray& aParams = nsTArray()) const = 0; + + // Fill in CompileOptions, as well as produce the introducer script for + // subsequent calls to UpdateDebuggerMetadata + virtual nsresult FillCompileOptionsForRequest( + JSContext* cx, ScriptLoadRequest* aRequest, JS::CompileOptions* aOptions, + JS::MutableHandle aIntroductionScript) = 0; + + virtual void MaybePrepareModuleForBytecodeEncodingBeforeExecute( + JSContext* aCx, ModuleLoadRequest* aRequest) {} + + virtual nsresult MaybePrepareModuleForBytecodeEncodingAfterExecute( + ModuleLoadRequest* aRequest, nsresult aRv) { + return NS_OK; + } + + virtual void MaybeTriggerBytecodeEncoding() {} +}; + +/* + * [DOMDOC] Module Loading + * + * ModuleLoaderBase provides support for loading module graphs as defined in the + * EcmaScript specification. A derived module loader class must be created for a + * specific use case (for example loading HTML module scripts). The derived + * class provides operations such as fetching of source code and scheduling of + * module execution. + * + * Module loading works in terms of 'requests' which hold data about modules as + * they move through the loading process. There may be more than one load + * request active for a single module URI, but the module is only loaded + * once. This is achieved by tracking all fetching and fetched modules in the + * module map. + * + * The module map is made up of two parts. A module that has been requested but + * has not finished fetching is represented by an entry in the mFetchingModules + * map. A module which has been fetched and compiled is represented by a + * ModuleScript in the mFetchedModules map. + * + * Module loading typically works as follows: + * + * 1. The client ensures there is an instance of the derived module loader + * class for its global or creates one if necessary. + * + * 2. The client creates a ModuleLoadRequest object for the module to load and + * calls the loader's StartModuleLoad() method. This is a top-level request, + * i.e. not an import. + * + * 3. The module loader calls the virtual method CanStartLoad() to check + * whether the request should be loaded. + * + * 4. If the module is not already present in the module map, the loader calls + * the virtual method StartFetch() to set up an asynchronous operation to + * fetch the module source. + * + * 5. When the fetch operation is complete, the derived loader calls + * OnFetchComplete() passing an error code to indicate success or failure. + * + * 6. On success, the loader attempts to create a module script by calling the + * virtual CompileFetchedModule() method. + * + * 7. If compilation is successful, the loader creates load requests for any + * imported modules if present. If so, the process repeats from step 3. + * + * 8. When a load request is completed, the virtual OnModuleLoadComplete() + * method is called. This is called for the top-level request and import + * requests. + * + * 9. The client calls InstantiateModuleGraph() for the top-level request. This + * links the loaded module graph. + * + * 10. The client calls EvaluateModule() to execute the top-level module. + */ +class ModuleLoaderBase : public nsISupports { + /* + * The set of requests that are waiting for an ongoing fetch to complete. + */ + class WaitingRequests final : public nsISupports { + virtual ~WaitingRequests() = default; + + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(WaitingRequests) + + nsTArray> mWaiting; + }; + + // Module map + nsRefPtrHashtable mFetchingModules; + nsRefPtrHashtable mFetchedModules; + + // List of dynamic imports that are currently being loaded. + ScriptLoadRequestList mDynamicImportRequests; + + nsCOMPtr mGlobalObject; + + // If non-null, this module loader is overridden by the module loader pointed + // by mOverriddenBy. + // See ModuleLoaderBase::GetCurrentModuleLoader for more details. + RefPtr mOverriddenBy; + + // https://html.spec.whatwg.org/multipage/webappapis.html#import-maps-allowed + // + // Each Window has an import maps allowed boolean, initially true. + bool mImportMapsAllowed = true; + + protected: + RefPtr mLoader; + + mozilla::UniquePtr mImportMap; + + virtual ~ModuleLoaderBase(); + +#ifdef DEBUG + const ScriptLoadRequestList& DynamicImportRequests() const { + return mDynamicImportRequests; + } +#endif + + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(ModuleLoaderBase) + explicit ModuleLoaderBase(ScriptLoaderInterface* aLoader, + nsIGlobalObject* aGlobalObject); + + // Called to break cycles during shutdown to prevent memory leaks. + void Shutdown(); + + virtual nsIURI* GetBaseURI() const { return mLoader->GetBaseURI(); }; + + using LoadedScript = JS::loader::LoadedScript; + using ScriptFetchOptions = JS::loader::ScriptFetchOptions; + using ScriptLoadRequest = JS::loader::ScriptLoadRequest; + using ModuleLoadRequest = JS::loader::ModuleLoadRequest; + + using MaybeSourceText = + mozilla::MaybeOneOf, JS::SourceText>; + + // Methods that must be implemented by an extending class. These are called + // internally by ModuleLoaderBase. + + private: + // Create a module load request for a static module import. + virtual already_AddRefed CreateStaticImport( + nsIURI* aURI, ModuleLoadRequest* aParent) = 0; + + // Called by HostImportModuleDynamically hook. + virtual already_AddRefed CreateDynamicImport( + JSContext* aCx, nsIURI* aURI, LoadedScript* aMaybeActiveScript, + JS::Handle aSpecifier, JS::Handle aPromise) = 0; + + // Called when dynamic import started successfully. + virtual void OnDynamicImportStarted(ModuleLoadRequest* aRequest) {} + + // Check whether we can load a module. May return false with |aRvOut| set to + // NS_OK to abort load without returning an error. + virtual bool CanStartLoad(ModuleLoadRequest* aRequest, nsresult* aRvOut) = 0; + + // Start the process of fetching module source (or bytecode). This is only + // called if CanStartLoad returned true. + virtual nsresult StartFetch(ModuleLoadRequest* aRequest) = 0; + + // Create a JS module for a fetched module request. This might compile source + // text or decode cached bytecode. + virtual nsresult CompileFetchedModule( + JSContext* aCx, JS::Handle aGlobal, + JS::CompileOptions& aOptions, ModuleLoadRequest* aRequest, + JS::MutableHandle aModuleOut) = 0; + + // Called when a module script has been loaded, including imports. + virtual void OnModuleLoadComplete(ModuleLoadRequest* aRequest) = 0; + + virtual bool IsModuleEvaluationAborted(ModuleLoadRequest* aRequest) { + return false; + } + + // Get the error message when resolving failed. The default is to call + // nsContentUtils::FormatLoalizedString. But currently + // nsContentUtils::FormatLoalizedString cannot be called on a worklet thread, + // see bug 1808301. So WorkletModuleLoader will override this function to + // get the error message. + virtual nsresult GetResolveFailureMessage(ResolveError aError, + const nsAString& aSpecifier, + nsAString& aResult); + + // Public API methods. + + public: + ScriptLoaderInterface* GetScriptLoaderInterface() const { return mLoader; } + + nsIGlobalObject* GetGlobalObject() const { return mGlobalObject; } + + bool HasFetchingModules() const; + + bool HasPendingDynamicImports() const; + void CancelDynamicImport(ModuleLoadRequest* aRequest, nsresult aResult); +#ifdef DEBUG + bool HasDynamicImport(const ModuleLoadRequest* aRequest) const; +#endif + + // Start a load for a module script URI. Returns immediately if the module is + // already being loaded. + nsresult StartModuleLoad(ModuleLoadRequest* aRequest); + nsresult RestartModuleLoad(ModuleLoadRequest* aRequest); + + // Notify the module loader when a fetch started by StartFetch() completes. + nsresult OnFetchComplete(ModuleLoadRequest* aRequest, nsresult aRv); + + // Link the module and all its imports. This must occur prior to evaluation. + bool InstantiateModuleGraph(ModuleLoadRequest* aRequest); + + // Executes the module. + // Implements https://html.spec.whatwg.org/#run-a-module-script + nsresult EvaluateModule(ModuleLoadRequest* aRequest); + + // Evaluate a module in the given context. Does not push an entry to the + // execution stack. + nsresult EvaluateModuleInContext(JSContext* aCx, ModuleLoadRequest* aRequest, + JS::ModuleErrorBehaviour errorBehaviour); + + nsresult StartDynamicImport(ModuleLoadRequest* aRequest); + void ProcessDynamicImport(ModuleLoadRequest* aRequest); + void CancelAndClearDynamicImports(); + + // Process