From 8a939e5954baa50d50dd799fe099edbbef284bbb Mon Sep 17 00:00:00 2001 From: Yoshi Cheng-Hao Huang Date: Thu, 5 May 2022 21:19:02 +0000 Subject: [PATCH] Bug 1688879 - Part 5: ResolveModuleSpecifier for import maps. r=jonco,yulia,flod Implement https://wicg.github.io/import-maps/#resolve-a-module-specifier Differential Revision: https://phabricator.services.mozilla.com/D142072 --- dom/locales/en-US/chrome/dom/dom.properties | 4 + js/loader/ImportMap.cpp | 224 +++++++++++++++++++- js/loader/ImportMap.h | 12 ++ js/loader/ModuleLoaderBase.cpp | 15 +- js/loader/ResolveResult.h | 12 ++ 5 files changed, 264 insertions(+), 3 deletions(-) diff --git a/dom/locales/en-US/chrome/dom/dom.properties b/dom/locales/en-US/chrome/dom/dom.properties index 24f5927d7671..bffad8a8da2f 100644 --- a/dom/locales/en-US/chrome/dom/dom.properties +++ b/dom/locales/en-US/chrome/dom/dom.properties @@ -330,6 +330,10 @@ ImportMapInvalidAddress=Address “%S” was invalid. # %1$S is the specifier key, %2$S is the URL. ImportMapAddressNotEndsWithSlash=An invalid address was given for the specifier key “%1$S”; since “%1$S” ended in a slash, the address “%2$S” needs to as well. ImportMapScopePrefixNotParseable=The scope prefix URL “%S” was not parseable. +ImportMapResolutionBlockedByNullEntry=Resolution of specifier “%S” was blocked by a null entry. +ImportMapResolutionBlockedByAfterPrefix=Resolution of specifier “%S” was blocked since the substring after prefix could not be parsed as a URL relative to the address in the import map. +ImportMapResolutionBlockedByBacktrackingPrefix=Resolution of specifier “%S” was blocked since the parsed URL does not start with the address in the import map. +ImportMapResolveInvalidBareSpecifier=The specifier “%S” was a bare specifier, but was not remapped to anything. # LOCALIZATION NOTE: %1$S is the invalid property value and %2$S is the property name. InvalidKeyframePropertyValue=Keyframe property value “%1$S” is invalid according to the syntax for “%2$S”. # LOCALIZATION NOTE: Do not translate "ReadableStream". diff --git a/js/loader/ImportMap.cpp b/js/loader/ImportMap.cpp index 579f54416890..35764be84407 100644 --- a/js/loader/ImportMap.cpp +++ b/js/loader/ImportMap.cpp @@ -9,7 +9,8 @@ #include "js/Array.h" // IsArrayObject #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* #include "js/JSON.h" // JS_ParseJSON -#include "ModuleLoaderBase.h" // ScriptLoaderInterface +#include "LoadedScript.h" +#include "ModuleLoaderBase.h" // ScriptLoaderInterface #include "nsContentUtils.h" #include "nsIScriptElement.h" #include "nsIScriptError.h" @@ -18,9 +19,11 @@ #include "ScriptLoadRequest.h" using JS::SourceText; +using mozilla::Err; using mozilla::LazyLogModule; using mozilla::MakeUnique; using mozilla::UniquePtr; +using mozilla::WrapNotNull; namespace JS::loader { @@ -436,6 +439,225 @@ UniquePtr ImportMap::ParseString( 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://wicg.github.io/import-maps/#resolve-an-imports-match +static mozilla::Result, ResolveError> ResolveImportsMatch( + nsString& aNormalizedSpecifier, nsIURI* aAsURL, + const SpecifierMap* aSpecifierMap) { + // Step 1. For each specifierKey → resolutionResult of specifierMap, + for (auto&& [specifierKey, resolutionResult] : *aSpecifierMap) { + nsAutoString specifier{aNormalizedSpecifier}; + nsCString asURL = aAsURL ? aAsURL->GetSpecOrDefault() : EmptyCString(); + + // Step 1.1. If specifierKey is normalizedSpecifier, then: + if (specifierKey.Equals(aNormalizedSpecifier)) { + // Step 1.1.1. If resolutionResult is null, then throw a TypeError + // indicating that resolution of specifierKey was blocked by a null entry. + // This will terminate the entire resolve a module specifier algorithm, + // without any further fallbacks. + if (!resolutionResult) { + LOG( + ("ImportMap::ResolveImportsMatch normalizedSpecifier: %s, " + "specifierKey: %s, but resolution is null.", + NS_ConvertUTF16toUTF8(aNormalizedSpecifier).get(), + NS_ConvertUTF16toUTF8(specifierKey).get())); + return Err(ResolveError::BlockedByNullEntry); + } + + // Step 1.1.2. Assert: resolutionResult is a URL. + MOZ_ASSERT(resolutionResult); + + // Step 1.1.3. Return resolutionResult. + return resolutionResult; + } + + // Step 1.2. If all of the following are true: + // specifierKey ends with U+002F (/), + // normalizedSpecifier starts with specifierKey, 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 "/", as + // enforced during parsing. + MOZ_ASSERT(StringEndsWith(resolutionResult->GetSpecOrDefault(), "/"_ns)); + + // Step 1.2.5. Let url be the result of parsing afterPrefix relative to + // the base URL resolutionResult. + nsCOMPtr url; + nsresult rv = NS_NewURI(getter_AddRefs(url), afterPrefix, nullptr, + resolutionResult); + + // Step 1.2.6. If url is failure, then throw a TypeError indicating that + // resolution of normalizedSpecifier was blocked since the afterPrefix + // portion could not be URL-parsed relative to the resolutionResult mapped + // to by the specifierKey prefix. + // + // This will terminate the entire resolve a module specifier algorithm, + // without any further fallbacks. + if (NS_FAILED(rv)) { + LOG( + ("ImportMap::ResolveImportsMatch normalizedSpecifier: %s, " + "specifierKey: %s, resolutionResult: %s, afterPrefix: %s, " + "but URL is not parsable.", + NS_ConvertUTF16toUTF8(aNormalizedSpecifier).get(), + NS_ConvertUTF16toUTF8(specifierKey).get(), + resolutionResult->GetSpecOrDefault().get(), + NS_ConvertUTF16toUTF8(afterPrefix).get())); + return Err(ResolveError::BlockedByAfterPrefix); + } + + // Step 1.2.7. Assert: url is a URL. + MOZ_ASSERT(url); + + // Step 1.2.8. If the serialization of url does not start with the + // serialization of resolutionResult, then throw a TypeError indicating + // that resolution of normalizedSpecifier was blocked due to it + // backtracking above its prefix specifierKey. + // + // This will terminate the entire resolve a module specifier algorithm, + // without any further fallbacks. + if (!StringBeginsWith(url->GetSpecOrDefault(), + resolutionResult->GetSpecOrDefault())) { + LOG( + ("ImportMap::ResolveImportsMatch normalizedSpecifier: %s, " + "specifierKey: %s, " + "url %s does not start with resolutionResult %s.", + NS_ConvertUTF16toUTF8(aNormalizedSpecifier).get(), + NS_ConvertUTF16toUTF8(specifierKey).get(), + url->GetSpecOrDefault().get(), + resolutionResult->GetSpecOrDefault().get())); + return Err(ResolveError::BlockedByBacktrackingPrefix); + } + + // Step 1.2.9. Return url. + return std::move(url); + } + } + + // Step 2. Return null. + return nsCOMPtr(nullptr); +} + +// https://wicg.github.io/import-maps/#resolve-a-module-specifier +// static +ResolveResult ImportMap::ResolveModuleSpecifier(ImportMap* aImportMap, + ScriptLoaderInterface* aLoader, + LoadedScript* aScript, + const nsAString& aSpecifier) { + LOG(("ImportMap::ResolveModuleSpecifier specifier: %s", + NS_ConvertUTF16toUTF8(aSpecifier).get())); + nsCOMPtr baseURL; + if (aScript) { + baseURL = aScript->BaseURL(); + } else { + baseURL = aLoader->GetBaseURI(); + } + + // Step 6. Let asURL be the result of parsing a URL-like import specifier + // given specifier and baseURL. + // + // Impl note: Step 5 is done below if aImportMap exists. + nsCOMPtr asURL = ParseURLLikeImportSpecifier(aSpecifier, baseURL); + + if (aImportMap) { + // Step 5. Let baseURLString be baseURL, serialized. + nsCString baseURLString = baseURL->GetSpecOrDefault(); + + // Step 7. Let normalizedSpecifier be the serialization of asURL, if asURL + // is non-null; otherwise, specifier. + nsAutoString normalizedSpecifier = + asURL ? NS_ConvertUTF8toUTF16(asURL->GetSpecOrDefault()) + : nsAutoString{aSpecifier}; + + // Step 8. For each scopePrefix → scopeImports of importMap’s scopes, + for (auto&& [scopePrefix, scopeImports] : *aImportMap->mScopes) { + // Step 8.1. If scopePrefix is baseURLString, or if scopePrefix ends with + // U+002F (/) and baseURLString starts with scopePrefix, then: + if (scopePrefix.Equals(baseURLString) || + (StringEndsWith(scopePrefix, "/"_ns) && + StringBeginsWith(baseURLString, scopePrefix))) { + // Step 8.1.1. Let scopeImportsMatch be the result of resolving an + // imports match given normalizedSpecifier, asURL, and scopeImports. + auto result = + ResolveImportsMatch(normalizedSpecifier, asURL, scopeImports.get()); + if (result.isErr()) { + return result.propagateErr(); + } + + nsCOMPtr scopeImportsMatch = result.unwrap(); + // Step 8.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 9. Let topLevelImportsMatch be the result of resolving an imports + // match given normalizedSpecifier, asURL, and importMap’s imports. + auto result = ResolveImportsMatch(normalizedSpecifier, asURL, + aImportMap->mImports.get()); + if (result.isErr()) { + return result.propagateErr(); + } + nsCOMPtr topLevelImportsMatch = result.unwrap(); + + // Step 10. If topLevelImportsMatch is not null, then return + // topLevelImportsMatch. + if (topLevelImportsMatch) { + LOG(("ImportMap::ResolveModuleSpecifier returns topLevelImportsMatch: %s", + topLevelImportsMatch->GetSpecOrDefault().get())); + return WrapNotNull(topLevelImportsMatch); + } + } + + // Step 11. 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 12. Throw a TypeError indicating that specifier was a bare specifier, + // but was not remapped to anything by importMap. + return Err(ResolveError::InvalidBareSpecifier); +} + #undef LOG #undef LOG_ENABLED } // namespace JS::loader diff --git a/js/loader/ImportMap.h b/js/loader/ImportMap.h index 31f4957adaf6..eb438ee86117 100644 --- a/js/loader/ImportMap.h +++ b/js/loader/ImportMap.h @@ -17,6 +17,7 @@ #include "mozilla/UniquePtr.h" #include "nsStringFwd.h" #include "nsTArray.h" +#include "ResolveResult.h" struct JSContext; class nsIScriptElement; @@ -75,6 +76,17 @@ class ImportMap { JSContext* aCx, JS::SourceText& aInput, nsIURI* aBaseURL, const ReportWarningHelper& aWarning); + /** + * This implements "Resolve a module specifier" algorithm defined in the + * Import maps spec. + * + * See https://wicg.github.io/import-maps/#resolve-a-module-specifier + */ + static ResolveResult ResolveModuleSpecifier(ImportMap* aImportMap, + ScriptLoaderInterface* aLoader, + LoadedScript* aScript, + const nsAString& aSpecifier); + // Logging static mozilla::LazyLogModule gImportMapLog; diff --git a/js/loader/ModuleLoaderBase.cpp b/js/loader/ModuleLoaderBase.cpp index 143433ebb80b..bfcf7ab458ef 100644 --- a/js/loader/ModuleLoaderBase.cpp +++ b/js/loader/ModuleLoaderBase.cpp @@ -579,12 +579,23 @@ nsresult ModuleLoaderBase::HandleResolveFailure( ResolveResult ModuleLoaderBase::ResolveModuleSpecifier( LoadedScript* aScript, const nsAString& aSpecifier) { + bool importMapsEnabled = Preferences::GetBool("dom.importMaps.enabled"); + // 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 currently disallowed as these may be given - // special meanings in the future. + // Bareword module specifiers are handled in Import maps. nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), aSpecifier); diff --git a/js/loader/ResolveResult.h b/js/loader/ResolveResult.h index ac586e591dec..156ea869f241 100644 --- a/js/loader/ResolveResult.h +++ b/js/loader/ResolveResult.h @@ -15,6 +15,10 @@ namespace JS::loader { enum class ResolveError : uint8_t { ModuleResolveFailure, + BlockedByNullEntry, + BlockedByAfterPrefix, + BlockedByBacktrackingPrefix, + InvalidBareSpecifier }; struct ResolveErrorInfo { @@ -22,6 +26,14 @@ struct ResolveErrorInfo { switch (aError) { case ResolveError::ModuleResolveFailure: return "ModuleResolveFailure"; + case ResolveError::BlockedByNullEntry: + return "ImportMapResolutionBlockedByNullEntry"; + case ResolveError::BlockedByAfterPrefix: + return "ImportMapResolutionBlockedByAfterPrefix"; + case ResolveError::BlockedByBacktrackingPrefix: + return "ImportMapResolutionBlockedByBacktrackingPrefix"; + case ResolveError::InvalidBareSpecifier: + return "ImportMapResolveInvalidBareSpecifier"; default: MOZ_CRASH("Unexpected ResolveError value"); }