diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /js/loader | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/loader')
-rw-r--r-- | js/loader/ImportMap.cpp | 697 | ||||
-rw-r--r-- | js/loader/ImportMap.h | 118 | ||||
-rw-r--r-- | js/loader/LoadContextBase.cpp | 54 | ||||
-rw-r--r-- | js/loader/LoadContextBase.h | 69 | ||||
-rw-r--r-- | js/loader/LoadedScript.cpp | 211 | ||||
-rw-r--r-- | js/loader/LoadedScript.h | 119 | ||||
-rw-r--r-- | js/loader/ModuleLoadRequest.cpp | 207 | ||||
-rw-r--r-- | js/loader/ModuleLoadRequest.h | 164 | ||||
-rw-r--r-- | js/loader/ModuleLoaderBase.cpp | 1317 | ||||
-rw-r--r-- | js/loader/ModuleLoaderBase.h | 420 | ||||
-rw-r--r-- | js/loader/ResolveResult.h | 54 | ||||
-rw-r--r-- | js/loader/ScriptKind.h | 16 | ||||
-rw-r--r-- | js/loader/ScriptLoadRequest.cpp | 283 | ||||
-rw-r--r-- | js/loader/ScriptLoadRequest.h | 425 | ||||
-rw-r--r-- | js/loader/moz.build | 29 |
15 files changed, 4183 insertions, 0 deletions
diff --git a/js/loader/ImportMap.cpp b/js/loader/ImportMap.cpp new file mode 100644 index 0000000000..bf9eafe8e6 --- /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<nsString>& 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<nsIURI> 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<nsIURI> 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<SpecifierMap> SortAndNormalizeSpecifierMap( + JSContext* aCx, JS::HandleObject aOriginalMap, nsIURI* aBaseURL, + const ReportWarningHelper& aWarning) { + // Step 1. Let normalized be an empty ordered map. + UniquePtr<SpecifierMap> normalized = MakeUnique<SpecifierMap>(); + + JS::Rooted<JS::IdVector> 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<nsString, 1> 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<nsIURI> 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<nsString, 2> 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<ScopeMap> SortAndNormalizeScopes( + JSContext* aCx, JS::HandleObject aOriginalMap, nsIURI* aBaseURL, + const ReportWarningHelper& aWarning) { + JS::Rooted<JS::IdVector> scopeKeys(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, aOriginalMap, &scopeKeys)) { + return nullptr; + } + + // Step 1. Let normalized be an empty map. + UniquePtr<ScopeMap> normalized = MakeUnique<ScopeMap>(); + + // 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<nsIURI> 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<nsString, 1> 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> 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> ImportMap::ParseString( + JSContext* aCx, SourceText<char16_t>& aInput, nsIURI* aBaseURL, + const ReportWarningHelper& aWarning) { + // Step 1. Let parsed be the result of parsing JSON into Infra values given + // input. + JS::Rooted<JS::Value> 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<JS::Value> exn(aCx); + if (!JS_GetPendingException(aCx, &exn)) { + return nullptr; + } + MOZ_ASSERT(exn.isObject()); + JS::Rooted<JSObject*> 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<SpecifierMap> 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<ScopeMap> 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<JS::IdVector> 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<nsString, 1> 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<SpecifierMap>(); + } + if (!sortedAndNormalizedScopes) { + sortedAndNormalizedScopes = MakeUnique<ScopeMap>(); + } + + // Step 8. Return an import map whose imports are + // sortedAndNormalizedImports and whose scopes scopes are + // sortedAndNormalizedScopes. + return MakeUnique<ImportMap>(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<nsCOMPtr<nsIURI>, 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<nsIURI> 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<nsIURI>(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<nsIURI> baseURL; + if (aScript) { + 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<nsIURI> 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<nsIURI> 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<nsIURI> 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 <functional> +#include <map> + +#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<nsString>& aParams = nsTArray<nsString>()) const; + + private: + RefPtr<ScriptLoaderInterface> mLoader; + ScriptLoadRequest* mRequest; +}; + +// Specifier map from import maps. +// https://html.spec.whatwg.org/multipage/webappapis.html#module-specifier-map +using SpecifierMap = + std::map<nsString, nsCOMPtr<nsIURI>, std::greater<nsString>>; + +// Scope map from import maps. +// https://html.spec.whatwg.org/multipage/webappapis.html#concept-import-map-scopes +using ScopeMap = std::map<nsCString, mozilla::UniquePtr<SpecifierMap>, + std::greater<nsCString>>; + +/** + * Implementation of Import maps. + * https://html.spec.whatwg.org/multipage/webappapis.html#import-maps + */ +class ImportMap { + public: + ImportMap(mozilla::UniquePtr<SpecifierMap> aImports, + mozilla::UniquePtr<ScopeMap> 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<ImportMap> ParseString( + JSContext* aCx, JS::SourceText<char16_t>& 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<SpecifierMap> mImports; + mozilla::UniquePtr<ScopeMap> 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..980da9b510 --- /dev/null +++ b/js/loader/LoadContextBase.cpp @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/ScriptLoadContext.h" +#include "mozilla/loader/ComponentModuleLoader.h" +#include "mozilla/dom/WorkerLoadContext.h" +#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<mozilla::dom::ScriptLoadContext*>(this); +} + +mozilla::loader::ComponentLoadContext* LoadContextBase::AsComponentContext() { + MOZ_ASSERT(IsComponentContext()); + return static_cast<mozilla::loader::ComponentLoadContext*>(this); +} + +mozilla::dom::WorkerLoadContext* LoadContextBase::AsWorkerContext() { + MOZ_ASSERT(IsWorkerContext()); + return static_cast<mozilla::dom::WorkerLoadContext*>(this); +} + +} // namespace JS::loader diff --git a/js/loader/LoadContextBase.h b/js/loader/LoadContextBase.h new file mode 100644 index 0000000000..3753209dd4 --- /dev/null +++ b/js/loader/LoadContextBase.h @@ -0,0 +1,69 @@ +/* -*- 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; +} // namespace mozilla::dom + +namespace mozilla::loader { +class ComponentLoadContext; +} + +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, Component, Worker }; + +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 IsComponentContext() const { return mKind == ContextKind::Component; } + mozilla::loader::ComponentLoadContext* AsComponentContext(); + + bool IsWorkerContext() const { return mKind == ContextKind::Worker; } + mozilla::dom::WorkerLoadContext* AsWorkerContext(); + + RefPtr<JS::loader::ScriptLoadRequest> 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..d117ab55b4 --- /dev/null +++ b/js/loader/LoadedScript.cpp @@ -0,0 +1,211 @@ +/* -*- 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 "jsfriendapi.h" +#include "js/Modules.h" // JS::{Get,Set}ModulePrivate + +namespace JS::loader { + +////////////////////////////////////////////////////////////// +// LoadedScript +////////////////////////////////////////////////////////////// + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LoadedScript) +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_COLLECTION_TRACE_BEGIN(LoadedScript) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(LoadedScript) +NS_IMPL_CYCLE_COLLECTING_RELEASE(LoadedScript) + +LoadedScript::LoadedScript(ScriptKind aKind, ScriptFetchOptions* aFetchOptions, + nsIURI* aBaseURL) + : mKind(aKind), mFetchOptions(aFetchOptions), mBaseURL(aBaseURL) { + MOZ_ASSERT(mFetchOptions); + MOZ_ASSERT(mBaseURL); +} + +LoadedScript::~LoadedScript() { mozilla::DropJSObjects(this); } + +void LoadedScript::AssociateWithScript(JSScript* aScript) { + // 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)); +} + +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<LoadedScript*>(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<LoadedScript*>(aPrivate.toPrivate()); + CheckModuleScriptPrivate(script, aPrivate); + script->Release(); +} + +////////////////////////////////////////////////////////////// +// EventScript +////////////////////////////////////////////////////////////// + +EventScript::EventScript(ScriptFetchOptions* aFetchOptions, nsIURI* aBaseURL) + : LoadedScript(ScriptKind::eEvent, aFetchOptions, aBaseURL) {} + +////////////////////////////////////////////////////////////// +// ClassicScript +////////////////////////////////////////////////////////////// + +ClassicScript::ClassicScript(ScriptFetchOptions* aFetchOptions, + nsIURI* aBaseURL) + : LoadedScript(ScriptKind::eClassic, aFetchOptions, aBaseURL) {} + +////////////////////////////////////////////////////////////// +// ModuleScript +////////////////////////////////////////////////////////////// + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ModuleScript) +NS_INTERFACE_MAP_END_INHERITING(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 + +NS_IMPL_ADDREF_INHERITED(ModuleScript, LoadedScript) +NS_IMPL_RELEASE_INHERITED(ModuleScript, LoadedScript) + +ModuleScript::ModuleScript(ScriptFetchOptions* aFetchOptions, nsIURI* aBaseURL) + : LoadedScript(ScriptKind::eModule, aFetchOptions, aBaseURL), + 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<JSObject*> 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..a61884f4a2 --- /dev/null +++ b/js/loader/LoadedScript.h @@ -0,0 +1,119 @@ +/* -*- 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 "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "jsapi.h" +#include "ScriptLoadRequest.h" + +class nsIURI; + +namespace JS::loader { + +void HostAddRefTopLevelScript(const JS::Value& aPrivate); +void HostReleaseTopLevelScript(const JS::Value& aPrivate); + +class ClassicScript; +class ModuleScript; +class EventScript; + +class LoadedScript : public nsISupports { + ScriptKind mKind; + RefPtr<ScriptFetchOptions> mFetchOptions; + nsCOMPtr<nsIURI> mBaseURL; + + protected: + LoadedScript(ScriptKind aKind, ScriptFetchOptions* aFetchOptions, + nsIURI* aBaseURL); + + virtual ~LoadedScript(); + + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(LoadedScript) + + 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; } + + nsIURI* BaseURL() const { return mBaseURL; } + + void AssociateWithScript(JSScript* aScript); +}; + +class ClassicScript final : public LoadedScript { + ~ClassicScript() = default; + + public: + ClassicScript(ScriptFetchOptions* aFetchOptions, nsIURI* aBaseURL); +}; + +class EventScript final : public LoadedScript { + ~EventScript() = default; + + public: + EventScript(ScriptFetchOptions* aFetchOptions, nsIURI* aBaseURL); +}; + +// A single module script. May be used to satisfy multiple load requests. + +class ModuleScript final : public LoadedScript { + JS::Heap<JSObject*> mModuleRecord; + JS::Heap<JS::Value> mParseError; + JS::Heap<JS::Value> mErrorToRethrow; + bool mDebuggerDataInitialized; + + ~ModuleScript(); + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(ModuleScript, + LoadedScript) + + ModuleScript(ScriptFetchOptions* aFetchOptions, nsIURI* aBaseURL); + + void SetModuleRecord(JS::Handle<JSObject*> 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<ClassicScript*>(this); +} + +ModuleScript* LoadedScript::AsModuleScript() { + MOZ_ASSERT(IsModuleScript()); + return static_cast<ModuleScript*>(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..c86b91b34d --- /dev/null +++ b/js/loader/ModuleLoadRequest.cpp @@ -0,0 +1,207 @@ +/* -*- 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_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ModuleLoadRequest) +NS_INTERFACE_MAP_END_INHERITING(ScriptLoadRequest) + +NS_IMPL_CYCLE_COLLECTION_CLASS(ModuleLoadRequest) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(ModuleLoadRequest, + ScriptLoadRequest) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mLoader, mModuleScript, mImports, mRootModule) + tmp->ClearDynamicImport(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(ModuleLoadRequest, + ScriptLoadRequest) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLoader, mModuleScript, mImports, + mRootModule) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(ModuleLoadRequest, + ScriptLoadRequest) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mDynamicReferencingPrivate) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mDynamicSpecifier) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mDynamicPromise) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_ADDREF_INHERITED(ModuleLoadRequest, ScriptLoadRequest) +NS_IMPL_RELEASE_INHERITED(ModuleLoadRequest, ScriptLoadRequest) + +/* static */ +VisitedURLSet* ModuleLoadRequest::NewVisitedSetForTopLevelImport(nsIURI* aURI) { + auto set = new VisitedURLSet(); + set->PutEntry(aURI); + return set; +} + +ModuleLoadRequest::ModuleLoadRequest( + nsIURI* aURI, 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, aFetchOptions, aIntegrity, + aReferrer, aContext), + mIsTopLevel(aIsTopLevel), + mIsDynamicImport(aIsDynamicImport), + mLoader(aLoader), + mRootModule(aRootModule), + mVisitedSet(aVisitedSet) { + MOZ_ASSERT(mLoader); +} + +nsIGlobalObject* ModuleLoadRequest::GetGlobalObject() { + return mLoader->GetGlobalObject(); +} + +void ModuleLoadRequest::Cancel() { + ScriptLoadRequest::Cancel(); + mModuleScript = nullptr; + CancelImports(); + mReady.RejectIfExists(NS_ERROR_DOM_ABORT_ERR, __func__); +} + +void ModuleLoadRequest::CancelImports() { + for (size_t i = 0; i < mImports.Length(); i++) { + mImports[i]->Cancel(); + } +} + +void ModuleLoadRequest::SetReady() { + // 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. + // + // The mReady promise is used to ensure that when all dependencies of a module + // have become ready, DependenciesLoaded is called on that module + // request. This is set up in StartFetchingModuleDependencies. + +#ifdef DEBUG + for (size_t i = 0; i < mImports.Length(); i++) { + MOZ_ASSERT(mImports[i]->IsReadyToRun()); + } +#endif + + ScriptLoadRequest::SetReady(); + mReady.ResolveIfExists(true, __func__); +} + +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)); + + mModuleScript = mLoader->GetFetchedModule(mURI); + if (!mModuleScript || mModuleScript->HasParseError()) { + ModuleErrored(); + return; + } + + mLoader->StartFetchingModuleDependencies(this); +} + +void ModuleLoadRequest::ModuleErrored() { + if (IsCanceled()) { + return; + } + + LOG(("ScriptLoadRequest (%p): Module errored", this)); + + CheckModuleDependenciesLoaded(); + MOZ_ASSERT(!mModuleScript || mModuleScript->HasParseError()); + + CancelImports(); + SetReady(); + LoadFinished(); +} + +void ModuleLoadRequest::DependenciesLoaded() { + if (IsCanceled()) { + return; + } + + // The module and all of its dependencies have been successfully fetched and + // compiled. + + LOG(("ScriptLoadRequest (%p): Module dependencies loaded", this)); + + MOZ_ASSERT(mModuleScript); + + 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::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()) { + return; + } + + MOZ_ASSERT(!IsReadyToRun()); + MOZ_ASSERT(!mModuleScript); + + Cancel(); + LoadFinished(); +} + +void ModuleLoadRequest::LoadFinished() { + RefPtr<ModuleLoadRequest> request(this); + if (IsTopLevel() && IsDynamicImport()) { + mLoader->RemoveDynamicImport(request); + } + + mLoader->OnModuleLoadComplete(request); +} + +void ModuleLoadRequest::ClearDynamicImport() { + mDynamicReferencingPrivate = JS::UndefinedValue(); + mDynamicSpecifier = nullptr; + mDynamicPromise = nullptr; +} + +} // namespace JS::loader diff --git a/js/loader/ModuleLoadRequest.h b/js/loader/ModuleLoadRequest.h new file mode 100644 index 0000000000..249ca65a44 --- /dev/null +++ b/js/loader/ModuleLoadRequest.h @@ -0,0 +1,164 @@ +/* -*- 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 "mozilla/MozPromise.h" +#include "js/RootingAPI.h" +#include "js/Value.h" +#include "nsURIHashKey.h" +#include "nsTHashtable.h" + +namespace JS::loader { + +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<nsURIHashKey> { + 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() = default; + + 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; + + template <typename T> + using MozPromiseHolder = mozilla::MozPromiseHolder<T>; + using GenericPromise = mozilla::GenericPromise; + + ModuleLoadRequest(nsIURI* aURI, 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; } + + nsIGlobalObject* GetGlobalObject(); + + void SetReady() override; + void Cancel() override; + 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); } + + private: + void LoadFinished(); + void CancelImports(); + void CheckModuleDependenciesLoaded(); + + 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<ModuleLoaderBase> mLoader; + + // Pointer to the top level module of this module graph, nullptr if this is a + // top level module + RefPtr<ModuleLoadRequest> mRootModule; + + // Set to a module script object after a successful load or nullptr on + // failure. + RefPtr<ModuleScript> mModuleScript; + + // A promise that is completed on successful load of this module and all of + // its dependencies, indicating that the module is ready for instantiation and + // evaluation. + MozPromiseHolder<GenericPromise> mReady; + + // Array of imported modules. + nsTArray<RefPtr<ModuleLoadRequest>> mImports; + + // Set of module URLs visited while fetching the module graph this request is + // part of. + RefPtr<VisitedURLSet> mVisitedSet; + + // For dynamic imports, the details to pass to FinishDynamicImport. + JS::Heap<JS::Value> mDynamicReferencingPrivate; + JS::Heap<JSString*> mDynamicSpecifier; + JS::Heap<JSObject*> 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..784ae26322 --- /dev/null +++ b/js/loader/ModuleLoaderBase.cpp @@ -0,0 +1,1317 @@ +/* -*- 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/ContextOptions.h" // JS::ContextOptionsRef +#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/OffThreadScriptCompilation.h" +#include "js/PropertyAndElement.h" // JS_DefineProperty, JS_GetElement +#include "js/SourceText.h" +#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/StaticPrefs_dom.h" +#include "nsContentUtils.h" +#include "nsICacheInfoChannel.h" // nsICacheInfoChannel +#include "nsNetUtil.h" // NS_NewURI +#include "xpcpublic.h" + +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 +////////////////////////////////////////////////////////////// + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ModuleLoaderBase) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION(ModuleLoaderBase, mFetchedModules, + mDynamicImportRequests, mGlobalObject, mEventTarget, + 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); + + JS::ImportAssertionVector assertions; + // ImportAssertionVector has inline storage for one element so this cannot + // fail. + MOZ_ALWAYS_TRUE(assertions.reserve(1)); + assertions.infallibleAppend(JS::ImportAssertion::Type); + JS::SetSupportedImportAssertions(rt, assertions); +} + +// 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<JS::Value> aReferencingPrivate, + JS::Handle<JSObject*> aModuleRequest) { + JS::Rooted<JSObject*> module(aCx); + + { + // LoadedScript should only live in this block, otherwise it will be a GC + // hazard + RefPtr<LoadedScript> script( + GetLoadedScriptOrNull(aCx, aReferencingPrivate)); + + JS::Rooted<JSString*> 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<ModuleLoaderBase> 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<nsIURI> 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<JS::Value> aReferencingPrivate, + JS::Handle<JSString*> aSpecifier) { + RootedString urlString(aCx); + + { + // ModuleScript should only live in this block, otherwise it will be a GC + // hazard + RefPtr<ModuleScript> script = + static_cast<ModuleScript*>(aReferencingPrivate.toPrivate()); + MOZ_ASSERT(script->IsModuleScript()); + MOZ_ASSERT(JS::GetModulePrivate(script->ModuleRecord()) == + aReferencingPrivate); + + RefPtr<ModuleLoaderBase> 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<JS::Value> error(aCx); + nsresult rv = HandleResolveFailure(aCx, script, specifier, + result.unwrapErr(), 0, 0, &error); + if (NS_FAILED(rv)) { + JS_ReportOutOfMemory(aCx); + return nullptr; + } + + JS_SetPendingException(aCx, error); + + return nullptr; + } + + nsCOMPtr<nsIURI> 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<JS::Value> aReferencingPrivate, + JS::Handle<JSObject*> aMetaObject) { + RefPtr<ModuleScript> script = + static_cast<ModuleScript*>(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<JSString*> 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<JS::Value> aReferencingPrivate, + JS::Handle<JSObject*> aModuleRequest, JS::Handle<JSObject*> aPromise) { + MOZ_DIAGNOSTIC_ASSERT(aModuleRequest); + MOZ_DIAGNOSTIC_ASSERT(aPromise); + + RefPtr<LoadedScript> script(GetLoadedScriptOrNull(aCx, aReferencingPrivate)); + + JS::Rooted<JSString*> 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<ModuleLoaderBase> loader = GetCurrentModuleLoader(aCx); + if (!loader) { + return false; + } + + auto result = loader->ResolveModuleSpecifier(script, specifier); + if (result.isErr()) { + JS::Rooted<JS::Value> error(aCx); + nsresult rv = HandleResolveFailure(aCx, script, specifier, + result.unwrapErr(), 0, 0, &error); + if (NS_FAILED(rv)) { + JS_ReportOutOfMemory(aCx); + return false; + } + + JS_SetPendingException(aCx, error); + return false; + } + + // Create a new top-level load request. + nsCOMPtr<nsIURI> uri = result.unwrap(); + RefPtr<ModuleLoadRequest> request = loader->CreateDynamicImport( + aCx, uri, script, aReferencingPrivate, specifierString, aPromise); + + if (!request) { + JS_ReportErrorASCII(aCx, "Dynamic import not supported in this context"); + return false; + } + + loader->StartDynamicImport(request); + return true; +} + +// static +ModuleLoaderBase* ModuleLoaderBase::GetCurrentModuleLoader(JSContext* aCx) { + auto reportError = mozilla::MakeScopeExit([aCx]() { + JS_ReportErrorASCII(aCx, "No ScriptLoader found for the current context"); + }); + + JS::Rooted<JSObject*> 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(); + return loader; +} + +// static +LoadedScript* ModuleLoaderBase::GetLoadedScriptOrNull( + JSContext* aCx, JS::Handle<JS::Value> aReferencingPrivate) { + if (aReferencingPrivate.isUndefined()) { + return nullptr; + } + + auto* script = static_cast<LoadedScript*>(aReferencingPrivate.toPrivate()); + if (script->IsEventScript()) { + return nullptr; + } + + MOZ_ASSERT_IF( + script->IsModuleScript(), + JS::GetModulePrivate(script->AsModuleScript()->ModuleRecord()) == + aReferencingPrivate); + + return script; +} + +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->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->mURI) + ->Then(mEventTarget, __func__, request, + &ModuleLoadRequest::ModuleLoaded, + &ModuleLoadRequest::LoadFailed); + 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<nsCString>& 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()); + MOZ_ASSERT(!ModuleMapContainsURL(aRequest->mURI)); + + mFetchingModules.InsertOrUpdate( + aRequest->mURI, RefPtr<mozilla::GenericNonExclusivePromise::Private>{}); +} + +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, they + // will have ModuleLoaded or LoadFailed on them when the promise is + // resolved/rejected. This is set up in StartLoad. + + MOZ_ASSERT(aRequest->mLoader == this); + + LOG( + ("ScriptLoadRequest (%p): Module fetch finished (script == %p, result == " + "%u)", + aRequest, aRequest->mModuleScript.get(), unsigned(aResult))); + + RefPtr<mozilla::GenericNonExclusivePromise::Private> promise; + if (!mFetchingModules.Remove(aRequest->mURI, getter_AddRefs(promise))) { + LOG( + ("ScriptLoadRequest (%p): Key not found in mFetchingModules, " + "assuming we have an inline module or have finished fetching already", + aRequest)); + return; + } + + RefPtr<ModuleScript> moduleScript(aRequest->mModuleScript); + MOZ_ASSERT(NS_FAILED(aResult) == !moduleScript); + + mFetchedModules.InsertOrUpdate(aRequest->mURI, RefPtr{moduleScript}); + + if (promise) { + if (moduleScript) { + LOG(("ScriptLoadRequest (%p): resolving %p", aRequest, promise.get())); + promise->Resolve(true, __func__); + } else { + LOG(("ScriptLoadRequest (%p): rejecting %p", aRequest, promise.get())); + promise->Reject(aResult, __func__); + } + } +} + +RefPtr<mozilla::GenericNonExclusivePromise> +ModuleLoaderBase::WaitForModuleFetch(nsIURI* aURL) { + MOZ_ASSERT(ModuleMapContainsURL(aURL)); + + nsURIHashKey key(aURL); + if (auto entry = mFetchingModules.Lookup(aURL)) { + if (!entry.Data()) { + entry.Data() = new mozilla::GenericNonExclusivePromise::Private(__func__); + } + return entry.Data(); + } + + RefPtr<ModuleScript> ms; + MOZ_ALWAYS_TRUE(mFetchedModules.Get(aURL, getter_AddRefs(ms))); + if (!ms) { + return mozilla::GenericNonExclusivePromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + } + + return mozilla::GenericNonExclusivePromise::CreateAndResolve(true, __func__); +} + +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->mModuleScript && !aRequest->mModuleScript->HasParseError()) { + 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<JSObject*> module(cx); + + JS::CompileOptions options(cx); + JS::RootedScript introductionScript(cx); + rv = mLoader->FillCompileOptionsForRequest(cx, aRequest, &options, + &introductionScript); + + if (NS_SUCCEEDED(rv)) { + JS::Rooted<JSObject*> 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; + } + } + + RefPtr<ModuleScript> moduleScript = + new ModuleScript(aRequest->mFetchOptions, aRequest->mBaseURL); + aRequest->mModuleScript = moduleScript; + + if (!module) { + LOG(("ScriptLoadRequest (%p): compilation failed (%d)", aRequest, + unsigned(rv))); + + JS::Rooted<JS::Value> 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)) { + aRequest->ModuleErrored(); + return NS_OK; + } + } + + LOG(("ScriptLoadRequest (%p): module script == %p", aRequest, + aRequest->mModuleScript.get())); + + return rv; +} + +nsresult ModuleLoaderBase::HandleResolveFailure( + JSContext* aCx, LoadedScript* aScript, const nsAString& aSpecifier, + ResolveError aError, uint32_t aLineNumber, uint32_t aColumnNumber, + JS::MutableHandle<JS::Value> aErrorOut) { + JS::Rooted<JSString*> 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; + } + + AutoTArray<nsString, 1> errorParams; + errorParams.AppendElement(aSpecifier); + + nsAutoString errorText; + nsresult rv = nsContentUtils::FormatLocalizedString( + nsContentUtils::eDOM_PROPERTIES, ResolveErrorInfo::GetString(aError), + errorParams, errorText); + NS_ENSURE_SUCCESS(rv, rv); + + JS::Rooted<JSString*> 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; +} + +// Helper for getting import maps pref across main thread and workers +bool ImportMapsEnabled() { + if (NS_IsMainThread()) { + return mozilla::StaticPrefs::dom_importMaps_enabled(); + } + return false; +} + +ResolveResult ModuleLoaderBase::ResolveModuleSpecifier( + LoadedScript* aScript, const nsAString& aSpecifier) { + // If import map is enabled, forward to the updated 'Resolve a module + // specifier' algorithm defined in Import maps spec. + // + // Once import map is enabled by default, + // ModuleLoaderBase::ResolveModuleSpecifier should be replaced by + // ImportMap::ResolveModuleSpecifier. + if (ImportMapsEnabled()) { + return ImportMap::ResolveModuleSpecifier(mImportMap.get(), mLoader, aScript, + aSpecifier); + } + + // The following module specifiers are allowed by the spec: + // - a valid absolute URL + // - a valid relative URL that starts with "/", "./" or "../" + // + // Bareword module specifiers are handled in Import maps. + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aSpecifier); + if (NS_SUCCEEDED(rv)) { + return WrapNotNull(uri); + } + + if (rv != NS_ERROR_MALFORMED_URI) { + return Err(ResolveError::Failure); + } + + if (!StringBeginsWith(aSpecifier, u"/"_ns) && + !StringBeginsWith(aSpecifier, u"./"_ns) && + !StringBeginsWith(aSpecifier, u"../"_ns)) { + return Err(ResolveError::FailureMayBeBare); + } + + // Get the document's base URL if we don't have a referencing script here. + nsCOMPtr<nsIURI> baseURL; + if (aScript) { + baseURL = aScript->BaseURL(); + } else { + baseURL = mLoader->GetBaseURI(); + } + + rv = NS_NewURI(getter_AddRefs(uri), aSpecifier, nullptr, baseURL); + if (NS_SUCCEEDED(rv)) { + return WrapNotNull(uri); + } + + return Err(ResolveError::Failure); +} + +nsresult ModuleLoaderBase::ResolveRequestedModules( + ModuleLoadRequest* aRequest, nsCOMArray<nsIURI>* aUrlsOut) { + ModuleScript* ms = aRequest->mModuleScript; + + AutoJSAPI jsapi; + if (!jsapi.Init(mGlobalObject)) { + return NS_ERROR_FAILURE; + } + + JSContext* cx = jsapi.cx(); + JS::Rooted<JSObject*> moduleRecord(cx, ms->ModuleRecord()); + uint32_t length = JS::GetRequestedModulesCount(cx, moduleRecord); + + JS::Rooted<JS::Value> requestedModule(cx); + for (uint32_t i = 0; i < length; i++) { + JS::Rooted<JSString*> 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; + uint32_t columnNumber = 0; + JS::GetRequestedModuleSourcePos(cx, moduleRecord, i, &lineNumber, + &columnNumber); + + JS::Rooted<JS::Value> error(cx); + nsresult rv = HandleResolveFailure(cx, ms, specifier, result.unwrapErr(), + lineNumber, columnNumber, &error); + NS_ENSURE_SUCCESS(rv, rv); + + ms->SetParseError(error); + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIURI> 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->IsReadyToRun()); + + auto visitedSet = aRequest->mVisitedSet; + MOZ_ASSERT(visitedSet->Contains(aRequest->mURI)); + + aRequest->mState = ModuleLoadRequest::State::LoadingImports; + + nsCOMArray<nsIURI> 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; + } + + // For each url in urls, fetch a module script graph given url, module + // script's CORS setting, and module script's settings object. + nsTArray<RefPtr<mozilla::GenericPromise>> importsReady; + for (auto* url : urls) { + RefPtr<mozilla::GenericPromise> childReady = + StartFetchingModuleAndDependencies(aRequest, url); + importsReady.AppendElement(childReady); + } + + // Wait for all imports to become ready. + RefPtr<mozilla::GenericPromise::AllPromiseType> allReady = + mozilla::GenericPromise::All(mEventTarget, importsReady); + allReady->Then(mEventTarget, __func__, aRequest, + &ModuleLoadRequest::DependenciesLoaded, + &ModuleLoadRequest::ModuleErrored); +} + +RefPtr<mozilla::GenericPromise> +ModuleLoaderBase::StartFetchingModuleAndDependencies(ModuleLoadRequest* aParent, + nsIURI* aURI) { + MOZ_ASSERT(aURI); + + RefPtr<ModuleLoadRequest> 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())); + } + + RefPtr<mozilla::GenericPromise> ready = childRequest->mReady.Ensure(__func__); + + nsresult rv = StartModuleLoad(childRequest); + if (NS_FAILED(rv)) { + MOZ_ASSERT(!childRequest->mModuleScript); + LOG(("ScriptLoadRequest (%p): rejecting %p", aParent, + &childRequest->mReady)); + + mLoader->ReportErrorToConsole(childRequest, rv); + childRequest->mReady.Reject(rv, __func__); + return ready; + } + + return ready; +} + +void 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); + } +} + +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); +} + +void ModuleLoaderBase::FinishDynamicImport( + JSContext* aCx, ModuleLoadRequest* aRequest, nsresult aResult, + JS::Handle<JSObject*> aEvaluationPromise) { + // 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); + LOG(("ScriptLoadRequest (%p): Finish dynamic import %x %d", aRequest, + unsigned(aResult), JS_IsExceptionPending(aCx))); + + // 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)); + JS_ReportErrorNumberUC(aCx, js::GetErrorMessage, nullptr, + JSMSG_DYNAMIC_IMPORT_FAILED); + } + + JS::Rooted<JS::Value> referencingScript(aCx, + aRequest->mDynamicReferencingPrivate); + JS::Rooted<JSString*> specifier(aCx, aRequest->mDynamicSpecifier); + JS::Rooted<JSObject*> promise(aCx, aRequest->mDynamicPromise); + + JS::Rooted<JSObject*> 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, + nsISerialEventTarget* aEventTarget) + : mGlobalObject(aGlobalObject), + mEventTarget(aEventTarget), + mLoader(aLoader) { + MOZ_ASSERT(mGlobalObject); + MOZ_ASSERT(mEventTarget); + MOZ_ASSERT(mLoader); + + EnsureModuleHooksInitialized(); +} + +ModuleLoaderBase::~ModuleLoaderBase() { + mDynamicImportRequests.CancelRequestsAndClear(); + + LOG(("ModuleLoaderBase::~ModuleLoaderBase %p", this)); +} + +void ModuleLoaderBase::Shutdown() { + MOZ_ASSERT(mFetchingModules.IsEmpty()); + + for (const auto& entry : mFetchedModules) { + entry.GetData()->Shutdown(); + } + + mFetchedModules.Clear(); + mGlobalObject = nullptr; + mEventTarget = nullptr; + mLoader = nullptr; +} + +bool ModuleLoaderBase::HasPendingDynamicImports() const { + return !mDynamicImportRequests.isEmpty(); +} + +void ModuleLoaderBase::CancelDynamicImport(ModuleLoadRequest* aRequest, + nsresult aResult) { + MOZ_ASSERT(aRequest->mLoader == this); + + RefPtr<ScriptLoadRequest> 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<ModuleLoadRequest*>(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<JSObject*> 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<JSObject*> module(aCx, moduleScript->ModuleRecord()); + MOZ_ASSERT(module); + + // The script is now ready to be exposed to the debugger. + JS::Rooted<JSScript*> script(aCx, JS::GetModuleScript(module)); + JS::ExposeScriptToDebugger(aCx, script); + + moduleScript->SetDebuggerDataInitialized(); + return NS_OK; +} + +void ModuleLoaderBase::ProcessDynamicImport(ModuleLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->mLoader == this); + + if (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); + + 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()->mOffThreadToken); + + ModuleScript* moduleScript = request->mModuleScript; + if (moduleScript->HasErrorToRethrow()) { + LOG(("ScriptLoadRequest (%p): module has error to rethrow", aRequest)); + JS::Rooted<JS::Value> 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<JSObject*> module(aCx, moduleScript->ModuleRecord()); + MOZ_ASSERT(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<JS::Value> 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) { + LOG(("ScriptLoadRequest (%p): evaluation failed", aRequest)); + // For a dynamic import, the promise is rejected. Otherwise an error is + // reported by AutoEntryScript. + } + + // 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<JSObject*> evaluationPromise(aCx); + if (rval.isObject()) { + evaluationPromise.set(&rval.toObject()); + } + + if (request->IsDynamicImport()) { + 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<ImportMap> 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); + if (NS_FAILED(rv)) { + return nullptr; + } + + JS::SourceText<char16_t>& text = maybeSource.ref<SourceText<char16_t>>(); + 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<ImportMap> 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); +} + +#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..ebbcc42ebb --- /dev/null +++ b/js/loader/ModuleLoaderBase.h @@ -0,0 +1,420 @@ +/* -*- 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/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/CORSMode.h" +#include "mozilla/dom/JSExecutionContext.h" +#include "mozilla/MaybeOneOf.h" +#include "mozilla/MozPromise.h" +#include "mozilla/UniquePtr.h" +#include "ResolveResult.h" + +class nsIURI; + +namespace mozilla { + +class LazyLogModule; +union Utf8Unit; + +} // namespace mozilla + +namespace JS { + +class CompileOptions; + +template <typename UnitT> +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<nsString>& aParams = nsTArray<nsString>()) 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<JSScript*> 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 yet loaded is represented by a promise in the mFetchingModules map. A + * module which has been loaded 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 { + private: + using GenericNonExclusivePromise = mozilla::GenericNonExclusivePromise; + using GenericPromise = mozilla::GenericPromise; + + // Module map + nsRefPtrHashtable<nsURIHashKey, GenericNonExclusivePromise::Private> + mFetchingModules; + nsRefPtrHashtable<nsURIHashKey, ModuleScript> mFetchedModules; + + // List of dynamic imports that are currently being loaded. + ScriptLoadRequestList mDynamicImportRequests; + + nsCOMPtr<nsIGlobalObject> mGlobalObject; + + // Event handler used to process MozPromise actions, used internally to wait + // for fetches to finish and for imports to become avilable. + nsCOMPtr<nsISerialEventTarget> mEventTarget; + + // 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<ScriptLoaderInterface> mLoader; + + mozilla::UniquePtr<ImportMap> mImportMap; + + virtual ~ModuleLoaderBase(); + + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(ModuleLoaderBase) + explicit ModuleLoaderBase(ScriptLoaderInterface* aLoader, + nsIGlobalObject* aGlobalObject, + nsISerialEventTarget* aEventTarget = + mozilla::GetMainThreadSerialEventTarget()); + + // Called to break cycles during shutdown to prevent memory leaks. + void Shutdown(); + + 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<char16_t>, JS::SourceText<Utf8Unit>>; + + // 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<ModuleLoadRequest> CreateStaticImport( + nsIURI* aURI, ModuleLoadRequest* aParent) = 0; + + // Called by HostImportModuleDynamically hook. + virtual already_AddRefed<ModuleLoadRequest> CreateDynamicImport( + JSContext* aCx, nsIURI* aURI, LoadedScript* aMaybeActiveScript, + JS::Handle<JS::Value> aReferencingPrivate, + JS::Handle<JSString*> aSpecifier, JS::Handle<JSObject*> aPromise) = 0; + + // 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<JSObject*> aGlobal, + JS::CompileOptions& aOptions, ModuleLoadRequest* aRequest, + JS::MutableHandle<JSObject*> aModuleOut) = 0; + + // Called when a module script has been loaded, including imports. + virtual void OnModuleLoadComplete(ModuleLoadRequest* aRequest) = 0; + + // Public API methods. + + public: + ScriptLoaderInterface* GetScriptLoaderInterface() const { return mLoader; } + + nsIGlobalObject* GetGlobalObject() const { return mGlobalObject; } + + 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); + + void StartDynamicImport(ModuleLoadRequest* aRequest); + void ProcessDynamicImport(ModuleLoadRequest* aRequest); + void CancelAndClearDynamicImports(); + + // Process <script type="importmap"> + mozilla::UniquePtr<ImportMap> ParseImportMap(ScriptLoadRequest* aRequest); + + // Implements + // https://html.spec.whatwg.org/multipage/webappapis.html#register-an-import-map + void RegisterImportMap(mozilla::UniquePtr<ImportMap> aImportMap); + + // Getter for mImportMapsAllowed. + bool IsImportMapAllowed() const { return mImportMapsAllowed; } + // https://html.spec.whatwg.org/multipage/webappapis.html#disallow-further-import-maps + void DisallowImportMaps() { mImportMapsAllowed = false; } + + // Returns true if the module for given URL is already fetched. + bool IsModuleFetched(nsIURI* aURL) const; + + nsresult GetFetchedModuleURLs(nsTArray<nsCString>& aURLs); + + // Internal methods. + + private: + friend class JS::loader::ModuleLoadRequest; + + static ModuleLoaderBase* GetCurrentModuleLoader(JSContext* aCx); + static LoadedScript* GetLoadedScriptOrNull( + JSContext* aCx, JS::Handle<JS::Value> aReferencingPrivate); + + static void EnsureModuleHooksInitialized(); + + static JSObject* HostResolveImportedModule( + JSContext* aCx, JS::Handle<JS::Value> aReferencingPrivate, + JS::Handle<JSObject*> aModuleRequest); + static bool HostPopulateImportMeta(JSContext* aCx, + JS::Handle<JS::Value> aReferencingPrivate, + JS::Handle<JSObject*> aMetaObject); + static bool ImportMetaResolve(JSContext* cx, unsigned argc, Value* vp); + static JSString* ImportMetaResolveImpl( + JSContext* aCx, JS::Handle<JS::Value> aReferencingPrivate, + JS::Handle<JSString*> aSpecifier); + static bool HostImportModuleDynamically( + JSContext* aCx, JS::Handle<JS::Value> aReferencingPrivate, + JS::Handle<JSObject*> aModuleRequest, JS::Handle<JSObject*> aPromise); + static bool HostGetSupportedImportAssertions( + JSContext* aCx, JS::ImportAssertionVector& aValues); + + ResolveResult ResolveModuleSpecifier(LoadedScript* aScript, + const nsAString& aSpecifier); + + static nsresult HandleResolveFailure(JSContext* aCx, LoadedScript* aScript, + const nsAString& aSpecifier, + ResolveError aError, + uint32_t aLineNumber, + uint32_t aColumnNumber, + JS::MutableHandle<JS::Value> aErrorOut); + + enum class RestartRequest { No, Yes }; + nsresult StartOrRestartModuleLoad(ModuleLoadRequest* aRequest, + RestartRequest aRestart); + + bool ModuleMapContainsURL(nsIURI* aURL) const; + bool IsModuleFetching(nsIURI* aURL) const; + RefPtr<GenericNonExclusivePromise> WaitForModuleFetch(nsIURI* aURL); + void SetModuleFetchStarted(ModuleLoadRequest* aRequest); + + ModuleScript* GetFetchedModule(nsIURI* aURL) const; + + JS::Value FindFirstParseError(ModuleLoadRequest* aRequest); + static nsresult InitDebuggerDataForModuleGraph(JSContext* aCx, + ModuleLoadRequest* aRequest); + nsresult ResolveRequestedModules(ModuleLoadRequest* aRequest, + nsCOMArray<nsIURI>* aUrlsOut); + + void SetModuleFetchFinishedAndResumeWaitingRequests( + ModuleLoadRequest* aRequest, nsresult aResult); + + void StartFetchingModuleDependencies(ModuleLoadRequest* aRequest); + + RefPtr<GenericPromise> StartFetchingModuleAndDependencies( + ModuleLoadRequest* aParent, nsIURI* aURI); + + /** + * Shorthand Wrapper for JSAPI FinishDynamicImport function for the reject + * case where we do not have `aEvaluationPromise`. As there is no evaluation + * Promise, JS::FinishDynamicImport will always reject. + * + * @param aRequest + * The module load request for the dynamic module. + * @param aResult + * The result of running ModuleEvaluate -- If this is successful, then + * we can await the associated EvaluationPromise. + */ + void FinishDynamicImportAndReject(ModuleLoadRequest* aRequest, + nsresult aResult); + + /** + * Wrapper for JSAPI FinishDynamicImport function. Takes an optional argument + * `aEvaluationPromise` which, if null, exits early. + * + * This is the Top Level Await version, which works with modules which return + * promises. + * + * @param aCX + * The JSContext for the module. + * @param aRequest + * The module load request for the dynamic module. + * @param aResult + * The result of running ModuleEvaluate -- If this is successful, then + * we can await the associated EvaluationPromise. + * @param aEvaluationPromise + * The evaluation promise returned from evaluating the module. If this + * is null, JS::FinishDynamicImport will reject the dynamic import + * module promise. + */ + static void FinishDynamicImport(JSContext* aCx, ModuleLoadRequest* aRequest, + nsresult aResult, + JS::Handle<JSObject*> aEvaluationPromise); + + void RemoveDynamicImport(ModuleLoadRequest* aRequest); + + nsresult CreateModuleScript(ModuleLoadRequest* aRequest); + + // The slot stored in ImportMetaResolve function. + enum { ModulePrivateSlot = 0, SlotCount }; + + // The number of args in ImportMetaResolve. + static const uint32_t ImportMetaResolveNumArgs = 1; + // The index of the 'specifier' argument in ImportMetaResolve. + static const uint32_t ImportMetaResolveSpecifierArg = 0; + + public: + static mozilla::LazyLogModule gCspPRLog; + static mozilla::LazyLogModule gModuleLoaderBaseLog; +}; + +} // namespace loader +} // namespace JS + +#endif // js_loader_ModuleLoaderBase_h diff --git a/js/loader/ResolveResult.h b/js/loader/ResolveResult.h new file mode 100644 index 0000000000..e625e0e43e --- /dev/null +++ b/js/loader/ResolveResult.h @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef js_loader_ResolveResult_h +#define js_loader_ResolveResult_h + +#include "mozilla/ResultVariant.h" +#include "mozilla/NotNull.h" +#include "nsIURI.h" + +namespace JS::loader { + +enum class ResolveError : uint8_t { + Failure, + FailureMayBeBare, + BlockedByNullEntry, + BlockedByAfterPrefix, + BlockedByBacktrackingPrefix, + InvalidBareSpecifier +}; + +struct ResolveErrorInfo { + static const char* GetString(ResolveError aError) { + switch (aError) { + case ResolveError::Failure: + return "ModuleResolveFailureNoWarn"; + case ResolveError::FailureMayBeBare: + return "ModuleResolveFailureWarnRelative"; + case ResolveError::BlockedByNullEntry: + return "ImportMapResolutionBlockedByNullEntry"; + case ResolveError::BlockedByAfterPrefix: + return "ImportMapResolutionBlockedByAfterPrefix"; + case ResolveError::BlockedByBacktrackingPrefix: + return "ImportMapResolutionBlockedByBacktrackingPrefix"; + case ResolveError::InvalidBareSpecifier: + return "ImportMapResolveInvalidBareSpecifierWarnRelative"; + default: + MOZ_CRASH("Unexpected ResolveError value"); + } + } +}; + +/** + * ResolveResult is used to store the result of 'resolving a module specifier', + * which could be an URI on success or a ResolveError on failure. + */ +using ResolveResult = + mozilla::Result<mozilla::NotNull<nsCOMPtr<nsIURI>>, ResolveError>; +} // namespace JS::loader + +#endif // js_loader_ResolveResult_h diff --git a/js/loader/ScriptKind.h b/js/loader/ScriptKind.h new file mode 100644 index 0000000000..70ab0b9c3e --- /dev/null +++ b/js/loader/ScriptKind.h @@ -0,0 +1,16 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef js_loader_ScriptKind_h +#define js_loader_ScriptKind_h + +namespace JS::loader { + +enum class ScriptKind { eClassic, eModule, eEvent, eImportMap }; + +} // namespace JS::loader + +#endif diff --git a/js/loader/ScriptLoadRequest.cpp b/js/loader/ScriptLoadRequest.cpp new file mode 100644 index 0000000000..ef6c7b80ff --- /dev/null +++ b/js/loader/ScriptLoadRequest.cpp @@ -0,0 +1,283 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ScriptLoadRequest.h" +#include "GeckoProfiler.h" + +#include "mozilla/dom/Document.h" +#include "mozilla/dom/ScriptLoadContext.h" +#include "mozilla/dom/WorkerLoadContext.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/Unused.h" +#include "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include "js/OffThreadScriptCompilation.h" +#include "js/SourceText.h" + +#include "ModuleLoadRequest.h" +#include "nsContentUtils.h" +#include "nsICacheInfoChannel.h" +#include "nsIClassOfService.h" +#include "nsISupportsPriority.h" + +using JS::SourceText; + +namespace JS::loader { + +////////////////////////////////////////////////////////////// +// ScriptFetchOptions +////////////////////////////////////////////////////////////// + +NS_IMPL_CYCLE_COLLECTION(ScriptFetchOptions, mTriggeringPrincipal, mElement) + +ScriptFetchOptions::ScriptFetchOptions( + mozilla::CORSMode aCORSMode, mozilla::dom::ReferrerPolicy aReferrerPolicy, + nsIPrincipal* aTriggeringPrincipal, mozilla::dom::Element* aElement) + : mCORSMode(aCORSMode), + mReferrerPolicy(aReferrerPolicy), + mTriggeringPrincipal(aTriggeringPrincipal), + mElement(aElement) {} + +ScriptFetchOptions::~ScriptFetchOptions() = default; + +////////////////////////////////////////////////////////////// +// ScriptLoadRequest +////////////////////////////////////////////////////////////// + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ScriptLoadRequest) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(ScriptLoadRequest) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ScriptLoadRequest) + +NS_IMPL_CYCLE_COLLECTION_CLASS(ScriptLoadRequest) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ScriptLoadRequest) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFetchOptions, mCacheInfo, mLoadContext) + tmp->mScriptForBytecodeEncoding = nullptr; + tmp->DropBytecodeCacheReferences(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(ScriptLoadRequest) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFetchOptions, mCacheInfo, mLoadContext) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(ScriptLoadRequest) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mScriptForBytecodeEncoding) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +ScriptLoadRequest::ScriptLoadRequest(ScriptKind aKind, nsIURI* aURI, + ScriptFetchOptions* aFetchOptions, + const SRIMetadata& aIntegrity, + nsIURI* aReferrer, + LoadContextBase* aContext) + : mKind(aKind), + mState(State::Fetching), + mFetchSourceOnly(false), + mDataType(DataType::eUnknown), + mFetchOptions(aFetchOptions), + mIntegrity(aIntegrity), + mReferrer(aReferrer), + mScriptTextLength(0), + mScriptBytecode(), + mBytecodeOffset(0), + mURI(aURI), + mLoadContext(aContext) { + MOZ_ASSERT(mFetchOptions); + if (mLoadContext) { + mLoadContext->SetRequest(this); + } +} + +ScriptLoadRequest::~ScriptLoadRequest() { + if (IsMarkedForBytecodeEncoding()) { + DropBytecodeCacheReferences(); + } + mLoadContext = nullptr; + DropJSObjects(this); +} + +void ScriptLoadRequest::SetReady() { + MOZ_ASSERT(!IsReadyToRun()); + mState = State::Ready; +} + +void ScriptLoadRequest::Cancel() { + mState = State::Canceled; + if (HasScriptLoadContext()) { + GetScriptLoadContext()->MaybeCancelOffThreadScript(); + } +} + +void ScriptLoadRequest::DropBytecodeCacheReferences() { + mCacheInfo = nullptr; + DropJSObjects(this); +} + +bool ScriptLoadRequest::HasScriptLoadContext() const { + return HasLoadContext() && mLoadContext->IsWindowContext(); +} + +bool ScriptLoadRequest::HasWorkerLoadContext() const { + return HasLoadContext() && mLoadContext->IsWorkerContext(); +} + +mozilla::dom::ScriptLoadContext* ScriptLoadRequest::GetScriptLoadContext() { + MOZ_ASSERT(mLoadContext); + return mLoadContext->AsWindowContext(); +} + +mozilla::loader::ComponentLoadContext* +ScriptLoadRequest::GetComponentLoadContext() { + MOZ_ASSERT(mLoadContext); + return mLoadContext->AsComponentContext(); +} + +mozilla::dom::WorkerLoadContext* ScriptLoadRequest::GetWorkerLoadContext() { + MOZ_ASSERT(mLoadContext); + return mLoadContext->AsWorkerContext(); +} + +ModuleLoadRequest* ScriptLoadRequest::AsModuleRequest() { + MOZ_ASSERT(IsModuleRequest()); + return static_cast<ModuleLoadRequest*>(this); +} + +const ModuleLoadRequest* ScriptLoadRequest::AsModuleRequest() const { + MOZ_ASSERT(IsModuleRequest()); + return static_cast<const ModuleLoadRequest*>(this); +} + +void ScriptLoadRequest::SetBytecode() { + MOZ_ASSERT(IsUnknownDataType()); + mDataType = DataType::eBytecode; +} + +bool ScriptLoadRequest::IsUTF8ParsingEnabled() { + if (HasLoadContext()) { + if (mLoadContext->IsWindowContext()) { + return mozilla::StaticPrefs:: + dom_script_loader_external_scripts_utf8_parsing_enabled(); + } + if (mLoadContext->IsWorkerContext()) { + return mozilla::StaticPrefs:: + dom_worker_script_loader_utf8_parsing_enabled(); + } + } + return false; +} + +void ScriptLoadRequest::ClearScriptSource() { + if (IsTextSource()) { + ClearScriptText(); + } +} + +void ScriptLoadRequest::MarkForBytecodeEncoding(JSScript* aScript) { + MOZ_ASSERT(!IsModuleRequest()); + MOZ_ASSERT(!IsMarkedForBytecodeEncoding()); + mScriptForBytecodeEncoding = aScript; + HoldJSObjects(this); +} + +bool ScriptLoadRequest::IsMarkedForBytecodeEncoding() const { + if (IsModuleRequest()) { + return AsModuleRequest()->IsModuleMarkedForBytecodeEncoding(); + } + + return !!mScriptForBytecodeEncoding; +} + +nsresult ScriptLoadRequest::GetScriptSource(JSContext* aCx, + MaybeSourceText* aMaybeSource) { + // If there's no script text, we try to get it from the element + if (HasScriptLoadContext() && GetScriptLoadContext()->mIsInline) { + nsAutoString inlineData; + GetScriptLoadContext()->GetScriptElement()->GetScriptText(inlineData); + + size_t nbytes = inlineData.Length() * sizeof(char16_t); + JS::UniqueTwoByteChars chars( + static_cast<char16_t*>(JS_malloc(aCx, nbytes))); + if (!chars) { + return NS_ERROR_OUT_OF_MEMORY; + } + + memcpy(chars.get(), inlineData.get(), nbytes); + + SourceText<char16_t> srcBuf; + if (!srcBuf.init(aCx, std::move(chars), inlineData.Length())) { + return NS_ERROR_OUT_OF_MEMORY; + } + + aMaybeSource->construct<SourceText<char16_t>>(std::move(srcBuf)); + return NS_OK; + } + + size_t length = ScriptTextLength(); + if (IsUTF16Text()) { + JS::UniqueTwoByteChars chars; + chars.reset(ScriptText<char16_t>().extractOrCopyRawBuffer()); + if (!chars) { + JS_ReportOutOfMemory(aCx); + return NS_ERROR_OUT_OF_MEMORY; + } + + SourceText<char16_t> srcBuf; + if (!srcBuf.init(aCx, std::move(chars), length)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + aMaybeSource->construct<SourceText<char16_t>>(std::move(srcBuf)); + return NS_OK; + } + + MOZ_ASSERT(IsUTF8Text()); + UniquePtr<Utf8Unit[], JS::FreePolicy> chars; + chars.reset(ScriptText<Utf8Unit>().extractOrCopyRawBuffer()); + if (!chars) { + JS_ReportOutOfMemory(aCx); + return NS_ERROR_OUT_OF_MEMORY; + } + + SourceText<Utf8Unit> srcBuf; + if (!srcBuf.init(aCx, std::move(chars), length)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + aMaybeSource->construct<SourceText<Utf8Unit>>(std::move(srcBuf)); + return NS_OK; +} + +////////////////////////////////////////////////////////////// +// ScriptLoadRequestList +////////////////////////////////////////////////////////////// + +ScriptLoadRequestList::~ScriptLoadRequestList() { CancelRequestsAndClear(); } + +void ScriptLoadRequestList::CancelRequestsAndClear() { + while (!isEmpty()) { + RefPtr<ScriptLoadRequest> first = StealFirst(); + first->Cancel(); + // And just let it go out of scope and die. + } +} + +#ifdef DEBUG +bool ScriptLoadRequestList::Contains(ScriptLoadRequest* aElem) const { + for (const ScriptLoadRequest* req = getFirst(); req; req = req->getNext()) { + if (req == aElem) { + return true; + } + } + + return false; +} +#endif // DEBUG + +} // namespace JS::loader diff --git a/js/loader/ScriptLoadRequest.h b/js/loader/ScriptLoadRequest.h new file mode 100644 index 0000000000..4aa75517e3 --- /dev/null +++ b/js/loader/ScriptLoadRequest.h @@ -0,0 +1,425 @@ +/* -*- 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_ScriptLoadRequest_h +#define js_loader_ScriptLoadRequest_h + +#include "js/AllocPolicy.h" +#include "js/RootingAPI.h" +#include "js/SourceText.h" +#include "js/TypeDecls.h" +#include "mozilla/Atomics.h" +#include "mozilla/Assertions.h" +#include "mozilla/CORSMode.h" +#include "mozilla/dom/SRIMetadata.h" +#include "mozilla/dom/ReferrerPolicyBinding.h" +#include "mozilla/LinkedList.h" +#include "mozilla/Maybe.h" +#include "mozilla/MaybeOneOf.h" +#include "mozilla/PreloaderBase.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/Utf8.h" // mozilla::Utf8Unit +#include "mozilla/Variant.h" +#include "mozilla/Vector.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIGlobalObject.h" +#include "ScriptKind.h" +#include "nsIScriptElement.h" + +class nsICacheInfoChannel; + +namespace mozilla::dom { + +class ScriptLoadContext; +class WorkerLoadContext; + +} // namespace mozilla::dom + +namespace mozilla::loader { +class ComponentLoadContext; +} // namespace mozilla::loader + +namespace JS { +class OffThreadToken; + +namespace loader { + +using Utf8Unit = mozilla::Utf8Unit; + +class LoadContextBase; +class ModuleLoadRequest; +class ScriptLoadRequestList; + +/* + * ScriptFetchOptions loosely corresponds to HTML's "script fetch options", + * https://html.spec.whatwg.org/multipage/webappapis.html#script-fetch-options + * with the exception of the following properties: + * cryptographic nonce + * The cryptographic nonce metadata used for the initial fetch and for + * fetching any imported modules. As this is populated by a DOM element, + * this is implemented via mozilla::dom::Element as the field + * mElement. The default value is an empty string, and is indicated + * when this field is a nullptr. Nonce is not represented on the dom + * side as per bug 1374612. + * parser metadata + * The parser metadata used for the initial fetch and for fetching any + * imported modules. This is populated from a mozilla::dom::Element and is + * handled by the field mElement. The default value is an empty string, + * and is indicated when this field is a nullptr. + * integrity metadata + * The integrity metadata used for the initial fetch. This is + * implemented in ScriptLoadRequest, as it changes for every + * ScriptLoadRequest. + * + * In the case of classic scripts without dynamic import, this object is + * used once. For modules, this object is propogated throughout the module + * tree. If there is a dynamically imported module in any type of script, + * the ScriptFetchOptions object will be propogated from its importer. + */ + +class ScriptFetchOptions { + ~ScriptFetchOptions(); + + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(ScriptFetchOptions) + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(ScriptFetchOptions) + + ScriptFetchOptions(mozilla::CORSMode aCORSMode, + enum mozilla::dom::ReferrerPolicy aReferrerPolicy, + nsIPrincipal* aTriggeringPrincipal, + mozilla::dom::Element* aElement = nullptr); + + /* + * The credentials mode used for the initial fetch (for module scripts) + * and for fetching any imported modules (for both module scripts and + * classic scripts) + */ + const mozilla::CORSMode mCORSMode; + + /* + * The referrer policy used for the initial fetch and for fetching any + * imported modules + */ + const enum mozilla::dom::ReferrerPolicy mReferrerPolicy; + + /* + * Used to determine CSP and if we are on the About page. + * Only used in DOM content scripts. + * TODO: Move to ScriptLoadContext + */ + nsCOMPtr<nsIPrincipal> mTriggeringPrincipal; + /* + * Represents fields populated by DOM elements (nonce, parser metadata) + * Leave this field as a nullptr for any fetch that requires the + * default classic script options. + * (https://html.spec.whatwg.org/multipage/webappapis.html#default-classic-script-fetch-options) + * TODO: extract necessary fields rather than passing this object + */ + nsCOMPtr<mozilla::dom::Element> mElement; +}; + +/* + * ScriptLoadRequest + * + * ScriptLoadRequest is a generic representation of a JavaScript script that + * will be loaded by a Script/Module loader. This representation is used by the + * DOM ScriptLoader and will be used by workers and MOZJSComponentLoader. + * + * The ScriptLoadRequest contains information about the kind of script (classic + * or module), the URI, and the ScriptFetchOptions associated with the script. + * It is responsible for holding the script data once the fetch is complete, or + * if the request is cached, the bytecode. + * + * Relationship to ScriptLoadContext: + * + * ScriptLoadRequest and ScriptLoadContexts have a circular pointer. A + * ScriptLoadContext augments the loading of a ScriptLoadRequest by providing + * additional information regarding the loading and evaluation behavior (see + * the ScriptLoadContext class for details). In terms of responsibility, + * the ScriptLoadRequest represents "What" is being loaded, and the + * ScriptLoadContext represents "How". + * + * TODO: see if we can use it in the jsshell script loader. We need to either + * remove ISUPPORTS or find a way to encorporate that in the jsshell. We would + * then only have one implementation of the script loader, and it would be + * tested whenever jsshell tests are run. This would mean finding another way to + * create ScriptLoadRequest lists. + * + */ + +class ScriptLoadRequest + : public nsISupports, + private mozilla::LinkedListElement<ScriptLoadRequest> { + using super = LinkedListElement<ScriptLoadRequest>; + + // Allow LinkedListElement<ScriptLoadRequest> to cast us to itself as needed. + friend class mozilla::LinkedListElement<ScriptLoadRequest>; + friend class ScriptLoadRequestList; + + protected: + virtual ~ScriptLoadRequest(); + + public: + using SRIMetadata = mozilla::dom::SRIMetadata; + ScriptLoadRequest(ScriptKind aKind, nsIURI* aURI, + ScriptFetchOptions* aFetchOptions, + const SRIMetadata& aIntegrity, nsIURI* aReferrer, + LoadContextBase* aContext); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(ScriptLoadRequest) + + using super::getNext; + using super::isInList; + + template <typename T> + using VariantType = mozilla::VariantType<T>; + + template <typename... Ts> + using Variant = mozilla::Variant<Ts...>; + + template <typename T, typename D = JS::DeletePolicy<T>> + using UniquePtr = mozilla::UniquePtr<T, D>; + + using MaybeSourceText = + mozilla::MaybeOneOf<JS::SourceText<char16_t>, JS::SourceText<Utf8Unit>>; + + bool IsModuleRequest() const { return mKind == ScriptKind::eModule; } + bool IsImportMapRequest() const { return mKind == ScriptKind::eImportMap; } + + ModuleLoadRequest* AsModuleRequest(); + const ModuleLoadRequest* AsModuleRequest() const; + + virtual bool IsTopLevel() const { return true; }; + + virtual void Cancel(); + + virtual void SetReady(); + + enum class State : uint8_t { + Fetching, + Compiling, + LoadingImports, + Ready, + Canceled + }; + + bool IsFetching() const { return mState == State::Fetching; } + + bool IsCompiling() const { return mState == State::Compiling; } + + bool IsReadyToRun() const { + return mState == State::Ready || mState == State::Canceled; + } + + bool IsCanceled() const { return mState == State::Canceled; } + + // Type of data provided by the nsChannel. + enum class DataType : uint8_t { eUnknown, eTextSource, eBytecode }; + + bool IsUnknownDataType() const { return mDataType == DataType::eUnknown; } + bool IsTextSource() const { return mDataType == DataType::eTextSource; } + bool IsSource() const { return IsTextSource(); } + + void SetUnknownDataType() { + mDataType = DataType::eUnknown; + mScriptData.reset(); + } + + bool IsUTF8ParsingEnabled(); + + void SetTextSource() { + MOZ_ASSERT(IsUnknownDataType()); + mDataType = DataType::eTextSource; + if (IsUTF8ParsingEnabled()) { + mScriptData.emplace(VariantType<ScriptTextBuffer<Utf8Unit>>()); + } else { + mScriptData.emplace(VariantType<ScriptTextBuffer<char16_t>>()); + } + } + + // 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 <typename Unit> + using ScriptTextBuffer = mozilla::Vector<Unit, 0, js::MallocAllocPolicy>; + + bool IsUTF16Text() const { + return mScriptData->is<ScriptTextBuffer<char16_t>>(); + } + bool IsUTF8Text() const { + return mScriptData->is<ScriptTextBuffer<Utf8Unit>>(); + } + + template <typename Unit> + const ScriptTextBuffer<Unit>& ScriptText() const { + MOZ_ASSERT(IsTextSource()); + return mScriptData->as<ScriptTextBuffer<Unit>>(); + } + template <typename Unit> + ScriptTextBuffer<Unit>& ScriptText() { + MOZ_ASSERT(IsTextSource()); + return mScriptData->as<ScriptTextBuffer<Unit>>(); + } + + size_t ScriptTextLength() const { + MOZ_ASSERT(IsTextSource()); + return IsUTF16Text() ? ScriptText<char16_t>().length() + : ScriptText<Utf8Unit>().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); + + void ClearScriptText() { + MOZ_ASSERT(IsTextSource()); + return IsUTF16Text() ? ScriptText<char16_t>().clearAndFree() + : ScriptText<Utf8Unit>().clearAndFree(); + } + + enum mozilla::dom::ReferrerPolicy ReferrerPolicy() const { + return mFetchOptions->mReferrerPolicy; + } + + nsIPrincipal* TriggeringPrincipal() const { + return mFetchOptions->mTriggeringPrincipal; + } + + void ClearScriptSource(); + + void MarkForBytecodeEncoding(JSScript* aScript); + + bool IsMarkedForBytecodeEncoding() const; + + bool IsBytecode() const { return mDataType == DataType::eBytecode; } + + void SetBytecode(); + + mozilla::CORSMode CORSMode() const { return mFetchOptions->mCORSMode; } + + void DropBytecodeCacheReferences(); + + bool HasLoadContext() const { return mLoadContext; } + bool HasScriptLoadContext() const; + bool HasWorkerLoadContext() const; + + mozilla::dom::ScriptLoadContext* GetScriptLoadContext(); + + mozilla::loader::ComponentLoadContext* GetComponentLoadContext(); + + mozilla::dom::WorkerLoadContext* GetWorkerLoadContext(); + + const ScriptKind mKind; // Whether this is a classic script or a module + // script. + + State mState; // Are we still waiting for a load to complete? + bool mFetchSourceOnly; // Request source, not cached bytecode. + DataType mDataType; // Does this contain Source or Bytecode? + RefPtr<ScriptFetchOptions> mFetchOptions; + const SRIMetadata mIntegrity; + const nsCOMPtr<nsIURI> mReferrer; + mozilla::Maybe<nsString> + mSourceMapURL; // Holds source map url for loaded scripts + + // Holds script source data for non-inline scripts. + mozilla::Maybe< + Variant<ScriptTextBuffer<char16_t>, ScriptTextBuffer<Utf8Unit>>> + 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 mScriptTextLength; + + // 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. + mozilla::Vector<uint8_t> mScriptBytecode; + uint32_t mBytecodeOffset; // Offset of the bytecode in mScriptBytecode + + const nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsIPrincipal> mOriginPrincipal; + + // Keep the URI's filename alive during off thread parsing. + // Also used by workers to report on errors while loading. + nsAutoCString mURL; + + // The base URL used for resolving relative module imports. + nsCOMPtr<nsIURI> mBaseURL; + + // Holds the top-level JSScript that corresponds to the current source, once + // it is parsed, and planned to be saved in the bytecode cache. + // + // NOTE: This field is not used for ModuleLoadRequest. + // See ModuleLoadRequest::mIsMarkedForBytecodeEncoding. + JS::Heap<JSScript*> mScriptForBytecodeEncoding; + + // Holds the Cache information, which is used to register the bytecode + // on the cache entry, such that we can load it the next time. + nsCOMPtr<nsICacheInfoChannel> mCacheInfo; + + // LoadContext for augmenting the load depending on the loading + // context (DOM, Worker, etc.) + RefPtr<LoadContextBase> mLoadContext; +}; + +class ScriptLoadRequestList : private mozilla::LinkedList<ScriptLoadRequest> { + using super = mozilla::LinkedList<ScriptLoadRequest>; + + public: + ~ScriptLoadRequestList(); + + void CancelRequestsAndClear(); + +#ifdef DEBUG + bool Contains(ScriptLoadRequest* aElem) const; +#endif // DEBUG + + using super::getFirst; + using super::isEmpty; + + void AppendElement(ScriptLoadRequest* aElem) { + MOZ_ASSERT(!aElem->isInList()); + NS_ADDREF(aElem); + insertBack(aElem); + } + + already_AddRefed<ScriptLoadRequest> Steal(ScriptLoadRequest* aElem) { + aElem->removeFrom(*this); + return dont_AddRef(aElem); + } + + already_AddRefed<ScriptLoadRequest> StealFirst() { + MOZ_ASSERT(!isEmpty()); + return Steal(getFirst()); + } + + void Remove(ScriptLoadRequest* aElem) { + aElem->removeFrom(*this); + NS_RELEASE(aElem); + } +}; + +inline void ImplCycleCollectionUnlink(ScriptLoadRequestList& aField) { + while (!aField.isEmpty()) { + RefPtr<ScriptLoadRequest> first = aField.StealFirst(); + } +} + +inline void ImplCycleCollectionTraverse( + nsCycleCollectionTraversalCallback& aCallback, + ScriptLoadRequestList& aField, const char* aName, uint32_t aFlags) { + for (ScriptLoadRequest* request = aField.getFirst(); request; + request = request->getNext()) { + CycleCollectionNoteChild(aCallback, request, aName, aFlags); + } +} + +} // namespace loader +} // namespace JS + +#endif // js_loader_ScriptLoadRequest_h diff --git a/js/loader/moz.build b/js/loader/moz.build new file mode 100644 index 0000000000..e80f43bb5f --- /dev/null +++ b/js/loader/moz.build @@ -0,0 +1,29 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS.js.loader += [ + "ImportMap.h", + "LoadContextBase.h", + "LoadedScript.h", + "ModuleLoaderBase.h", + "ModuleLoadRequest.h", + "ResolveResult.h", + "ScriptKind.h", + "ScriptLoadRequest.h", +] + +UNIFIED_SOURCES += [ + "ImportMap.cpp", + "LoadContextBase.cpp", + "LoadedScript.cpp", + "ModuleLoaderBase.cpp", + "ModuleLoadRequest.cpp", + "ScriptLoadRequest.cpp", +] + +FINAL_LIBRARY = "xul" + +include("/ipc/chromium/chromium-config.mozbuild") |