diff --git a/toolkit/components/aboutthirdparty/AboutThirdParty.cpp b/toolkit/components/aboutthirdparty/AboutThirdParty.cpp index 0549cc268086..b5a7dfb2cbf1 100644 --- a/toolkit/components/aboutthirdparty/AboutThirdParty.cpp +++ b/toolkit/components/aboutthirdparty/AboutThirdParty.cpp @@ -6,10 +6,12 @@ #include "AboutThirdParty.h" +#include "AboutThirdPartyUtils.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/dom/Promise.h" #include "mozilla/NativeNt.h" #include "mozilla/StaticPtr.h" +#include "MsiDatabase.h" #include "nsComponentManagerUtils.h" #include "nsIWindowsRegKey.h" #include "nsThreadUtils.h" @@ -56,6 +58,292 @@ void EnumSubkeys(nsIWindowsRegKey* aRegBase, const CallbackT& aCallback) { } // anonymous namespace +InstallLocationComparator::InstallLocationComparator(const nsAString& aFilePath) + : mFilePath(aFilePath) {} + +int InstallLocationComparator::operator()( + const InstallLocationT& aLocation) const { + // Firstly we check whether mFilePath begins with aLocation. + // If yes, mFilePath is a part of the target installation, + // so we return 0 showing match. + const nsAString& location = aLocation.first(); + size_t locationLen = location.Length(); + if (locationLen <= mFilePath.Length() && + nsCaseInsensitiveStringComparator(mFilePath.BeginReading(), + location.BeginReading(), locationLen, + locationLen) == 0) { + return 0; + } + + return CompareIgnoreCase(mFilePath, location); +} + +// The InstalledApplications class behaves like Chrome's InstalledApplications, +// which collects installed applications from two resources below. +// +// 1) Path strings in MSI package components +// An MSI package is consisting of multiple components. This class collects +// MSI components representing a file and stores them as a hash table. +// +// 2) Install location paths in the InstallLocation registry value +// If an application's installer is not MSI but sets the InstallLocation +// registry value, we can use it to search for an application by comparing +// a target module is located under that location path. This class stores +// location path strings as a sorted array so that we can binary-search it. +class InstalledApplications final { + // Limit the number of entries to avoid consuming too much memory + constexpr static uint32_t kMaxComponents = 1000000; + constexpr static uint32_t kMaxInstallLocations = 1000; + + nsCOMPtr mInstallerData; + nsCOMPtr mCurrentApp; + ComponentPathMapT mComponentPaths; + nsTArray mLocations; + + void AddInstallLocation(nsIWindowsRegKey* aProductSubKey) { + nsString location; + if (NS_FAILED( + aProductSubKey->ReadStringValue(u"InstallLocation"_ns, location)) || + location.IsEmpty()) { + return; + } + + if (location.Last() != u'\\') { + location.Append(u'\\'); + } + + mLocations.EmplaceBack(location, this->mCurrentApp); + } + + void AddComponentGuid(const nsString& aPackedProductGuid, + const nsString& aPackedComponentGuid) { + nsAutoString componentSubkey(L"Components\\"); + componentSubkey += aPackedComponentGuid; + + // Pick a first value in the subkeys under |componentSubkey|. + nsString componentPath; + + EnumSubkeys(mInstallerData, [&aPackedProductGuid, &componentSubkey, + &componentPath](const nsString& aSid, + nsIWindowsRegKey* aSidSubkey) { + // If we have a value in |componentPath|, the loop should + // have been stopped. + MOZ_ASSERT(componentPath.IsEmpty()); + + nsCOMPtr compKey; + nsresult rv = + aSidSubkey->OpenChild(componentSubkey, nsIWindowsRegKey::ACCESS_READ, + getter_AddRefs(compKey)); + if (NS_FAILED(rv)) { + return CallbackResult::Continue; + } + + nsString compData; + if (NS_FAILED(compKey->ReadStringValue(aPackedProductGuid, compData))) { + return CallbackResult::Continue; + } + + if (!CorrectMsiComponentPath(compData)) { + return CallbackResult::Continue; + } + + componentPath = std::move(compData); + return CallbackResult::Stop; + }); + + if (componentPath.IsEmpty()) { + return; + } + + // Use a full path as a key rather than a leaf name because + // the same name's module can be installed under system32 + // and syswow64. + mComponentPaths.WithEntryHandle(componentPath, [this](auto&& addPtr) { + if (addPtr) { + // If the same file appeared in multiple installations, we set null + // for its value because there is no way to know which installation is + // the real owner. + addPtr.Data() = nullptr; + } else { + addPtr.Insert(this->mCurrentApp); + } + }); + } + + void AddProduct(const nsString& aProductId, + nsIWindowsRegKey* aProductSubKey) { + nsString displayName; + if (NS_FAILED( + aProductSubKey->ReadStringValue(u"DisplayName"_ns, displayName)) || + displayName.IsEmpty()) { + // Skip if no name is found. + return; + } + + nsString publisher; + if (NS_SUCCEEDED( + aProductSubKey->ReadStringValue(u"Publisher"_ns, publisher)) && + publisher.EqualsIgnoreCase("Microsoft") && + publisher.EqualsIgnoreCase("Microsoft Corporation")) { + // Skip if the publisher is Microsoft because it's not a third-party. + // We don't skip an application without the publisher name. + return; + } + + mCurrentApp = + new InstalledApplication(std::move(displayName), std::move(publisher)); + // Try an MSI database first because it's more accurate, + // then fall back to the InstallLocation key. + do { + if (!mInstallerData) { + break; + } + + nsAutoString packedProdGuid; + if (!MsiPackGuid(aProductId, packedProdGuid)) { + break; + } + + auto db = MsiDatabase::FromProductId(aProductId.get()); + if (db.isNothing()) { + break; + } + + db->ExecuteSingleColumnQuery( + L"SELECT DISTINCT ComponentId FROM Component", + [this, &packedProdGuid](const wchar_t* aComponentGuid) { + if (this->mComponentPaths.Count() >= kMaxComponents) { + return MsiDatabase::CallbackResult::Stop; + } + + nsAutoString packedComponentGuid; + if (MsiPackGuid(nsDependentString(aComponentGuid), + packedComponentGuid)) { + this->AddComponentGuid(packedProdGuid, packedComponentGuid); + } + + return MsiDatabase::CallbackResult::Continue; + }); + + // We've decided to collect data from an MSI database. + // Exiting the function. + return; + } while (false); + + if (mLocations.Length() >= kMaxInstallLocations) { + return; + } + + // If we cannot use an MSI database for any reason, + // try the InstallLocation key. + AddInstallLocation(aProductSubKey); + } + + public: + InstalledApplications() { + nsresult rv; + nsCOMPtr regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv); + if (NS_SUCCEEDED(rv) && + NS_SUCCEEDED(regKey->Open( + nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, + u"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\" + u"Installer\\UserData"_ns, + nsIWindowsRegKey::ACCESS_READ | nsIWindowsRegKey::WOW64_64))) { + mInstallerData.swap(regKey); + } + } + ~InstalledApplications() = default; + + InstalledApplications(InstalledApplications&&) = delete; + InstalledApplications& operator=(InstalledApplications&&) = delete; + InstalledApplications(const InstalledApplications&) = delete; + InstalledApplications& operator=(const InstalledApplications&) = delete; + + void Collect(ComponentPathMapT& aOutComponentPaths, + nsTArray& aOutLocations) { + const nsLiteralString kUninstallKey( + u"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall"); + + static const uint16_t sProcessor = []() -> uint16_t { + SYSTEM_INFO si; + ::GetSystemInfo(&si); + return si.wProcessorArchitecture; + }(); + + nsresult rv; + nsCOMPtr regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv); + if (NS_FAILED(rv)) { + return; + } + + switch (sProcessor) { + case PROCESSOR_ARCHITECTURE_INTEL: + rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, + kUninstallKey, nsIWindowsRegKey::ACCESS_READ); + if (NS_SUCCEEDED(rv)) { + EnumSubkeys(regKey, [this](const nsString& aProductId, + nsIWindowsRegKey* aProductSubKey) { + this->AddProduct(aProductId, aProductSubKey); + return CallbackResult::Continue; + }); + } + break; + + case PROCESSOR_ARCHITECTURE_AMD64: + // A 64-bit application may be installed by a 32-bit installer, + // or vice versa. So we enumerate both views regardless of + // the process's (not processor's) bitness. + rv = regKey->Open( + nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, kUninstallKey, + nsIWindowsRegKey::ACCESS_READ | nsIWindowsRegKey::WOW64_64); + if (NS_SUCCEEDED(rv)) { + EnumSubkeys(regKey, [this](const nsString& aProductId, + nsIWindowsRegKey* aProductSubKey) { + this->AddProduct(aProductId, aProductSubKey); + return CallbackResult::Continue; + }); + } + rv = regKey->Open( + nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, kUninstallKey, + nsIWindowsRegKey::ACCESS_READ | nsIWindowsRegKey::WOW64_32); + if (NS_SUCCEEDED(rv)) { + EnumSubkeys(regKey, [this](const nsString& aProductId, + nsIWindowsRegKey* aProductSubKey) { + this->AddProduct(aProductId, aProductSubKey); + return CallbackResult::Continue; + }); + } + break; + + default: + MOZ_ASSERT(false, "Unsupported CPU architecture"); + return; + } + + // The "HKCU\SOFTWARE\" subtree is shared between the 32-bits and 64 bits + // views. No need to enumerate wow6432node for HKCU. + // https://docs.microsoft.com/en-us/windows/win32/winprog64/shared-registry-keys + rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, kUninstallKey, + nsIWindowsRegKey::ACCESS_READ); + if (NS_SUCCEEDED(rv)) { + EnumSubkeys(regKey, [this](const nsString& aProductId, + nsIWindowsRegKey* aProductSubKey) { + this->AddProduct(aProductId, aProductSubKey); + return CallbackResult::Continue; + }); + } + + aOutComponentPaths.SwapElements(mComponentPaths); + + mLocations.Sort([](const InstallLocationT& aA, const InstallLocationT& aB) { + return CompareIgnoreCase(aA.first(), aB.first()); + }); + aOutLocations.SwapElements(mLocations); + } +}; + class KnownModule final { static KnownModule sKnownExtensions[static_cast(KnownModuleType::Last)]; @@ -317,8 +605,25 @@ namespace mozilla { static StaticRefPtr sSingleton; +NS_IMPL_ISUPPORTS(InstalledApplication, nsIInstalledApplication); NS_IMPL_ISUPPORTS(AboutThirdParty, nsIAboutThirdParty); +InstalledApplication::InstalledApplication(nsString&& aAppName, + nsString&& aPublisher) + : mName(std::move(aAppName)), mPublisher(std::move(aPublisher)) {} + +NS_IMETHODIMP +InstalledApplication::GetName(nsAString& aResult) { + aResult = mName; + return NS_OK; +} + +NS_IMETHODIMP +InstalledApplication::GetPublisher(nsAString& aResult) { + aResult = mPublisher; + return NS_OK; +} + /*static*/ already_AddRefed AboutThirdParty::GetSingleton() { if (!sSingleton) { @@ -355,6 +660,9 @@ void AboutThirdParty::BackgroundThread() { self->AddKnownModule(aDllPath, aType); }); + InstalledApplications apps; + apps.Collect(mComponentPaths, mLocations); + mWorkerState = WorkerState::Done; } @@ -395,6 +703,41 @@ NS_IMETHODIMP AboutThirdParty::LookupModuleType(const nsAString& aLeafName, return NS_OK; } +NS_IMETHODIMP AboutThirdParty::LookupApplication( + const nsAString& aModulePath, nsIInstalledApplication** aResult) { + MOZ_ASSERT(NS_IsMainThread()); + + *aResult = nullptr; + if (mWorkerState != WorkerState::Done) { + return NS_OK; + } + + const nsDependentSubstring leaf = nt::GetLeafName(aModulePath); + if (leaf.IsEmpty()) { + return NS_OK; + } + + // Look up the component path's map first because it's more accurate + // than the location's array. + nsCOMPtr app = mComponentPaths.Get(aModulePath); + if (app) { + app.forget(aResult); + return NS_OK; + } + + auto bounds = EqualRange(mLocations, 0, mLocations.Length(), + InstallLocationComparator(aModulePath)); + + // If more than one application includes the module, we return null + // because there is no way to know which is the real owner. + if (bounds.second() - bounds.first() != 1) { + return NS_OK; + } + + app = mLocations[bounds.first()].second(); + app.forget(aResult); + return NS_OK; +} RefPtr AboutThirdParty::CollectSystemInfoAsync() { MOZ_ASSERT(NS_IsMainThread()); diff --git a/toolkit/components/aboutthirdparty/AboutThirdParty.h b/toolkit/components/aboutthirdparty/AboutThirdParty.h index b55ea4e75c5f..124f3d6e2dda 100644 --- a/toolkit/components/aboutthirdparty/AboutThirdParty.h +++ b/toolkit/components/aboutthirdparty/AboutThirdParty.h @@ -9,10 +9,17 @@ #include "mozilla/MozPromise.h" #include "nsIAboutThirdParty.h" +#include "nsInterfaceHashtable.h" +#include "nsTArray.h" #include "nsTHashMap.h" namespace mozilla { +using InstallLocationT = + CompactPair>; +using ComponentPathMapT = nsInterfaceHashtable; + enum class KnownModuleType : uint32_t { Ime = 0, IconOverlay, @@ -29,6 +36,32 @@ enum class KnownModuleType : uint32_t { Last, }; +struct InstallLocationComparator { + const nsAString& mFilePath; + + explicit InstallLocationComparator(const nsAString& aFilePath); + int operator()(const InstallLocationT& aLocation) const; +}; + +class InstalledApplication final : public nsIInstalledApplication { + nsString mName; + nsString mPublisher; + + ~InstalledApplication() = default; + + public: + InstalledApplication() = default; + InstalledApplication(nsString&& aAppName, nsString&& aPublisher); + + InstalledApplication(InstalledApplication&&) = delete; + InstalledApplication& operator=(InstalledApplication&&) = delete; + InstalledApplication(const InstalledApplication&) = delete; + InstalledApplication& operator=(const InstalledApplication&) = delete; + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINSTALLEDAPPLICATION +}; + using BackgroundThreadPromise = MozPromise; @@ -42,6 +75,8 @@ class AboutThirdParty final : public nsIAboutThirdParty { Atomic mWorkerState; RefPtr mPromise; nsTHashMap mKnownModules; + ComponentPathMapT mComponentPaths; + nsTArray mLocations; ~AboutThirdParty() = default; void BackgroundThread(); diff --git a/toolkit/components/aboutthirdparty/AboutThirdPartyUtils.cpp b/toolkit/components/aboutthirdparty/AboutThirdPartyUtils.cpp new file mode 100644 index 000000000000..050975c9da13 --- /dev/null +++ b/toolkit/components/aboutthirdparty/AboutThirdPartyUtils.cpp @@ -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/. */ + +#include "AboutThirdPartyUtils.h" + +#include "nsUnicharUtils.h" + +namespace mozilla { + +int32_t CompareIgnoreCase(const nsAString& aStr1, const nsAString& aStr2) { + uint32_t len1 = aStr1.Length(); + uint32_t len2 = aStr2.Length(); + uint32_t lenMin = XPCOM_MIN(len1, len2); + + int32_t result = nsCaseInsensitiveStringComparator( + aStr1.BeginReading(), aStr2.BeginReading(), lenMin, lenMin); + return result ? result : len1 - len2; +} + +bool MsiPackGuid(const nsAString& aGuid, nsAString& aPacked) { + if (aGuid.Length() != 38 || aGuid.First() != u'{' || aGuid.Last() != u'}') { + return false; + } + + constexpr int kPackedLength = 32; + const uint8_t kIndexMapping[kPackedLength] = { + // clang-format off + 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, + 0x0d, 0x0c, 0x0b, 0x0a, 0x12, 0x11, 0x10, 0x0f, + 0x15, 0x14, 0x17, 0x16, 0x1a, 0x19, 0x1c, 0x1b, + 0x1e, 0x1d, 0x20, 0x1f, 0x22, 0x21, 0x24, 0x23, + // clang-format on + }; + + int index = 0; + aPacked.SetLength(kPackedLength); + for (auto iter = aPacked.BeginWriting(), strEnd = aPacked.EndWriting(); + iter != strEnd; ++iter, ++index) { + *iter = aGuid[kIndexMapping[index]]; + } + + return true; +} + +bool CorrectMsiComponentPath(nsAString& aPath) { + if (aPath.Length() < 3 || !aPath.BeginReading()[0]) { + return false; + } + + char16_t* strBegin = aPath.BeginWriting(); + + if (strBegin[1] == u'?') { + strBegin[1] = strBegin[0] == u'\\' ? u'\\' : u':'; + } + + if (strBegin[1] != u':' || strBegin[2] != u'\\') { + return false; + } + + if (aPath.Length() > 3 && aPath.BeginReading()[3] == u'?') { + aPath.ReplaceLiteral(3, 1, u""); + } + return true; +} + +} // namespace mozilla diff --git a/toolkit/components/aboutthirdparty/AboutThirdPartyUtils.h b/toolkit/components/aboutthirdparty/AboutThirdPartyUtils.h new file mode 100644 index 000000000000..c65e5f7c5704 --- /dev/null +++ b/toolkit/components/aboutthirdparty/AboutThirdPartyUtils.h @@ -0,0 +1,36 @@ +/* -*- 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 __AboutThirdPartyUtils_h__ +#define __AboutThirdPartyUtils_h__ + +#include "nsString.h" + +namespace mozilla { + +// Define a custom case-insensitive string comparator wrapping +// nsCaseInsensitiveStringComparator to sort items alphabetically because +// nsCaseInsensitiveStringComparator sorts items by the length first. +int32_t CompareIgnoreCase(const nsAString& aStr1, const nsAString& aStr2); + +// Mimicking the logic in msi!PackGUID to convert a GUID string to +// a packed GUID used as registry keys. +bool MsiPackGuid(const nsAString& aGuid, nsAString& aPacked); + +// Mimicking the validation logic for a path in msi!_GetComponentPath +// +// Accecpted patterns and conversions: +// C:\path --> C:\path +// C?\path --> C:\path +// C:\?path --> C:\path +// +// msi!_GetComponentPath also checks the existence by calling +// RegOpenKeyExW or GetFileAttributesExW, but we don't need it. +bool CorrectMsiComponentPath(nsAString& aPath); + +} // namespace mozilla + +#endif // __AboutThirdPartyUtils_h__ diff --git a/toolkit/components/aboutthirdparty/MsiDatabase.cpp b/toolkit/components/aboutthirdparty/MsiDatabase.cpp new file mode 100644 index 000000000000..6c667072b7fb --- /dev/null +++ b/toolkit/components/aboutthirdparty/MsiDatabase.cpp @@ -0,0 +1,88 @@ +/* -*- 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 "MsiDatabase.h" + +#ifdef UNICODE +# define MSIDBOPEN_READONLY_W MSIDBOPEN_READONLY +# define INSTALLPROPERTY_LOCALPACKAGE_W INSTALLPROPERTY_LOCALPACKAGE +#else +// MSIDBOPEN_READONLY is defined as `(LPCTSTR)0` in msiquery.h, so we need to +// cast it to LPCWSTR. +# define MSIDBOPEN_READONLY_W reinterpret_cast(MSIDBOPEN_READONLY) +// INSTALLPROPERTY_LOCALPACKAGE is defined as `__TEXT("LocalPackage")` in msi.h, +// so we need to define a wchar_t version. +# define INSTALLPROPERTY_LOCALPACKAGE_W L"LocalPackage" +#endif // UNICODE + +namespace mozilla { + +/*static*/ +UniquePtr MsiDatabase::GetRecordString(MSIHANDLE aRecord, + UINT aFieldIndex) { + // The 3rd parameter of MsiRecordGetStringW must not be nullptr. + wchar_t kEmptyString[] = L""; + DWORD len = 0; + UINT ret = ::MsiRecordGetStringW(aRecord, aFieldIndex, kEmptyString, &len); + if (ret != ERROR_MORE_DATA) { + return nullptr; + } + + // |len| returned from MsiRecordGetStringW does not include + // a null-character, but a length to pass to MsiRecordGetStringW + // needs to include a null-character. + ++len; + + auto buf = MakeUnique(len); + ret = ::MsiRecordGetStringW(aRecord, aFieldIndex, buf.get(), &len); + if (ret != ERROR_SUCCESS) { + return nullptr; + } + + return buf; +} + +MsiDatabase::MsiDatabase(const wchar_t* aDatabasePath) { + MSIHANDLE handle = 0; + UINT ret = ::MsiOpenDatabaseW(aDatabasePath, MSIDBOPEN_READONLY_W, &handle); + if (ret != ERROR_SUCCESS) { + return; + } + + mDatabase.own(handle); +} + +Maybe MsiDatabase::FromProductId(const wchar_t* aProductId) { + DWORD len = MAX_PATH; + wchar_t bufStack[MAX_PATH]; + UINT ret = ::MsiGetProductInfoW(aProductId, INSTALLPROPERTY_LOCALPACKAGE_W, + bufStack, &len); + if (ret == ERROR_SUCCESS) { + return Some(MsiDatabase(bufStack)); + } + + if (ret != ERROR_MORE_DATA) { + return Nothing(); + } + + // |len| returned from MsiGetProductInfoW does not include + // a null-character, but a length to pass to MsiGetProductInfoW + // needs to include a null-character. + ++len; + + std::unique_ptr bufHeap(new wchar_t[len]); + ret = ::MsiGetProductInfoW(aProductId, INSTALLPROPERTY_LOCALPACKAGE_W, + bufHeap.get(), &len); + if (ret == ERROR_SUCCESS) { + return Some(MsiDatabase(bufHeap.get())); + } + + return Nothing(); +} + +MsiDatabase::operator bool() const { return !!mDatabase; } + +} // namespace mozilla diff --git a/toolkit/components/aboutthirdparty/MsiDatabase.h b/toolkit/components/aboutthirdparty/MsiDatabase.h new file mode 100644 index 000000000000..91c1370b8d61 --- /dev/null +++ b/toolkit/components/aboutthirdparty/MsiDatabase.h @@ -0,0 +1,94 @@ +/* -*- 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 __MsiDatabase_h__ +#define __MsiDatabase_h__ + +#include "mozilla/Maybe.h" +#include "mozilla/UniquePtr.h" +#include "nsWindowsHelpers.h" + +#include +#include +#include + +namespace mozilla { + +class MsiDatabase final { + static UniquePtr GetRecordString(MSIHANDLE aRecord, + UINT aFieldIndex); + + nsAutoMsiHandle mDatabase; + + MsiDatabase() = default; + explicit MsiDatabase(const wchar_t* aDatabasePath); + + public: + // A callback function passed to ExecuteSingleColumnQuery uses this type + // to control the enumeration loop. + enum class CallbackResult { Continue, Stop }; + + static Maybe FromProductId(const wchar_t* aProductId); + + MsiDatabase(const MsiDatabase&) = delete; + MsiDatabase& operator=(const MsiDatabase&) = delete; + MsiDatabase(MsiDatabase&& aOther) : mDatabase(aOther.mDatabase.disown()) {} + MsiDatabase& operator=(MsiDatabase&& aOther) { + if (this != &aOther) { + mDatabase.own(aOther.mDatabase.disown()); + } + return *this; + } + + explicit operator bool() const; + + template + bool ExecuteSingleColumnQuery(const wchar_t* aQuery, + const CallbackT& aCallback) const { + MSIHANDLE handle; + UINT ret = ::MsiDatabaseOpenViewW(mDatabase, aQuery, &handle); + if (ret != ERROR_SUCCESS) { + return false; + } + + nsAutoMsiHandle view(handle); + + ret = ::MsiViewExecute(view, 0); + if (ret != ERROR_SUCCESS) { + return false; + } + + for (;;) { + ret = ::MsiViewFetch(view, &handle); + if (ret == ERROR_NO_MORE_ITEMS) { + break; + } else if (ret != ERROR_SUCCESS) { + return false; + } + + nsAutoMsiHandle record(handle); + UniquePtr guidStr = GetRecordString(record, 1); + if (!guidStr) { + continue; + } + + CallbackResult result = aCallback(guidStr.get()); + if (result == CallbackResult::Continue) { + continue; + } else if (result == CallbackResult::Stop) { + break; + } else { + MOZ_ASSERT_UNREACHABLE("Unexpected CallbackResult."); + } + } + + return true; + } +}; + +} // namespace mozilla + +#endif // __MsiDatabase_h__ diff --git a/toolkit/components/aboutthirdparty/content/aboutThirdParty.js b/toolkit/components/aboutthirdparty/content/aboutThirdParty.js index 3a6289a7ccc9..783b8c10573a 100644 --- a/toolkit/components/aboutthirdparty/content/aboutThirdParty.js +++ b/toolkit/components/aboutthirdparty/content/aboutThirdParty.js @@ -58,6 +58,9 @@ async function fetchData() { module.typeFlags = AboutThirdParty.lookupModuleType( module.dllFile?.leafName ); + module.application = AboutThirdParty.lookupApplication( + module.dllFile?.path + ); } for (const [proc, perProc] of Object.entries(data.processes)) { @@ -166,6 +169,10 @@ function copyDataToClipboard(aData) { if (module.companyName) { copied.companyName = module.companyName; } + if (module.application) { + copied.applicationName = module.application.name; + copied.applicationPublisher = module.application.publisher; + } if (Array.isArray(module.events)) { copied.events = module.events.map(event => { @@ -225,6 +232,18 @@ function visualizeData(aData) { const modDetailContainer = newCard.querySelector(".module-details"); + if (module.application) { + modDetailContainer.appendChild( + createDetailRow("third-party-detail-app", module.application.name) + ); + modDetailContainer.appendChild( + createDetailRow( + "third-party-detail-publisher", + module.application.publisher + ) + ); + } + if (module.fileVersion) { modDetailContainer.appendChild( createDetailRow("third-party-detail-version", module.fileVersion) diff --git a/toolkit/components/aboutthirdparty/moz.build b/toolkit/components/aboutthirdparty/moz.build index 208c98cd5ab7..dc7b1018418c 100644 --- a/toolkit/components/aboutthirdparty/moz.build +++ b/toolkit/components/aboutthirdparty/moz.build @@ -21,4 +21,12 @@ EXPORTS.mozilla += [ SOURCES += [ "AboutThirdParty.cpp", + "AboutThirdPartyUtils.cpp", + "MsiDatabase.cpp", ] + +OS_LIBS += ["msi"] +DELAYLOAD_DLLS += ["msi.dll"] + +if CONFIG["ENABLE_TESTS"]: + DIRS += ["tests/gtest"] diff --git a/toolkit/components/aboutthirdparty/nsIAboutThirdParty.idl b/toolkit/components/aboutthirdparty/nsIAboutThirdParty.idl index b5cd55443d36..ca67ddf5ac00 100644 --- a/toolkit/components/aboutthirdparty/nsIAboutThirdParty.idl +++ b/toolkit/components/aboutthirdparty/nsIAboutThirdParty.idl @@ -5,6 +5,13 @@ #include "nsISupports.idl" +[scriptable, uuid(063813a0-85d8-4e77-80ea-b61292c0493d)] +interface nsIInstalledApplication : nsISupports +{ + readonly attribute AString name; + readonly attribute AString publisher; +}; + [scriptable, uuid(d33ff086-b328-4ae6-aaf5-52d41aa5df38)] interface nsIAboutThirdParty : nsISupports { @@ -21,6 +28,12 @@ interface nsIAboutThirdParty : nsISupports */ unsigned long lookupModuleType(in AString aLeafName); + /** + * Returns an object representing an application which includes + * the given path of a module in its installation. + */ + nsIInstalledApplication lookupApplication(in AString aModulePath); + /** * Posts a background task to collect system information and resolves * the returned promise when the task is finished. diff --git a/toolkit/components/aboutthirdparty/tests/gtest/TestAboutThirdParty.cpp b/toolkit/components/aboutthirdparty/tests/gtest/TestAboutThirdParty.cpp new file mode 100644 index 000000000000..7defc2c7acf6 --- /dev/null +++ b/toolkit/components/aboutthirdparty/tests/gtest/TestAboutThirdParty.cpp @@ -0,0 +1,125 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include +#include "gtest/gtest.h" + +#include "../../AboutThirdPartyUtils.h" +#include "mozilla/AboutThirdParty.h" +#include "mozilla/ArrayUtils.h" +#include "nsTArray.h" + +using namespace mozilla; + +#define WEATHER_RU u"\x041F\x043E\x0433\x043E\x0434\x0430"_ns +#define WEATHER_JA u"\x5929\x6C17"_ns + +TEST(AboutThirdParty, CompareIgnoreCase) +{ + EXPECT_EQ(CompareIgnoreCase(u""_ns, u""_ns), 0); + EXPECT_EQ(CompareIgnoreCase(u"abc"_ns, u"aBc"_ns), 0); + EXPECT_LT(CompareIgnoreCase(u"a"_ns, u"ab"_ns), 0); + EXPECT_GT(CompareIgnoreCase(u"ab"_ns, u"A"_ns), 0); + EXPECT_LT(CompareIgnoreCase(u""_ns, u"aB"_ns), 0); + EXPECT_GT(CompareIgnoreCase(u"ab"_ns, u""_ns), 0); + + // non-ascii testcases + EXPECT_EQ(CompareIgnoreCase(WEATHER_JA, WEATHER_JA), 0); + EXPECT_EQ(CompareIgnoreCase(WEATHER_RU, WEATHER_RU), 0); + EXPECT_LT(CompareIgnoreCase(WEATHER_RU, WEATHER_JA), 0); + EXPECT_GT(CompareIgnoreCase(WEATHER_JA, WEATHER_RU), 0); + EXPECT_EQ(CompareIgnoreCase(WEATHER_RU u"x"_ns WEATHER_JA, + WEATHER_RU u"X"_ns WEATHER_JA), + 0); + EXPECT_GT( + CompareIgnoreCase(WEATHER_RU u"a"_ns WEATHER_JA, WEATHER_RU u"A"_ns), 0); + EXPECT_LT(CompareIgnoreCase(WEATHER_RU u"a"_ns WEATHER_RU, + WEATHER_RU u"A"_ns WEATHER_JA), + 0); +} + +TEST(AboutThirdParty, MsiPackGuid) +{ + nsAutoString packedGuid; + EXPECT_FALSE( + MsiPackGuid(u"EDA620E3-AA98-3846-B81E-3493CB2E0E02"_ns, packedGuid)); + EXPECT_FALSE( + MsiPackGuid(u"*EDA620E3-AA98-3846-B81E-3493CB2E0E02*"_ns, packedGuid)); + EXPECT_TRUE( + MsiPackGuid(u"{EDA620E3-AA98-3846-B81E-3493CB2E0E02}"_ns, packedGuid)); + EXPECT_STREQ(packedGuid.get(), L"3E026ADE89AA64838BE14339BCE2E020"); +} + +TEST(AboutThirdParty, CorrectMsiComponentPath) +{ + nsAutoString testPath; + + testPath = u""_ns; + EXPECT_FALSE(CorrectMsiComponentPath(testPath)); + + testPath = u"\\\\server\\share"_ns; + EXPECT_FALSE(CorrectMsiComponentPath(testPath)); + + testPath = u"hello"_ns; + EXPECT_FALSE(CorrectMsiComponentPath(testPath)); + + testPath = u"02:\\Software"_ns; + EXPECT_FALSE(CorrectMsiComponentPath(testPath)); + + testPath = u"C:\\path\\"_ns; + EXPECT_TRUE(CorrectMsiComponentPath(testPath)); + EXPECT_STREQ(testPath.get(), L"C:\\path\\"); + + testPath = u"C?\\path\\"_ns; + EXPECT_TRUE(CorrectMsiComponentPath(testPath)); + EXPECT_STREQ(testPath.get(), L"C:\\path\\"); + + testPath = u"C:\\?path\\"_ns; + EXPECT_TRUE(CorrectMsiComponentPath(testPath)); + EXPECT_STREQ(testPath.get(), L"C:\\path\\"); + + testPath = u"\\?path\\"_ns; + EXPECT_FALSE(CorrectMsiComponentPath(testPath)); +} + +TEST(AboutThirdParty, InstallLocations) +{ + const nsLiteralString kDirectoriesUnsorted[] = { + u"C:\\duplicate\\"_ns, u"C:\\duplicate\\"_ns, u"C:\\app1\\"_ns, + u"C:\\app2\\"_ns, u"C:\\app11\\"_ns, u"C:\\app12\\"_ns, + }; + + struct TestCase { + nsLiteralString mFile; + nsLiteralString mInstallPath; + } const kTestCases[] = { + {u"C:\\app\\sub\\file.dll"_ns, u""_ns}, + {u"C:\\app1\\sub\\file.dll"_ns, u"C:\\app1\\"_ns}, + {u"C:\\app11\\sub\\file.dll"_ns, u"C:\\app11\\"_ns}, + {u"C:\\app12\\sub\\file.dll"_ns, u"C:\\app12\\"_ns}, + {u"C:\\app13\\sub\\file.dll"_ns, u""_ns}, + {u"C:\\duplicate\\sub\\file.dll"_ns, u""_ns}, + }; + + nsTArray locations(ArrayLength(kDirectoriesUnsorted)); + for (int i = 0; i < ArrayLength(kDirectoriesUnsorted); ++i) { + locations.EmplaceBack(kDirectoriesUnsorted[i], new InstalledApplication()); + } + + locations.Sort([](const InstallLocationT& aA, const InstallLocationT& aB) { + return CompareIgnoreCase(aA.first(), aB.first()); + }); + + for (const auto& testCase : kTestCases) { + auto bounds = EqualRange(locations, 0, locations.Length(), + InstallLocationComparator(testCase.mFile)); + if (bounds.second() - bounds.first() != 1) { + EXPECT_TRUE(testCase.mInstallPath.IsEmpty()); + continue; + } + + EXPECT_EQ(locations[bounds.first()].first(), testCase.mInstallPath); + } +} diff --git a/toolkit/components/aboutthirdparty/tests/gtest/moz.build b/toolkit/components/aboutthirdparty/tests/gtest/moz.build new file mode 100644 index 000000000000..659c0836db47 --- /dev/null +++ b/toolkit/components/aboutthirdparty/tests/gtest/moz.build @@ -0,0 +1,13 @@ +# -*- 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/. + +LOCAL_INCLUDES += [ + "../..", +] + +UNIFIED_SOURCES += ["TestAboutThirdParty.cpp"] + +FINAL_LIBRARY = "xul-gtest" diff --git a/toolkit/components/aboutthirdparty/tests/xpcshell/test_aboutthirdparty.js b/toolkit/components/aboutthirdparty/tests/xpcshell/test_aboutthirdparty.js index 109122b80472..281fe42188ef 100644 --- a/toolkit/components/aboutthirdparty/tests/xpcshell/test_aboutthirdparty.js +++ b/toolkit/components/aboutthirdparty/tests/xpcshell/test_aboutthirdparty.js @@ -50,4 +50,16 @@ add_task(async () => { Ci.nsIAboutThirdParty.ModuleType_Unknown, "Looking up an invalid name succeeds and returns ModuleType_Unknown." ); + + Assert.equal( + kATP.lookupApplication(""), + null, + "Looking up an empty string returns null." + ); + + Assert.equal( + kATP.lookupApplication("invalid path"), + null, + "Looking up an invalid path returns null." + ); }); diff --git a/toolkit/locales/en-US/toolkit/about/aboutThirdParty.ftl b/toolkit/locales/en-US/toolkit/about/aboutThirdParty.ftl index c7080d90c1a9..6c95e0485ba6 100644 --- a/toolkit/locales/en-US/toolkit/about/aboutThirdParty.ftl +++ b/toolkit/locales/en-US/toolkit/about/aboutThirdParty.ftl @@ -22,6 +22,8 @@ third-party-detail-occurrences = Occurrences .title = How many times this module was loaded. third-party-detail-duration = Avg. Blocking time (ms) .title = How long this module blocked the application. +third-party-detail-app = Application +third-party-detail-publisher = Publisher third-party-th-process = Process third-party-th-duration = Loading Duration (ms) diff --git a/xpcom/base/nsWindowsHelpers.h b/xpcom/base/nsWindowsHelpers.h index a336e085f149..2299fb2d090b 100644 --- a/xpcom/base/nsWindowsHelpers.h +++ b/xpcom/base/nsWindowsHelpers.h @@ -11,6 +11,7 @@ #define nsWindowsHelpers_h #include +#include #include "nsAutoRef.h" #include "mozilla/Assertions.h" #include "mozilla/UniquePtr.h" @@ -172,6 +173,19 @@ class nsAutoRefTraits { } }; +template <> +class nsAutoRefTraits { + public: + typedef MSIHANDLE RawRef; + static RawRef Void() { return 0; } + + static void Release(RawRef aHandle) { + if (aHandle != Void()) { + ::MsiCloseHandle(aHandle); + } + } +}; + // HGLOBAL is just a typedef of HANDLE which nsSimpleRef has a specialization // of, that means having a nsAutoRefTraits specialization for HGLOBAL is // useless. Therefore we create a wrapper class for HGLOBAL to make @@ -237,6 +251,7 @@ typedef nsAutoRef nsModuleHandle; typedef nsAutoRef nsAutoDevMode; typedef nsAutoRef nsAutoGlobalMem; typedef nsAutoRef nsAutoPrinter; +typedef nsAutoRef nsAutoMsiHandle; namespace {