diff options
Diffstat (limited to '')
-rw-r--r-- | xpcom/components/ManifestParser.cpp | 678 |
1 files changed, 678 insertions, 0 deletions
diff --git a/xpcom/components/ManifestParser.cpp b/xpcom/components/ManifestParser.cpp new file mode 100644 index 0000000000..88ee06d78d --- /dev/null +++ b/xpcom/components/ManifestParser.cpp @@ -0,0 +1,678 @@ +/* -*- 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/ArrayUtils.h" +#include "mozilla/Printf.h" +#include "mozilla/UniquePtr.h" + +#include "ManifestParser.h" + +#include <string.h> + +#include "prio.h" +#if defined(XP_WIN) +# include <windows.h> +#elif defined(MOZ_WIDGET_COCOA) +# include <CoreServices/CoreServices.h> +# include "nsCocoaFeatures.h" +#elif defined(MOZ_WIDGET_GTK) +# include <gtk/gtk.h> +#endif + +#ifdef MOZ_WIDGET_ANDROID +# include "AndroidBuild.h" +# include "mozilla/java/GeckoAppShellWrappers.h" +#endif + +#ifdef MOZ_BACKGROUNDTASKS +# include "mozilla/BackgroundTasks.h" +#endif + +#include "mozilla/Services.h" + +#include "nsCRT.h" +#include "nsConsoleMessage.h" +#include "nsTextFormatter.h" +#include "nsVersionComparator.h" +#include "nsXPCOMCIDInternal.h" + +#include "nsIConsoleService.h" +#include "nsIScriptError.h" +#include "nsIXULAppInfo.h" +#include "nsIXULRuntime.h" + +using namespace mozilla; + +struct ManifestDirective { + const char* directive; + int argc; + + bool ischrome; + + // The contentaccessible flags only apply to content/resource directives. + bool contentflags; + + // Function to handle this directive. This isn't a union because C++ still + // hasn't learned how to initialize unions in a sane way. + void (nsComponentManagerImpl::*mgrfunc)( + nsComponentManagerImpl::ManifestProcessingContext& aCx, int aLineNo, + char* const* aArgv); + void (nsChromeRegistry::*regfunc)( + nsChromeRegistry::ManifestProcessingContext& aCx, int aLineNo, + char* const* aArgv, int aFlags); +}; +static const ManifestDirective kParsingTable[] = { + // clang-format off + { + "manifest", 1, true, false, + &nsComponentManagerImpl::ManifestManifest, nullptr, + }, + { + "category", 3, false, false, + &nsComponentManagerImpl::ManifestCategory, nullptr, + }, + { + "content", 2, true, true, + nullptr, &nsChromeRegistry::ManifestContent, + }, + { + "locale", 3, true, false, + nullptr, &nsChromeRegistry::ManifestLocale, + }, + { + "skin", 3, true, false, + nullptr, &nsChromeRegistry::ManifestSkin, + }, + { + // NB: note that while skin manifests can use this, they are only allowed + // to use it for chrome://../skin/ URLs + "override", 2, true, false, + nullptr, &nsChromeRegistry::ManifestOverride, + }, + { + "resource", 2, false, true, + nullptr, &nsChromeRegistry::ManifestResource, + } + // clang-format on +}; + +static const char kWhitespace[] = "\t "; + +static bool IsNewline(char aChar) { return aChar == '\n' || aChar == '\r'; } + +void LogMessage(const char* aMsg, ...) { + MOZ_ASSERT(nsComponentManagerImpl::gComponentManager); + + nsCOMPtr<nsIConsoleService> console = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + if (!console) { + return; + } + + va_list args; + va_start(args, aMsg); + SmprintfPointer formatted(mozilla::Vsmprintf(aMsg, args)); + va_end(args); + + nsCOMPtr<nsIConsoleMessage> error = + new nsConsoleMessage(NS_ConvertUTF8toUTF16(formatted.get())); + console->LogMessage(error); +} + +void LogMessageWithContext(FileLocation& aFile, uint32_t aLineNumber, + const char* aMsg, ...) { + va_list args; + va_start(args, aMsg); + SmprintfPointer formatted(mozilla::Vsmprintf(aMsg, args)); + va_end(args); + if (!formatted) { + return; + } + + MOZ_ASSERT(nsComponentManagerImpl::gComponentManager); + + nsCString file; + aFile.GetURIString(file); + + nsCOMPtr<nsIScriptError> error = do_CreateInstance(NS_SCRIPTERROR_CONTRACTID); + if (!error) { + // This can happen early in component registration. Fall back to a + // generic console message. + LogMessage("Warning: in '%s', line %i: %s", file.get(), aLineNumber, + formatted.get()); + return; + } + + nsCOMPtr<nsIConsoleService> console = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + if (!console) { + return; + } + + nsresult rv = error->Init( + NS_ConvertUTF8toUTF16(formatted.get()), NS_ConvertUTF8toUTF16(file), + u""_ns, aLineNumber, 0, nsIScriptError::warningFlag, + "chrome registration"_ns, false /* from private window */, + true /* from chrome context */); + if (NS_FAILED(rv)) { + return; + } + + console->LogMessage(error); +} + +/** + * Check for a modifier flag of the following forms: + * "flag" (same as "true") + * "flag=yes|true|1" + * "flag="no|false|0" + * @param aFlag The flag to compare. + * @param aData The tokenized data to check; this is lowercased + * before being passed in. + * @param aResult If the flag is found, the value is assigned here. + * @return Whether the flag was handled. + */ +static bool CheckFlag(const nsAString& aFlag, const nsAString& aData, + bool& aResult) { + if (!StringBeginsWith(aData, aFlag)) { + return false; + } + + if (aFlag.Length() == aData.Length()) { + // the data is simply "flag", which is the same as "flag=yes" + aResult = true; + return true; + } + + if (aData.CharAt(aFlag.Length()) != '=') { + // the data is "flag2=", which is not anything we care about + return false; + } + + if (aData.Length() == aFlag.Length() + 1) { + aResult = false; + return true; + } + + switch (aData.CharAt(aFlag.Length() + 1)) { + case '1': + case 't': // true + case 'y': // yes + aResult = true; + return true; + + case '0': + case 'f': // false + case 'n': // no + aResult = false; + return true; + } + + return false; +} + +enum TriState { eUnspecified, eBad, eOK }; + +/** + * Check for a modifier flag of the following form: + * "flag=string" + * "flag!=string" + * @param aFlag The flag to compare. + * @param aData The tokenized data to check; this is lowercased + * before being passed in. + * @param aValue The value that is expected. + * @param aResult If this is "ok" when passed in, this is left alone. + * Otherwise if the flag is found it is set to eBad or eOK. + * @return Whether the flag was handled. + */ +static bool CheckStringFlag(const nsAString& aFlag, const nsAString& aData, + const nsAString& aValue, TriState& aResult) { + if (aData.Length() < aFlag.Length() + 1) { + return false; + } + + if (!StringBeginsWith(aData, aFlag)) { + return false; + } + + bool comparison = true; + if (aData[aFlag.Length()] != '=') { + if (aData[aFlag.Length()] == '!' && aData.Length() >= aFlag.Length() + 2 && + aData[aFlag.Length() + 1] == '=') { + comparison = false; + } else { + return false; + } + } + + if (aResult != eOK) { + nsDependentSubstring testdata = + Substring(aData, aFlag.Length() + (comparison ? 1 : 2)); + if (testdata.Equals(aValue)) { + aResult = comparison ? eOK : eBad; + } else { + aResult = comparison ? eBad : eOK; + } + } + + return true; +} + +static bool CheckOsFlag(const nsAString& aFlag, const nsAString& aData, + const nsAString& aValue, TriState& aResult) { + bool result = CheckStringFlag(aFlag, aData, aValue, aResult); +#if defined(XP_UNIX) && !defined(XP_DARWIN) && !defined(ANDROID) + if (result && aResult == eBad) { + result = CheckStringFlag(aFlag, aData, u"likeunix"_ns, aResult); + } +#endif + return result; +} + +/** + * Check for a modifier flag of the following form: + * "flag=version" + * "flag<=version" + * "flag<version" + * "flag>=version" + * "flag>version" + * @param aFlag The flag to compare. + * @param aData The tokenized data to check; this is lowercased + * before being passed in. + * @param aValue The value that is expected. If this is empty then no + * comparison will match. + * @param aResult If this is eOK when passed in, this is left alone. + * Otherwise if the flag is found it is set to eBad or eOK. + * @return Whether the flag was handled. + */ + +#define COMPARE_EQ 1 << 0 +#define COMPARE_LT 1 << 1 +#define COMPARE_GT 1 << 2 + +static bool CheckVersionFlag(const nsString& aFlag, const nsString& aData, + const nsString& aValue, TriState& aResult) { + if (aData.Length() < aFlag.Length() + 2) { + return false; + } + + if (!StringBeginsWith(aData, aFlag)) { + return false; + } + + if (aValue.Length() == 0) { + if (aResult != eOK) { + aResult = eBad; + } + return true; + } + + uint32_t comparison; + nsAutoString testdata; + + switch (aData[aFlag.Length()]) { + case '=': + comparison = COMPARE_EQ; + testdata = Substring(aData, aFlag.Length() + 1); + break; + + case '<': + if (aData[aFlag.Length() + 1] == '=') { + comparison = COMPARE_EQ | COMPARE_LT; + testdata = Substring(aData, aFlag.Length() + 2); + } else { + comparison = COMPARE_LT; + testdata = Substring(aData, aFlag.Length() + 1); + } + break; + + case '>': + if (aData[aFlag.Length() + 1] == '=') { + comparison = COMPARE_EQ | COMPARE_GT; + testdata = Substring(aData, aFlag.Length() + 2); + } else { + comparison = COMPARE_GT; + testdata = Substring(aData, aFlag.Length() + 1); + } + break; + + default: + return false; + } + + if (testdata.Length() == 0) { + return false; + } + + if (aResult != eOK) { + int32_t c = mozilla::CompareVersions(NS_ConvertUTF16toUTF8(aValue).get(), + NS_ConvertUTF16toUTF8(testdata).get()); + if ((c == 0 && comparison & COMPARE_EQ) || + (c < 0 && comparison & COMPARE_LT) || + (c > 0 && comparison & COMPARE_GT)) { + aResult = eOK; + } else { + aResult = eBad; + } + } + + return true; +} + +// In-place conversion of ascii characters to lower case +static void ToLowerCase(char* aToken) { + for (; *aToken; ++aToken) { + *aToken = NS_ToLower(*aToken); + } +} + +namespace { + +struct CachedDirective { + int lineno; + char* argv[4]; +}; + +} // namespace + +void ParseManifest(NSLocationType aType, FileLocation& aFile, char* aBuf, + bool aChromeOnly) { + nsComponentManagerImpl::ManifestProcessingContext mgrcx(aType, aFile, + aChromeOnly); + nsChromeRegistry::ManifestProcessingContext chromecx(aType, aFile); + nsresult rv; + + constexpr auto kContentAccessible = u"contentaccessible"_ns; + constexpr auto kRemoteEnabled = u"remoteenabled"_ns; + constexpr auto kRemoteRequired = u"remoterequired"_ns; + constexpr auto kApplication = u"application"_ns; + constexpr auto kAppVersion = u"appversion"_ns; + constexpr auto kGeckoVersion = u"platformversion"_ns; + constexpr auto kOs = u"os"_ns; + constexpr auto kOsVersion = u"osversion"_ns; + constexpr auto kABI = u"abi"_ns; + constexpr auto kProcess = u"process"_ns; +#if defined(MOZ_WIDGET_ANDROID) + constexpr auto kTablet = u"tablet"_ns; +#endif + // You might expect this to be guarded by MOZ_BACKGROUNDTASKS, but it's not + // possible to have conditional manifest contents, so we need to recognize and + // discard these tokens even when MOZ_BACKGROUNDTASKS is not set. + constexpr auto kBackgroundTask = u"backgroundtask"_ns; + + constexpr auto kMain = u"main"_ns; + constexpr auto kContent = u"content"_ns; + + // Obsolete + constexpr auto kXPCNativeWrappers = u"xpcnativewrappers"_ns; + + nsAutoString appID; + nsAutoString appVersion; + nsAutoString geckoVersion; + nsAutoString osTarget; + nsAutoString abi; + nsAutoString process; + + nsCOMPtr<nsIXULAppInfo> xapp(do_GetService(XULAPPINFO_SERVICE_CONTRACTID)); + if (xapp) { + nsAutoCString s; + rv = xapp->GetID(s); + if (NS_SUCCEEDED(rv)) { + CopyUTF8toUTF16(s, appID); + } + + rv = xapp->GetVersion(s); + if (NS_SUCCEEDED(rv)) { + CopyUTF8toUTF16(s, appVersion); + } + + rv = xapp->GetPlatformVersion(s); + if (NS_SUCCEEDED(rv)) { + CopyUTF8toUTF16(s, geckoVersion); + } + + nsCOMPtr<nsIXULRuntime> xruntime(do_QueryInterface(xapp)); + if (xruntime) { + rv = xruntime->GetOS(s); + if (NS_SUCCEEDED(rv)) { + ToLowerCase(s); + CopyUTF8toUTF16(s, osTarget); + } + + rv = xruntime->GetXPCOMABI(s); + if (NS_SUCCEEDED(rv) && osTarget.Length()) { + ToLowerCase(s); + CopyUTF8toUTF16(s, abi); + abi.Insert(char16_t('_'), 0); + abi.Insert(osTarget, 0); + } + } + } + + nsAutoString osVersion; +#if defined(XP_WIN) +# pragma warning(push) +# pragma warning(disable : 4996) // VC12+ deprecates GetVersionEx + OSVERSIONINFO info = {sizeof(OSVERSIONINFO)}; + if (GetVersionEx(&info)) { + nsTextFormatter::ssprintf(osVersion, u"%ld.%ld", info.dwMajorVersion, + info.dwMinorVersion); + } +# pragma warning(pop) +#elif defined(MOZ_WIDGET_COCOA) + SInt32 majorVersion = nsCocoaFeatures::macOSVersionMajor(); + SInt32 minorVersion = nsCocoaFeatures::macOSVersionMinor(); + nsTextFormatter::ssprintf(osVersion, u"%ld.%ld", majorVersion, minorVersion); +#elif defined(MOZ_WIDGET_GTK) + nsTextFormatter::ssprintf(osVersion, u"%ld.%ld", gtk_major_version, + gtk_minor_version); +#elif defined(MOZ_WIDGET_ANDROID) + bool isTablet = false; + if (jni::IsAvailable()) { + jni::String::LocalRef release = java::sdk::Build::VERSION::RELEASE(); + osVersion.Assign(release->ToString()); + isTablet = java::GeckoAppShell::IsTablet(); + } +#endif + + if (XRE_IsContentProcess()) { + process = kContent; + } else { + process = kMain; + } + + char* token; + char* newline = aBuf; + uint32_t line = 0; + + // outer loop tokenizes by newline + while (*newline) { + while (*newline && IsNewline(*newline)) { + ++newline; + ++line; + } + if (!*newline) { + break; + } + + token = newline; + while (*newline && !IsNewline(*newline)) { + ++newline; + } + + if (*newline) { + *newline = '\0'; + ++newline; + } + ++line; + + if (*token == '#') { // ignore lines that begin with # as comments + continue; + } + + char* whitespace = token; + token = nsCRT::strtok(whitespace, kWhitespace, &whitespace); + if (!token) { + continue; + } + + const ManifestDirective* directive = nullptr; + for (const ManifestDirective* d = kParsingTable; + d < ArrayEnd(kParsingTable); ++d) { + if (!strcmp(d->directive, token)) { + directive = d; + break; + } + } + + if (!directive) { + LogMessageWithContext( + aFile, line, "Ignoring unrecognized chrome manifest directive '%s'.", + token); + continue; + } + + if (!directive->ischrome && NS_BOOTSTRAPPED_LOCATION == aType) { + LogMessageWithContext( + aFile, line, + "Bootstrapped manifest not allowed to use '%s' directive.", token); + continue; + } + + NS_ASSERTION(directive->argc < 4, "Need to reset argv array length"); + char* argv[4]; + for (int i = 0; i < directive->argc; ++i) { + argv[i] = nsCRT::strtok(whitespace, kWhitespace, &whitespace); + } + + if (!argv[directive->argc - 1]) { + LogMessageWithContext(aFile, line, + "Not enough arguments for chrome manifest " + "directive '%s', expected %i.", + token, directive->argc); + continue; + } + + bool ok = true; + TriState stAppVersion = eUnspecified; + TriState stGeckoVersion = eUnspecified; + TriState stApp = eUnspecified; + TriState stOsVersion = eUnspecified; + TriState stOs = eUnspecified; + TriState stABI = eUnspecified; + TriState stProcess = eUnspecified; +#if defined(MOZ_WIDGET_ANDROID) + TriState stTablet = eUnspecified; +#endif +#ifdef MOZ_BACKGROUNDTASKS + // When in background task mode, default to not registering + // category directivies unless backgroundtask=1 is specified. + TriState stBackgroundTask = (BackgroundTasks::IsBackgroundTaskMode() && + strcmp("category", directive->directive) == 0) + ? eBad + : eUnspecified; +#endif + int flags = 0; + + while ((token = nsCRT::strtok(whitespace, kWhitespace, &whitespace)) && + ok) { + ToLowerCase(token); + NS_ConvertASCIItoUTF16 wtoken(token); + + if (CheckStringFlag(kApplication, wtoken, appID, stApp) || + CheckOsFlag(kOs, wtoken, osTarget, stOs) || + CheckStringFlag(kABI, wtoken, abi, stABI) || + CheckStringFlag(kProcess, wtoken, process, stProcess) || + CheckVersionFlag(kOsVersion, wtoken, osVersion, stOsVersion) || + CheckVersionFlag(kAppVersion, wtoken, appVersion, stAppVersion) || + CheckVersionFlag(kGeckoVersion, wtoken, geckoVersion, + stGeckoVersion)) { + continue; + } + +#if defined(MOZ_WIDGET_ANDROID) + bool tablet = false; + if (CheckFlag(kTablet, wtoken, tablet)) { + stTablet = (tablet == isTablet) ? eOK : eBad; + continue; + } +#endif + + // You might expect this to be guarded by MOZ_BACKGROUNDTASKS, it's not + // possible to have conditional manifest contents. + bool flag; + if (CheckFlag(kBackgroundTask, wtoken, flag)) { +#if defined(MOZ_BACKGROUNDTASKS) + // Background task mode is active: filter. + stBackgroundTask = + (flag == BackgroundTasks::IsBackgroundTaskMode()) ? eOK : eBad; +#endif /* defined(MOZ_BACKGROUNDTASKS) */ + continue; + } + + if (directive->contentflags) { + bool flag; + if (CheckFlag(kContentAccessible, wtoken, flag)) { + if (flag) flags |= nsChromeRegistry::CONTENT_ACCESSIBLE; + continue; + } + if (CheckFlag(kRemoteEnabled, wtoken, flag)) { + if (flag) flags |= nsChromeRegistry::REMOTE_ALLOWED; + continue; + } + if (CheckFlag(kRemoteRequired, wtoken, flag)) { + if (flag) flags |= nsChromeRegistry::REMOTE_REQUIRED; + continue; + } + } + + bool xpcNativeWrappers = true; // Dummy for CheckFlag. + if (CheckFlag(kXPCNativeWrappers, wtoken, xpcNativeWrappers)) { + LogMessageWithContext( + aFile, line, "Ignoring obsolete chrome registration modifier '%s'.", + token); + continue; + } + + LogMessageWithContext( + aFile, line, "Unrecognized chrome manifest modifier '%s'.", token); + ok = false; + } + + if (!ok || stApp == eBad || stAppVersion == eBad || + stGeckoVersion == eBad || stOs == eBad || stOsVersion == eBad || +#ifdef MOZ_WIDGET_ANDROID + stTablet == eBad || +#endif +#ifdef MOZ_BACKGROUNDTASKS + stBackgroundTask == eBad || +#endif + stABI == eBad || stProcess == eBad) { + continue; + } + + if (directive->regfunc) { + if (GeckoProcessType_Default != XRE_GetProcessType()) { + continue; + } + + if (!nsChromeRegistry::gChromeRegistry) { + nsCOMPtr<nsIChromeRegistry> cr = mozilla::services::GetChromeRegistry(); + if (!nsChromeRegistry::gChromeRegistry) { + LogMessageWithContext(aFile, line, + "Chrome registry isn't available yet."); + continue; + } + } + + (nsChromeRegistry::gChromeRegistry->*(directive->regfunc))(chromecx, line, + argv, flags); + } else if (directive->ischrome || !aChromeOnly) { + (nsComponentManagerImpl::gComponentManager->*(directive->mgrfunc))( + mgrcx, line, argv); + } + } +} |