commit 4233493b20200f3f798c92637950f73599fd998a parent 06c852d91f702769c215f3756689827ca645ee64 Author: yao-msft <50888816+yao-msft@users.noreply.github.com> Date: Wed, 17 May 2023 16:31:25 -0700 Add icon extraction to installer metadata collection (#3235) Diffstat:
25 files changed, 1071 insertions(+), 12 deletions(-)
diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -35,6 +35,7 @@ BEFACEF bfd BFirst bght +BITMAPINFOHEADER bitmask bkup blargle @@ -45,6 +46,7 @@ bomgar BOMs boop boundparms +bpp Browsable BSODs buildtransitive @@ -137,12 +139,15 @@ GHS gity goku Google +GRPICONDIR +GRPICONDIRENTRY guiddef Hackathon hashtable helplib helplibrary hhx +highcontrast HINSTANCE hkey hlocal @@ -152,6 +157,9 @@ hre hresults IARP IAttachment +ICONDIR +ICONDIRENTRY +ICONIMAGE idl idx IEnum @@ -162,10 +170,12 @@ IISOn inet inproc installinprogress +INSTALLPROPERTY installshield instream insufficientmemory Intelli +INTRESOURCE invalidparameter isable ishelp @@ -195,9 +205,15 @@ Linq liv liwpx localizationpriority +LOWORD LPBYTE LPCWSTR LPDWORD +LPGRPICONDIR +LPGRPICONDIRENTRY +LPICONDIR +LPICONDIRENTRY +LPICONIMAGE lpitemidlist LPW maclachlan @@ -247,6 +263,7 @@ netfx netlify NETSDK Newtonsoft +NOCRLF NOEXPAND NOLINKINFO nonetwork @@ -297,6 +314,7 @@ powertoys pri processthreads productcode +PRODUCTICON pscustomobject pseudocode PSHOST @@ -321,6 +339,7 @@ relativefilepath remoting reparse restsource +RGBQUAD rgex rgp rgpsz diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -208,6 +208,7 @@ <ClCompile Include="GroupPolicy.cpp" /> <ClCompile Include="HashCommand.cpp" /> <ClCompile Include="HttpClientHelper.cpp" /> + <ClCompile Include="IconExtraction.cpp" /> <ClCompile Include="ImportFlow.cpp" /> <ClCompile Include="InstallDependenciesFlow.cpp" /> <ClCompile Include="InstallerMetadataCollectionContext.cpp" /> @@ -813,6 +814,12 @@ <CopyFileToFolders Include="TestData\Node-Types.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\notepad.exe"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\notepad.ico"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> </ItemGroup> <ItemGroup> <ProjectReference Include="..\AppInstallerCLICore\AppInstallerCLICore.vcxproj"> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -293,6 +293,9 @@ <ClCompile Include="RestInterface_1_4.cpp"> <Filter>Source Files\Repository</Filter> </ClCompile> + <ClCompile Include="IconExtraction.cpp"> + <Filter>Source Files\Repository</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> @@ -837,5 +840,11 @@ <CopyFileToFolders Include="TestData\Node-Types.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\notepad.exe"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\notepad.ico"> + <Filter>TestData</Filter> + </CopyFileToFolders> </ItemGroup> </Project> \ No newline at end of file diff --git a/src/AppInstallerCLITests/IconExtraction.cpp b/src/AppInstallerCLITests/IconExtraction.cpp @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include <winget/IconExtraction.h> +#include <AppInstallerStrings.h> + +using namespace AppInstaller::Repository; + +TEST_CASE("ExtractIconFromBinaryFile", "IconExtraction") +{ + auto extracted = ExtractIconFromBinaryFile(TestCommon::TestDataFile{ "notepad.exe" }.GetPath()); + + std::ifstream expectedIconFile{ TestCommon::TestDataFile{ "notepad.ico" }.GetPath(), std::ios::in | std::ios::binary}; + auto expected = AppInstaller::Utility::ReadEntireStreamAsByteArray(expectedIconFile); + + REQUIRE(expected == extracted); +}+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/InstallerMetadataCollectionContext.cpp b/src/AppInstallerCLITests/InstallerMetadataCollectionContext.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "TestCommon.h" #include "TestSource.h" +#include "TestHooks.h" #include <winget/InstallerMetadataCollectionContext.h> @@ -912,6 +913,17 @@ TEST_CASE("MetadataCollection_NewPackage_1_2", "[metadata_collection]") installedFilesData->InstallationMetadata = std::move(installedFiles); + std::vector<AppInstaller::Repository::ExtractedIconInfo> testIcons; + AppInstaller::Repository::ExtractedIconInfo iconInfo; + iconInfo.IconContent = Utility::SHA256::ConvertToBytes("d2a45116709136462ee7a1c42f0e75f0efa258fe959b1504dc8ea4573451b700"); + iconInfo.IconSha256 = Utility::SHA256::ConvertToBytes("d2a45116709136462ee7a1c42f0e75f0efa258fe959b1504dc8ea4573451b759"); + iconInfo.IconFileType = Manifest::IconFileTypeEnum::Ico; + iconInfo.IconResolution = Manifest::IconResolutionEnum::Custom; + iconInfo.IconTheme = Manifest::IconThemeEnum::Default; + testIcons.emplace_back(std::move(iconInfo)); + + TestHook::SetExtractIconFromArpEntryResult_Override iconsOverride{ testIcons }; + InstallerMetadataCollectionContext context = CreateTestContext(std::move(correlationData), std::move(installedFilesData), input); TestOutput output = GetOutput(context); @@ -943,10 +955,16 @@ TEST_CASE("MetadataCollection_NewPackage_1_2", "[metadata_collection]") REQUIRE(entry.StartupLinkFiles->size() == 1); REQUIRE(entry.StartupLinkFiles->at(0).RelativeFilePath == "TestApp.lnk"); REQUIRE(entry.StartupLinkFiles->at(0).FileType == Manifest::InstalledFileTypeEnum::Launch); + REQUIRE(entry.Icons.size() == 1); + REQUIRE(entry.Icons[0].IconContent == Utility::SHA256::ConvertToBytes("d2a45116709136462ee7a1c42f0e75f0efa258fe959b1504dc8ea4573451b700")); + REQUIRE(entry.Icons[0].IconSha256 == Utility::SHA256::ConvertToBytes("d2a45116709136462ee7a1c42f0e75f0efa258fe959b1504dc8ea4573451b759")); + REQUIRE(entry.Icons[0].IconFileType == Manifest::IconFileTypeEnum::Ico); + REQUIRE(entry.Icons[0].IconResolution == Manifest::IconResolutionEnum::Custom); + REQUIRE(entry.Icons[0].IconTheme == Manifest::IconThemeEnum::Default); REQUIRE(output.Metadata->HistoricalMetadataList.empty()); } -TEST_CASE("MetadataCollection_NewPackage_NoInstallationMetadata", "[metadata_collection]") +TEST_CASE("MetadataCollection_NewPackage_NoInstallationMetadata_NoIcons", "[metadata_collection]") { TestInput input(MinimalDefaults); input.SupportedMetadataVersion = "1.2"; @@ -964,6 +982,9 @@ TEST_CASE("MetadataCollection_NewPackage_NoInstallationMetadata", "[metadata_col correlationData->CorrelateForNewlyInstalledResult.Package = std::make_shared<TestPackageVersion>(manifest, metadata); + std::vector<AppInstaller::Repository::ExtractedIconInfo> testIcons; + TestHook::SetExtractIconFromArpEntryResult_Override iconsOverride{ testIcons }; + InstallerMetadataCollectionContext context = CreateTestContext(std::move(correlationData), input); TestOutput output = GetOutput(context); @@ -976,9 +997,10 @@ TEST_CASE("MetadataCollection_NewPackage_NoInstallationMetadata", "[metadata_col REQUIRE(entry.Scope.empty()); REQUIRE_FALSE(entry.InstalledFiles.has_value()); REQUIRE_FALSE(entry.StartupLinkFiles.has_value()); + REQUIRE(entry.Icons.size() == 0); } -TEST_CASE("MetadataCollection_SameSubmission_SameInstaller_InstallationMetadata", "[metadata_collection]") +TEST_CASE("MetadataCollection_SameSubmission_SameInstaller_InstallationMetadata_Icons", "[metadata_collection]") { std::string version = "1.3.5"; std::string productCode = "{guid}"; @@ -1006,6 +1028,16 @@ TEST_CASE("MetadataCollection_SameSubmission_SameInstaller_InstallationMetadata" startupLinkFiles.emplace_back(std::move(startupLink)); input.CurrentMetadata->InstallerMetadataMap.begin()->second.StartupLinkFiles = std::move(startupLinkFiles); + std::vector<AppInstaller::Repository::ExtractedIconInfo> testIcons; + AppInstaller::Repository::ExtractedIconInfo iconInfo; + iconInfo.IconContent = Utility::SHA256::ConvertToBytes("d2a45116709136462ee7a1c42f0e75f0efa258fe959b1504dc8ea4573451b700"); + iconInfo.IconSha256 = Utility::SHA256::ConvertToBytes("d2a45116709136462ee7a1c42f0e75f0efa258fe959b1504dc8ea4573451b759"); + iconInfo.IconFileType = Manifest::IconFileTypeEnum::Ico; + iconInfo.IconResolution = Manifest::IconResolutionEnum::Custom; + iconInfo.IconTheme = Manifest::IconThemeEnum::Default; + testIcons.emplace_back(std::move(iconInfo)); + input.CurrentMetadata->InstallerMetadataMap.begin()->second.Icons = std::move(testIcons); + auto correlationData = std::make_unique<TestARPCorrelationData>(); auto installedFilesData = std::make_unique<TestInstalledFilesCorrelation>(); @@ -1038,6 +1070,17 @@ TEST_CASE("MetadataCollection_SameSubmission_SameInstaller_InstallationMetadata" installedFilesData->InstallationMetadata = std::move(newInstalledFiles); + std::vector<AppInstaller::Repository::ExtractedIconInfo> newTestIcons; + AppInstaller::Repository::ExtractedIconInfo newIconInfo; + newIconInfo.IconContent = Utility::SHA256::ConvertToBytes("011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6"); + newIconInfo.IconSha256 = Utility::SHA256::ConvertToBytes("011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df8499"); + newIconInfo.IconFileType = Manifest::IconFileTypeEnum::Jpeg; + newIconInfo.IconResolution = Manifest::IconResolutionEnum::Square16; + newIconInfo.IconTheme = Manifest::IconThemeEnum::Light; + newTestIcons.emplace_back(std::move(newIconInfo)); + + TestHook::SetExtractIconFromArpEntryResult_Override iconsOverride{ newTestIcons }; + InstallerMetadataCollectionContext context = CreateTestContext(std::move(correlationData), std::move(installedFilesData), input); TestOutput output = GetOutput(context); @@ -1054,6 +1097,13 @@ TEST_CASE("MetadataCollection_SameSubmission_SameInstaller_InstallationMetadata" // Non duplicate startup links get added. REQUIRE(entry.StartupLinkFiles.has_value()); REQUIRE(entry.StartupLinkFiles->size() == 2); + // New detected icons always take over + REQUIRE(entry.Icons.size() == 1); + REQUIRE(entry.Icons[0].IconContent == Utility::SHA256::ConvertToBytes("011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6")); + REQUIRE(entry.Icons[0].IconSha256 == Utility::SHA256::ConvertToBytes("011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df8499")); + REQUIRE(entry.Icons[0].IconFileType == Manifest::IconFileTypeEnum::Jpeg); + REQUIRE(entry.Icons[0].IconResolution == Manifest::IconResolutionEnum::Square16); + REQUIRE(entry.Icons[0].IconTheme == Manifest::IconThemeEnum::Light); } TEST_CASE("MetadataCollection_Merge_SameInstaller_InstalledFiles", "[metadata_collection]") @@ -1073,11 +1123,12 @@ TEST_CASE("MetadataCollection_Merge_SameInstaller_InstalledFiles", "[metadata_co mergeData.Metadatas->at(0).SchemaVersion = { "1.2" }; mergeData.Metadatas->at(0).InstallerMetadataMap.begin()->second.InstalledFiles = installedFiles; - mergeData.Metadatas->at(1).SchemaVersion = { "1.2" }; // Different default install location clears whole data Manifest::InstallationMetadataInfo newInstalledFiles = installedFiles; newInstalledFiles.DefaultInstallLocation = "%TEMP%\\NewTestApp"; + mergeData.Metadatas->at(1).SchemaVersion = { "1.2" }; + mergeData.Metadatas->at(1).InstallerMetadataMap.begin()->second.InstalledFiles = newInstalledFiles; std::wstring mergeResult = InstallerMetadataCollectionContext::Merge(mergeData.ToJSON(), 0, {}); REQUIRE(!mergeResult.empty()); @@ -1139,11 +1190,12 @@ TEST_CASE("MetadataCollection_Merge_SameInstaller_StartupLinkFiles", "[metadata_ mergeData.Metadatas->at(0).SchemaVersion = { "1.2" }; mergeData.Metadatas->at(0).InstallerMetadataMap.begin()->second.StartupLinkFiles = startupLinkFiles; - mergeData.Metadatas->at(1).SchemaVersion = { "1.2" }; // Different relative file path gets added std::vector<InstalledStartupLinkFile> newStartupLinkFiles = startupLinkFiles; newStartupLinkFiles[0].RelativeFilePath = "TestApp2.lnk"; + + mergeData.Metadatas->at(1).SchemaVersion = { "1.2" }; mergeData.Metadatas->at(1).InstallerMetadataMap.begin()->second.StartupLinkFiles = newStartupLinkFiles; std::wstring mergeResult = InstallerMetadataCollectionContext::Merge(mergeData.ToJSON(), 0, {}); REQUIRE(!mergeResult.empty()); @@ -1169,4 +1221,49 @@ TEST_CASE("MetadataCollection_Merge_SameInstaller_StartupLinkFiles", "[metadata_ REQUIRE(mergeMetadata.InstallerMetadataMap.begin()->second.StartupLinkFiles->size() == 1); REQUIRE(mergeMetadata.InstallerMetadataMap.begin()->second.StartupLinkFiles->at(0).RelativeFilePath == "TestApp.lnk"); REQUIRE(mergeMetadata.InstallerMetadataMap.begin()->second.StartupLinkFiles->at(0).FileType == Manifest::InstalledFileTypeEnum::Unknown); +} + +TEST_CASE("MetadataCollection_Merge_SameInstaller_Icons", "[metadata_collection]") +{ + TestMerge mergeData{ MinimalDefaults }; + mergeData.Metadatas->emplace_back(MakeProductMetadata()); + + std::vector<AppInstaller::Repository::ExtractedIconInfo> testIcons; + AppInstaller::Repository::ExtractedIconInfo iconInfo; + iconInfo.IconContent = Utility::SHA256::ConvertToBytes("d2a45116709136462ee7a1c42f0e75f0efa258fe959b1504dc8ea4573451b700"); + iconInfo.IconSha256 = Utility::SHA256::ConvertToBytes("d2a45116709136462ee7a1c42f0e75f0efa258fe959b1504dc8ea4573451b759"); + iconInfo.IconFileType = Manifest::IconFileTypeEnum::Ico; + iconInfo.IconResolution = Manifest::IconResolutionEnum::Custom; + iconInfo.IconTheme = Manifest::IconThemeEnum::Default; + testIcons.emplace_back(std::move(iconInfo)); + + mergeData.Metadatas->at(0).SchemaVersion = { "1.2" }; + mergeData.Metadatas->at(0).InstallerMetadataMap.begin()->second.Icons = testIcons; + + // Different test icons + std::vector<AppInstaller::Repository::ExtractedIconInfo> newTestIcons; + AppInstaller::Repository::ExtractedIconInfo newIconInfo; + newIconInfo.IconContent = Utility::SHA256::ConvertToBytes("011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6"); + newIconInfo.IconSha256 = Utility::SHA256::ConvertToBytes("011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df8499"); + newIconInfo.IconFileType = Manifest::IconFileTypeEnum::Jpeg; + newIconInfo.IconResolution = Manifest::IconResolutionEnum::Square16; + newIconInfo.IconTheme = Manifest::IconThemeEnum::Light; + newTestIcons.emplace_back(std::move(newIconInfo)); + + mergeData.Metadatas->at(1).SchemaVersion = { "1.2" }; + mergeData.Metadatas->at(1).InstallerMetadataMap.begin()->second.Icons = newTestIcons; + std::wstring mergeResult = InstallerMetadataCollectionContext::Merge(mergeData.ToJSON(), 0, {}); + REQUIRE(!mergeResult.empty()); + + ProductMetadata mergeMetadata; + mergeMetadata.FromJson(web::json::value::parse(mergeResult)); + + // New data always take over + REQUIRE(mergeMetadata.InstallerMetadataMap.size() == 1); + REQUIRE(mergeMetadata.InstallerMetadataMap.begin()->second.Icons.size() == 1); + REQUIRE(mergeMetadata.InstallerMetadataMap.begin()->second.Icons[0].IconContent == Utility::SHA256::ConvertToBytes("011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6")); + REQUIRE(mergeMetadata.InstallerMetadataMap.begin()->second.Icons[0].IconSha256 == Utility::SHA256::ConvertToBytes("011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df8499")); + REQUIRE(mergeMetadata.InstallerMetadataMap.begin()->second.Icons[0].IconFileType == Manifest::IconFileTypeEnum::Jpeg); + REQUIRE(mergeMetadata.InstallerMetadataMap.begin()->second.Icons[0].IconResolution == Manifest::IconResolutionEnum::Square16); + REQUIRE(mergeMetadata.InstallerMetadataMap.begin()->second.Icons[0].IconTheme == Manifest::IconThemeEnum::Light); } \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/notepad.exe b/src/AppInstallerCLITests/TestData/notepad.exe Binary files differ. diff --git a/src/AppInstallerCLITests/TestData/notepad.ico b/src/AppInstallerCLITests/TestData/notepad.ico Binary files differ. diff --git a/src/AppInstallerCLITests/TestHooks.h b/src/AppInstallerCLITests/TestHooks.h @@ -12,6 +12,7 @@ #include <winget/UserSettings.h> #include <winget/Filesystem.h> #include <winget/WindowsFeature.h> +#include <winget/IconExtraction.h> #ifdef AICLI_DISABLE_TEST_HOOKS static_assert(false, "Test hooks have been disabled"); @@ -33,6 +34,7 @@ namespace AppInstaller { void TestHook_SetSourceFactoryOverride(const std::string& type, std::function<std::unique_ptr<ISourceFactory>()>&& factory); void TestHook_ClearSourceFactoryOverrides(); + void TestHook_SetExtractIconFromArpEntryResult_Override(std::vector<AppInstaller::Repository::ExtractedIconInfo>* result); } namespace Repository::Microsoft @@ -210,4 +212,20 @@ namespace TestHook private: AppInstaller::WindowsFeature::DismRestartType m_restartType; }; + + struct SetExtractIconFromArpEntryResult_Override + { + SetExtractIconFromArpEntryResult_Override(std::vector<AppInstaller::Repository::ExtractedIconInfo> extractedIcons) : m_extractedIcons(std::move(extractedIcons)) + { + AppInstaller::Repository::TestHook_SetExtractIconFromArpEntryResult_Override(&m_extractedIcons); + } + + ~SetExtractIconFromArpEntryResult_Override() + { + AppInstaller::Repository::TestHook_SetExtractIconFromArpEntryResult_Override(nullptr); + } + + private: + std::vector<AppInstaller::Repository::ExtractedIconInfo> m_extractedIcons; + }; } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/JsonUtil.cpp b/src/AppInstallerCommonCore/JsonUtil.cpp @@ -221,4 +221,44 @@ namespace AppInstaller::JSON return true; } + + std::string Base64Encode(const std::vector<BYTE>& input) + { + if (input.size() == 0) + { + return {}; + } + + std::wstring result; + DWORD resultSize = 0; + CryptBinaryToStringW(input.data(), static_cast<DWORD>(input.size()), CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, nullptr, &resultSize); + THROW_LAST_ERROR_IF(resultSize == 0); + + result.resize(resultSize); + THROW_LAST_ERROR_IF(!CryptBinaryToStringW(input.data(), static_cast<DWORD>(input.size()), CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, result.data(), &resultSize)); + // Resize to remove trailing null terminator + result.resize(resultSize); + + return Utility::ConvertToUTF8(result); + } + + std::vector<BYTE> Base64Decode(const std::string& input) + { + if (input.empty()) + { + return {}; + } + + auto inputWide = Utility::ConvertToUTF16(input); + + std::vector<BYTE> result; + DWORD resultSize = 0; + CryptStringToBinaryW(inputWide.data(), static_cast<DWORD>(inputWide.size()), CRYPT_STRING_BASE64, nullptr, &resultSize, nullptr, nullptr); + THROW_LAST_ERROR_IF(resultSize == 0); + + result.resize(resultSize); + THROW_LAST_ERROR_IF(!CryptStringToBinaryW(inputWide.data(), static_cast<DWORD>(inputWide.size()), CRYPT_STRING_BASE64, result.data(), &resultSize, nullptr, nullptr)); + + return result; + } } diff --git a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp @@ -408,6 +408,121 @@ namespace AppInstaller::Manifest return result; } + IconFileTypeEnum ConvertToIconFileTypeEnum(std::string_view in) + { + std::string inStrLower = Utility::ToLower(in); + IconFileTypeEnum result = IconFileTypeEnum::Unknown; + + if (inStrLower == "jpeg") + { + result = IconFileTypeEnum::Jpeg; + } + else if (inStrLower == "png") + { + result = IconFileTypeEnum::Png; + } + else if (inStrLower == "ico") + { + result = IconFileTypeEnum::Ico; + } + + return result; + } + + IconThemeEnum ConvertToIconThemeEnum(std::string_view in) + { + std::string inStrLower = Utility::ToLower(in); + IconThemeEnum result = IconThemeEnum::Unknown; + + if (inStrLower == "default") + { + result = IconThemeEnum::Default; + } + else if (inStrLower == "dark") + { + result = IconThemeEnum::Dark; + } + else if (inStrLower == "light") + { + result = IconThemeEnum::Light; + } + else if (inStrLower == "highcontrast") + { + result = IconThemeEnum::HighContrast; + } + + return result; + } + + IconResolutionEnum ConvertToIconResolutionEnum(std::string_view in) + { + std::string inStrLower = Utility::ToLower(in); + IconResolutionEnum result = IconResolutionEnum::Unknown; + + if (inStrLower == "custom") + { + result = IconResolutionEnum::Custom; + } + else if (inStrLower == "16x16") + { + result = IconResolutionEnum::Square16; + } + else if (inStrLower == "20x20") + { + result = IconResolutionEnum::Square20; + } + else if (inStrLower == "24x24") + { + result = IconResolutionEnum::Square24; + } + else if (inStrLower == "30x30") + { + result = IconResolutionEnum::Square30; + } + else if (inStrLower == "32x32") + { + result = IconResolutionEnum::Square32; + } + else if (inStrLower == "36x36") + { + result = IconResolutionEnum::Square36; + } + else if (inStrLower == "40x40") + { + result = IconResolutionEnum::Square40; + } + else if (inStrLower == "48x48") + { + result = IconResolutionEnum::Square48; + } + else if (inStrLower == "60x60") + { + result = IconResolutionEnum::Square60; + } + else if (inStrLower == "64x64") + { + result = IconResolutionEnum::Square64; + } + else if (inStrLower == "72x72") + { + result = IconResolutionEnum::Square72; + } + else if (inStrLower == "80x80") + { + result = IconResolutionEnum::Square80; + } + else if (inStrLower == "96x96") + { + result = IconResolutionEnum::Square96; + } + else if (inStrLower == "256x256") + { + result = IconResolutionEnum::Square256; + } + + return result; + } + std::string_view InstallerTypeToString(InstallerTypeEnum installerType) { switch (installerType) @@ -465,6 +580,77 @@ namespace AppInstaller::Manifest return "unknown"; } + std::string_view IconFileTypeToString(IconFileTypeEnum iconFileType) + { + switch (iconFileType) + { + case IconFileTypeEnum::Ico: + return "ico"sv; + case IconFileTypeEnum::Jpeg: + return "jpeg"sv; + case IconFileTypeEnum::Png: + return "png"sv; + } + + return "unknown"; + } + + std::string_view IconThemeToString(IconThemeEnum iconTheme) + { + switch (iconTheme) + { + case IconThemeEnum::Default: + return "default"sv; + case IconThemeEnum::Dark: + return "dark"sv; + case IconThemeEnum::Light: + return "light"sv; + case IconThemeEnum::HighContrast: + return "highContrast"sv; + } + + return "unknown"; + } + + std::string_view IconResolutionToString(IconResolutionEnum iconResolution) + { + switch (iconResolution) + { + case IconResolutionEnum::Custom: + return "custom"sv; + case IconResolutionEnum::Square16: + return "16x16"sv; + case IconResolutionEnum::Square20: + return "20x20"sv; + case IconResolutionEnum::Square24: + return "24x24"sv; + case IconResolutionEnum::Square30: + return "30x30"sv; + case IconResolutionEnum::Square32: + return "32x32"sv; + case IconResolutionEnum::Square36: + return "36x36"sv; + case IconResolutionEnum::Square40: + return "40x40"sv; + case IconResolutionEnum::Square48: + return "48x48"sv; + case IconResolutionEnum::Square60: + return "60x60"sv; + case IconResolutionEnum::Square64: + return "64x64"sv; + case IconResolutionEnum::Square72: + return "72x72"sv; + case IconResolutionEnum::Square80: + return "80x80"sv; + case IconResolutionEnum::Square96: + return "96x96"sv; + case IconResolutionEnum::Square256: + return "256x256"sv; + } + + return "unknown"; + } + bool DoesInstallerTypeUsePackageFamilyName(InstallerTypeEnum installerType) { return (installerType == InstallerTypeEnum::Msix || installerType == InstallerTypeEnum::MSStore); diff --git a/src/AppInstallerCommonCore/Public/winget/JsonUtil.h b/src/AppInstallerCommonCore/Public/winget/JsonUtil.h @@ -56,4 +56,10 @@ namespace AppInstaller::JSON web::json::value GetStringValue(std::string_view value); bool IsValidNonEmptyStringValue(std::optional<std::string>& value); + + // Base64 encode + std::string Base64Encode(const std::vector<BYTE>& input); + + // Base64 decode + std::vector<BYTE>Base64Decode(const std::string& input); } diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h @@ -192,6 +192,44 @@ namespace AppInstaller::Manifest External }; + enum class IconFileTypeEnum + { + Unknown, + Jpeg, + Png, + Ico, + }; + + enum class IconThemeEnum + { + Unknown, + Default, + Light, + Dark, + HighContrast, + }; + + // Icon resolutions from https://learn.microsoft.com/en-us/windows/apps/design/style/iconography/app-icon-construction#app-icon + enum class IconResolutionEnum + { + Unknown, + Custom, + Square16, + Square20, + Square24, + Square30, + Square32, + Square36, + Square40, + Square48, + Square60, + Square64, + Square72, + Square80, + Square96, + Square256, + }; + struct ExpectedReturnCode { DWORD InstallerReturnCode = 0; @@ -316,12 +354,24 @@ namespace AppInstaller::Manifest InstalledFileTypeEnum ConvertToInstalledFileTypeEnum(const std::string& in); + IconFileTypeEnum ConvertToIconFileTypeEnum(std::string_view in); + + IconThemeEnum ConvertToIconThemeEnum(std::string_view in); + + IconResolutionEnum ConvertToIconResolutionEnum(std::string_view in); + std::string_view InstallerTypeToString(InstallerTypeEnum installerType); std::string_view ScopeToString(ScopeEnum scope); std::string_view InstalledFileTypeToString(InstalledFileTypeEnum installedFileType); + std::string_view IconFileTypeToString(IconFileTypeEnum iconFileType); + + std::string_view IconThemeToString(IconThemeEnum iconTheme); + + std::string_view IconResolutionToString(IconResolutionEnum iconResolution); + // Gets a value indicating whether the given installer uses the PackageFamilyName system reference. bool DoesInstallerTypeUsePackageFamilyName(InstallerTypeEnum installerType); diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -332,6 +332,7 @@ <ItemGroup> <ClInclude Include="ArpVersionValidation.h" /> <ClInclude Include="CompositeSource.h" /> + <ClInclude Include="IconDefs.h" /> <ClInclude Include="ICU\SQLiteICU.h" /> <ClInclude Include="ISource.h" /> <ClInclude Include="Microsoft\ARPHelper.h" /> @@ -389,6 +390,7 @@ <ClInclude Include="PackageTrackingCatalogSourceFactory.h" /> <ClInclude Include="pch.h" /> <ClInclude Include="Public\winget\ARPCorrelation.h" /> + <ClInclude Include="Public\winget\IconExtraction.h" /> <ClInclude Include="Public\winget\InstalledFilesCorrelation.h" /> <ClInclude Include="Public\winget\InstallerMetadataCollectionContext.h" /> <ClInclude Include="Public\winget\ManifestJSONParser.h" /> @@ -426,6 +428,7 @@ <ItemGroup> <ClCompile Include="ARPCorrelation.cpp" /> <ClCompile Include="ARPCorrelationAlgorithms.cpp" /> + <ClCompile Include="IconExtraction.cpp" /> <ClCompile Include="ArpVersionValidation.cpp" /> <ClCompile Include="CompositeSource.cpp" /> <ClCompile Include="ICU\SQLiteICU.c"> diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -357,6 +357,12 @@ <ClInclude Include="Public\winget\InstalledFilesCorrelation.h"> <Filter>Public\winget</Filter> </ClInclude> + <ClInclude Include="IconDefs.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="Public\winget\IconExtraction.h"> + <Filter>Public\winget</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -569,6 +575,9 @@ <ClCompile Include="InstalledFilesCorrelation.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="IconExtraction.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerRepositoryCore/IconDefs.h b/src/AppInstallerRepositoryCore/IconDefs.h @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +// Icon file structure and definitions: +// https://msdn.microsoft.com/en-us/library/ms997538.aspx + +// .ico icon structures + +// Icon entry struct +typedef struct +{ + BYTE bWidth; // Width, in pixels, of the image + BYTE bHeight; // Height, in pixels, of the image + BYTE bColorCount; // Number of colors in image (0 if >=8bpp) + BYTE bReserved; // Reserved ( must be 0) + WORD wPlanes; // Color Planes + WORD wBitCount; // Bits per pixel + DWORD dwBytesInRes; // How many bytes in this resource? + DWORD dwImageOffset; // Where in the file is this image? +} ICONDIRENTRY, *LPICONDIRENTRY; + + +// Icon directory struct +typedef struct +{ + WORD idReserved; // Reserved (must be 0) + WORD idType; // Resource Type (1 for icons) + WORD idCount; // How many images? + ICONDIRENTRY idEntries[1]; // An entry for each image (idCount of 'em) +} ICONDIR, *LPICONDIR; + + +// Image struct +typedef struct +{ + BITMAPINFOHEADER icHeader; // DIB header + RGBQUAD icColors[1]; // Color table + BYTE icXOR[1]; // DIB bits for XOR mask + BYTE icAND[1]; // DIB bits for AND mask +} ICONIMAGE, * LPICONIMAGE; + + +// .exe and .dll icon structures + +// #pragmas are used here to insure that the structure's +// packing in memory matches the packing of the EXE or DLL. +#pragma pack( push ) +#pragma pack( 2 ) + +typedef struct +{ + BYTE bWidth; // Width, in pixels, of the image + BYTE bHeight; // Height, in pixels, of the image + BYTE bColorCount; // Number of colors in image (0 if >=8bpp) + BYTE bReserved; // Reserved + WORD wPlanes; // Color Planes + WORD wBitCount; // Bits per pixel + DWORD dwBytesInRes; // how many bytes in this resource? + WORD nID; // the ID +} GRPICONDIRENTRY, *LPGRPICONDIRENTRY; + + +typedef struct +{ + WORD idReserved; // Reserved (must be 0) + WORD idType; // Resource type (1 for icons) + WORD idCount; // How many images? + GRPICONDIRENTRY idEntries[1]; // The entries for each image +} GRPICONDIR, *LPGRPICONDIR; + +#pragma pack( pop ) diff --git a/src/AppInstallerRepositoryCore/IconExtraction.cpp b/src/AppInstallerRepositoryCore/IconExtraction.cpp @@ -0,0 +1,307 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "IconDefs.h" +#include "winget/IconExtraction.h" +#include "Microsoft/ARPHelper.h" +#include <AppInstallerSHA256.h> +#include <winget/Filesystem.h> + +namespace AppInstaller::Repository +{ + using namespace AppInstaller::Repository::Microsoft; + + namespace + { + // Struct used as data object passed to Enumerate callback function of EnumResourceNamesEx + struct EnumGroupIconProcParameter + { + // Input to specify icon index + int IconIndex = 0; + // The result Resource handle of the group icon + HRSRC ResourceHandle = nullptr; + // How many icons were already found + int IconsFound = 0; + }; + + BOOL CALLBACK EnumGroupIconProc(HMODULE hModule, LPCWSTR lpType, LPWSTR lpName, LONG_PTR lParam) + { + EnumGroupIconProcParameter* parameter = reinterpret_cast<EnumGroupIconProcParameter*>(lParam); + bool foundRequestedIcon = false; + + // Find icon by resource name + if (parameter->IconIndex < 0) + { + if (IS_INTRESOURCE(lpName)) + { + if (-parameter->IconIndex == LOWORD(lpName)) + { + // Found icon by MAKEINTRESOURCE name + foundRequestedIcon = true; + } + } + else if (lpName[0] == TEXT('#')) + { + std::wstring resourceIdString = lpName + 1; // skip the # + try + { + auto resourceId = std::stoi(resourceIdString.c_str(), nullptr, 0); + if (-parameter->IconIndex == resourceId) + { + // Found icon by number as string #12 + foundRequestedIcon = true; + } + } + catch (...) + { + // Error occurred, stop enumerating + return FALSE; + } + } + } + else if (parameter->IconIndex == parameter->IconsFound) + { + // Found icon by index + foundRequestedIcon = TRUE; + } + + if (foundRequestedIcon) + { + parameter->ResourceHandle = FindResourceExW(hModule, lpType, lpName, 0); + return FALSE; + } + + // Continue enumerating + parameter->IconsFound++; + return TRUE; + }; + + void WriteIconDirHeaderToByteArray(std::vector<BYTE>& data, const ICONDIR& iconDir) + { + data.clear(); + BYTE const* toBeWritten = reinterpret_cast<BYTE const*>(&(iconDir.idReserved)); + data.insert(data.end(), toBeWritten, toBeWritten + sizeof(iconDir.idReserved)); + toBeWritten = reinterpret_cast<BYTE const*>(&(iconDir.idType)); + data.insert(data.end(), toBeWritten, toBeWritten + sizeof(iconDir.idType)); + toBeWritten = reinterpret_cast<BYTE const*>(&(iconDir.idCount)); + data.insert(data.end(), toBeWritten, toBeWritten + sizeof(iconDir.idCount)); + } + + void AppendIconDirEntryToByteArray(std::vector<BYTE>& data, const ICONDIRENTRY& iconDirEntry) + { + data.insert(data.end(), iconDirEntry.bWidth); + data.insert(data.end(), iconDirEntry.bHeight); + data.insert(data.end(), iconDirEntry.bColorCount); + data.insert(data.end(), iconDirEntry.bReserved); + BYTE const* toBeWritten = reinterpret_cast<BYTE const*>(&(iconDirEntry.wPlanes)); + data.insert(data.end(), toBeWritten, toBeWritten + sizeof(iconDirEntry.wPlanes)); + toBeWritten = reinterpret_cast<BYTE const*>(&(iconDirEntry.wBitCount)); + data.insert(data.end(), toBeWritten, toBeWritten + sizeof(iconDirEntry.wBitCount)); + toBeWritten = reinterpret_cast<BYTE const*>(&(iconDirEntry.dwBytesInRes)); + data.insert(data.end(), toBeWritten, toBeWritten + sizeof(iconDirEntry.dwBytesInRes)); + toBeWritten = reinterpret_cast<BYTE const*>(&(iconDirEntry.dwImageOffset)); + data.insert(data.end(), toBeWritten, toBeWritten + sizeof(iconDirEntry.dwImageOffset)); + } + } + + std::vector<BYTE> ExtractIconFromBinaryFile(const std::filesystem::path binaryPath, int iconIndex) + { + try + { + wil::unique_hmodule module; + module.reset(LoadLibraryEx(binaryPath.c_str(), nullptr, LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_AS_IMAGE_RESOURCE)); + THROW_LAST_ERROR_IF_NULL(module); + + EnumGroupIconProcParameter param; + param.IconIndex = iconIndex; + +#pragma warning( push ) +#pragma warning ( disable : 4302 ) + // First find the requested group icon + EnumResourceNamesExW( + module.get(), + MAKEINTRESOURCE(RT_GROUP_ICON), + EnumGroupIconProc, + reinterpret_cast<LONG_PTR>(¶m), + (RESOURCE_ENUM_MUI | RESOURCE_ENUM_LN | RESOURCE_ENUM_VALIDATE), + 0); +#pragma warning( pop ) + + if (param.ResourceHandle) + { + // Load and Lock to get a pointer to a GRPICONDIR + HGLOBAL groupIconResourceHandle = LoadResource(module.get(), param.ResourceHandle); + THROW_LAST_ERROR_IF_NULL(groupIconResourceHandle); + LPGRPICONDIR groupIconDir = reinterpret_cast<LPGRPICONDIR>(LockResource(groupIconResourceHandle)); + THROW_LAST_ERROR_IF_NULL(groupIconDir); + + // Basic validation + if (groupIconDir->idReserved != 0 || groupIconDir->idType != 1 || groupIconDir->idCount == 0) + { + return {}; + } + + struct SingleIconImage + { + ICONDIRENTRY DirEntry = { 0 }; + // pointer to byte contents with size + std::pair<const BYTE*, DWORD> Content; + }; + + // Read all individual icon image contents + std::vector<SingleIconImage> iconContents; + // The first image's offset. + DWORD imageOffset = 6 /* ICONDIR size */ + groupIconDir->idCount * 16 /* each ICONDIRENTRY size */; + + for (int i = 0; i < groupIconDir->idCount; i++) + { + SingleIconImage iconEntry; + + // Populate ICONDIRENTRY + iconEntry.DirEntry.bWidth = groupIconDir->idEntries[i].bWidth; + iconEntry.DirEntry.bHeight = groupIconDir->idEntries[i].bHeight; + iconEntry.DirEntry.bColorCount = groupIconDir->idEntries[i].bColorCount; + iconEntry.DirEntry.bReserved = groupIconDir->idEntries[i].bReserved; + iconEntry.DirEntry.wPlanes = groupIconDir->idEntries[i].wPlanes; + iconEntry.DirEntry.wBitCount = groupIconDir->idEntries[i].wBitCount; + iconEntry.DirEntry.dwBytesInRes = groupIconDir->idEntries[i].dwBytesInRes; + iconEntry.DirEntry.dwImageOffset = imageOffset; + + // Load individual icon content + HRSRC iconResourceHandle = FindResourceExW(module.get(), RT_ICON, MAKEINTRESOURCE(groupIconDir->idEntries[i].nID), 0); + THROW_LAST_ERROR_IF_NULL(iconResourceHandle); + HGLOBAL iconResourceContentHandle = LoadResource(module.get(), iconResourceHandle); + THROW_LAST_ERROR_IF_NULL(iconResourceContentHandle); + iconEntry.Content.second = SizeofResource(module.get(), iconResourceHandle); + THROW_LAST_ERROR_IF(iconEntry.Content.second == 0); + iconEntry.Content.first = reinterpret_cast<BYTE*>(LockResource(iconResourceContentHandle)); + THROW_LAST_ERROR_IF_NULL(iconEntry.Content.first); + + // This will be the next image offset. + imageOffset += iconEntry.Content.second; + + iconContents.emplace_back(std::move(iconEntry)); + } + + // Construct ico file icon dir header + ICONDIR iconDir; + iconDir.idReserved = groupIconDir->idReserved; + iconDir.idType = groupIconDir->idType; + iconDir.idCount = groupIconDir->idCount; + + std::vector<BYTE> result; + + // Write Icon Dir header + WriteIconDirHeaderToByteArray(result, iconDir); + + // Write Icon Dir entries + for (auto const& singleIconEntry : iconContents) + { + AppendIconDirEntryToByteArray(result, singleIconEntry.DirEntry); + } + + // Write Icon contents + for (auto const& singleIconEntry : iconContents) + { + result.insert(result.end(), singleIconEntry.Content.first, singleIconEntry.Content.first + singleIconEntry.Content.second); + } + + return result; + } + } + CATCH_LOG(); + + return {}; + } + +#ifndef AICLI_DISABLE_TEST_HOOKS + static std::vector<ExtractedIconInfo>* s_ExtractIconFromArpEntry_TestHook_Override = nullptr; + + void TestHook_SetExtractIconFromArpEntryResult_Override(std::vector<ExtractedIconInfo>* result) + { + s_ExtractIconFromArpEntry_TestHook_Override = result; + } +#endif + + std::vector<ExtractedIconInfo> ExtractIconFromArpEntry(const std::string& productCode, Manifest::ScopeEnum scope) + { +#ifndef AICLI_DISABLE_TEST_HOOKS + if (s_ExtractIconFromArpEntry_TestHook_Override) + { + return *s_ExtractIconFromArpEntry_TestHook_Override; + } +#endif + + ARPHelper arpHelper; + Registry::Key arpEntry = arpHelper.FindARPEntry(productCode, scope); + + if (arpEntry) + { + std::wstring iconPathRaw; + if (arpHelper.GetBoolValue(arpEntry, arpHelper.WindowsInstaller)) + { + // For msi, get icon from ProductInfo + auto productCodeWide = Utility::ConvertToUTF16(productCode); + DWORD iconPathSize = 0; + if (ERROR_MORE_DATA == MsiGetProductInfoW(productCodeWide.c_str(), INSTALLPROPERTY_PRODUCTICON, nullptr, &iconPathSize)) + { + std::wstring iconPathBuffer; + // The iconPathSize returned in previous call does not count the null terminator. + iconPathSize++; + iconPathBuffer.resize(iconPathSize); + if (ERROR_SUCCESS == MsiGetProductInfoW(productCodeWide.c_str(), INSTALLPROPERTY_PRODUCTICON, iconPathBuffer.data(), &iconPathSize)) + { + iconPathBuffer.resize(iconPathSize); + iconPathRaw = iconPathBuffer; + } + } + } + else + { + // For other win32 apps, try DisplayIcon. + iconPathRaw = Utility::ConvertToUTF16(arpHelper.GetStringValue(arpEntry, arpHelper.DisplayIcon)); + } + + if (!iconPathRaw.empty()) + { + PathUnquoteSpacesW(iconPathRaw.data()); + // For paths like C:\test\test.exe,-3 + int iconIndex = 0; + iconIndex = PathParseIconLocationW(iconPathRaw.data()); + // Above operations will modify the input string with null terminator in the middle. + iconPathRaw = iconPathRaw.c_str(); + auto iconPath = Filesystem::GetExpandedPath(Utility::ConvertToUTF8(iconPathRaw)); + + if (std::filesystem::exists(iconPath)) + { + auto extension = iconPath.extension().u8string(); + std::vector<BYTE> iconContent; + if (Utility::CaseInsensitiveEquals(extension, ".ico")) + { + std::ifstream iconFile{ iconPath, std::ios::in | std::ios::binary }; + iconContent = Utility::ReadEntireStreamAsByteArray(iconFile); + } + else if (Utility::CaseInsensitiveEquals(extension, ".exe") || Utility::CaseInsensitiveEquals(extension, ".dll")) + { + iconContent = ExtractIconFromBinaryFile(iconPath, iconIndex); + } + + // Construct ExtractedIconInfo return result + if (!iconContent.empty()) + { + ExtractedIconInfo iconInfo; + iconInfo.IconFileType = Manifest::IconFileTypeEnum::Ico; + iconInfo.IconTheme = Manifest::IconThemeEnum::Default; + iconInfo.IconResolution = Manifest::IconResolutionEnum::Custom; + iconInfo.IconSha256 = Utility::SHA256::ComputeHash(iconContent.data(), static_cast<uint32_t>(iconContent.size())); + iconInfo.IconContent = std::move(iconContent); + + return { std::move(iconInfo) }; + } + } + } + } + + return {}; + } +}+ \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/InstallerMetadataCollectionContext.cpp b/src/AppInstallerRepositoryCore/InstallerMetadataCollectionContext.cpp @@ -42,6 +42,12 @@ namespace AppInstaller::Repository::Metadata InstalledStartupLinks = L"startupLinks"; InstalledStartupLinkPath = L"RelativeFilePath"; InstalledStartupLinkType = L"FileType"; + Icons = L"icons"; + IconContent = L"IconContent"; + IconSha256 = L"IconSha256"; + IconFileType = L"IconFileType"; + IconResolution = L"IconResolution"; + IconTheme = L"IconTheme"; } } @@ -90,6 +96,13 @@ namespace AppInstaller::Repository::Metadata utility::string_t InstalledStartupLinks; utility::string_t InstalledStartupLinkPath; utility::string_t InstalledStartupLinkType; + // Icons + utility::string_t Icons; + utility::string_t IconContent; + utility::string_t IconSha256; + utility::string_t IconFileType; + utility::string_t IconResolution; + utility::string_t IconTheme; }; struct OutputFields_1_0 @@ -326,6 +339,59 @@ namespace AppInstaller::Repository::Metadata return startupLinks; } + + std::vector<ExtractedIconInfo> DeserializeExtractedIcons( + const web::json::value& icons, + const ProductMetadataFields_1_N& fields) + { + if (icons.is_null() || !icons.is_array()) + { + return {}; + } + + std::vector<ExtractedIconInfo> result; + for (auto const& iconInfo : icons.as_array()) + { + ExtractedIconInfo iconInfoEntry; + + auto content = AppInstaller::JSON::GetRawStringValueFromJsonNode(iconInfo, fields.IconContent); + if (!AppInstaller::JSON::IsValidNonEmptyStringValue(content)) + { + AICLI_LOG(Repo, Error, << "Missing IconContent in Extracted Icons."); + return {}; + } + + iconInfoEntry.IconContent = AppInstaller::JSON::Base64Decode(*content); + + std::optional<std::string> sha256 = AppInstaller::JSON::GetRawStringValueFromJsonNode(iconInfo, fields.IconSha256); + if (AppInstaller::JSON::IsValidNonEmptyStringValue(sha256)) + { + iconInfoEntry.IconSha256 = Utility::SHA256::ConvertToBytes(*sha256); + } + + std::optional<std::string> fileType = AppInstaller::JSON::GetRawStringValueFromJsonNode(iconInfo, fields.IconFileType); + if (AppInstaller::JSON::IsValidNonEmptyStringValue(fileType)) + { + iconInfoEntry.IconFileType = Manifest::ConvertToIconFileTypeEnum(*fileType); + } + + std::optional<std::string> theme = AppInstaller::JSON::GetRawStringValueFromJsonNode(iconInfo, fields.IconTheme); + if (AppInstaller::JSON::IsValidNonEmptyStringValue(theme)) + { + iconInfoEntry.IconTheme = Manifest::ConvertToIconThemeEnum(*theme); + } + + std::optional<std::string> resolution = AppInstaller::JSON::GetRawStringValueFromJsonNode(iconInfo, fields.IconResolution); + if (AppInstaller::JSON::IsValidNonEmptyStringValue(resolution)) + { + iconInfoEntry.IconResolution = Manifest::ConvertToIconResolutionEnum(*resolution); + } + + result.emplace_back(std::move(iconInfoEntry)); + } + + return result; + } } void ProductMetadata::Clear() @@ -535,6 +601,15 @@ namespace AppInstaller::Repository::Metadata } } + if (!fields.Icons.empty()) + { + auto icons = AppInstaller::JSON::GetJsonValueFromNode(item, fields.Icons); + if (icons) + { + installerMetadata.Icons = DeserializeExtractedIcons(icons->get(), fields); + } + } + InstallerMetadataMap[installerHashString] = std::move(installerMetadata); } } @@ -621,6 +696,28 @@ namespace AppInstaller::Repository::Metadata itemValue[fields.InstalledStartupLinks] = std::move(startupLinkFilesArray); } + if (!fields.Icons.empty() && !item.second.Icons.empty()) + { + web::json::value iconsArray = web::json::value::array(); + size_t iconIndex = 0; + for (const auto& entry : item.second.Icons) + { + web::json::value entryValue; + entryValue[fields.IconContent] = AppInstaller::JSON::GetStringValue(AppInstaller::JSON::Base64Encode(entry.IconContent)); + if (!entry.IconSha256.empty()) + { + entryValue[fields.IconSha256] = AppInstaller::JSON::GetStringValue(SHA256::ConvertToString(entry.IconSha256)); + } + entryValue[fields.IconFileType] = AppInstaller::JSON::GetStringValue(Manifest::IconFileTypeToString(entry.IconFileType)); + entryValue[fields.IconTheme] = AppInstaller::JSON::GetStringValue(Manifest::IconThemeToString(entry.IconTheme)); + entryValue[fields.IconResolution] = AppInstaller::JSON::GetStringValue(Manifest::IconResolutionToString(entry.IconResolution)); + + iconsArray[iconIndex++] = std::move(entryValue); + } + + itemValue[fields.Icons] = std::move(iconsArray); + } + web::json::value appsAndFeaturesArray = web::json::value::array(); size_t appsAndFeaturesEntryIndex = 0; for (const auto& entry : item.second.AppsAndFeaturesEntries) @@ -1003,6 +1100,9 @@ namespace AppInstaller::Repository::Metadata Manifest::ScopeEnum scope = Manifest::ConvertToScopeEnum(packageMetadata[PackageVersionMetadata::InstalledScope]); + // ARP entry icon extraction upon ARP correlation success + auto icons = ExtractIconFromArpEntry(newEntry.ProductCode, scope); + // Add or update the metadata for the installer hash auto itr = m_outputMetadata.InstallerMetadataMap.find(m_installerHash); @@ -1019,6 +1119,11 @@ namespace AppInstaller::Repository::Metadata newMetadata.Scope = Manifest::ScopeToString(scope); } + if (!icons.empty()) + { + newMetadata.Icons = std::move(icons); + } + m_outputMetadata.InstallerMetadataMap[m_installerHash] = std::move(newMetadata); } else @@ -1033,6 +1138,12 @@ namespace AppInstaller::Repository::Metadata itr->second.Scope = Manifest::ScopeToString(Manifest::ScopeEnum::Unknown); } + // We will always use the latest extracted icons upon confliction. + if (!icons.empty()) + { + itr->second.Icons = std::move(icons); + } + // Existing entry for installer hash, add/update the entry FilterAndAddToEntries(std::move(newEntry), itr->second.AppsAndFeaturesEntries); } @@ -1374,6 +1485,12 @@ namespace AppInstaller::Repository::Metadata } } + // We will always use the latest extracted icons upon confliction. + if (!installerMetadata.second.Icons.empty()) + { + itr->second.Icons = installerMetadata.second.Icons; + } + if (!itr->second.InstalledFiles.has_value()) { itr->second.InstalledFiles = installerMetadata.second.InstalledFiles; diff --git a/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp b/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp @@ -190,6 +190,44 @@ namespace AppInstaller::Repository::Microsoft } } + Registry::Key ARPHelper::FindARPEntry(const std::string& productCode, Manifest::ScopeEnum scope) const + { + if (productCode.empty()) + { + return {}; + } + + std::vector<Manifest::ScopeEnum> scopesToSearch; + if (scope == Manifest::ScopeEnum::Unknown) + { + scopesToSearch = { Manifest::ScopeEnum::User, Manifest::ScopeEnum::Machine }; + } + else + { + scopesToSearch = { scope }; + } + + for (auto scopeToSearch : scopesToSearch) + { + for (auto architecture : Utility::GetApplicableArchitectures()) + { + Registry::Key arpRootKey = GetARPKey(scopeToSearch, architecture); + if (arpRootKey) + { + for (const auto& entry : arpRootKey) + { + if (Utility::CaseInsensitiveEquals(productCode, entry.Name())) + { + return entry.Open(); + } + } + } + } + } + + return {}; + } + bool ARPHelper::GetBoolValue(const Registry::Key& arpKey, const std::wstring& name) { auto value = arpKey[name]; diff --git a/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.h b/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.h @@ -51,11 +51,17 @@ namespace AppInstaller::Repository::Microsoft const std::wstring WindowsInstaller{ L"WindowsInstaller" }; // REG_DWORD (bool) const std::wstring SystemComponent{ L"SystemComponent" }; + // REG_SZ + const std::wstring DisplayIcon{ L"DisplayIcon" }; // Gets the registry key associated with the given scope and architecture on this platform. // May return an empty key if there is no valid location (bad combination or not found). Registry::Key GetARPKey(Manifest::ScopeEnum scope, Utility::Architecture architecture) const; + // Gets the arp registry key associated with the given scope and product code. + // May return an empty key if not found. + Registry::Key FindARPEntry(const std::string& productCode, AppInstaller::Manifest::ScopeEnum scope = AppInstaller::Manifest::ScopeEnum::Unknown) const; + // Returns true IFF the value exists and contains a non-zero DWORD. static bool GetBoolValue(const Registry::Key& arpKey, const std::wstring& name); diff --git a/src/AppInstallerRepositoryCore/Public/winget/IconExtraction.h b/src/AppInstallerRepositoryCore/Public/winget/IconExtraction.h @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <vector> +#include <winget/Manifest.h> + +namespace AppInstaller::Repository +{ + struct ExtractedIconInfo + { + std::vector<BYTE> IconContent; + std::vector<BYTE> IconSha256; + AppInstaller::Manifest::IconFileTypeEnum IconFileType = AppInstaller::Manifest::IconFileTypeEnum::Unknown; + AppInstaller::Manifest::IconThemeEnum IconTheme = AppInstaller::Manifest::IconThemeEnum::Unknown; + AppInstaller::Manifest::IconResolutionEnum IconResolution = AppInstaller::Manifest::IconResolutionEnum::Unknown; + }; + + // The usage of iconIndex is same as usage in ExtractIconEx at + // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-extracticonexw + // The icon index usage is basically: + // 0, 1, 2 means zero-based index of the first icon to extract. + // -1, -2, -3 means extract icon whose resource name is 1, 2, 3 etc + std::vector<BYTE> ExtractIconFromBinaryFile(const std::filesystem::path binaryPath, int iconIndex = 0); + + // Extracts the app icon given the app's product code. + // This method uses similar logic for retrieving icon as app list in settings page. + // This method returns empty if only default icons would be picked. + // This method returns contents of .ico icons. + std::vector<ExtractedIconInfo> ExtractIconFromArpEntry(const std::string& productCode, AppInstaller::Manifest::ScopeEnum scope = AppInstaller::Manifest::ScopeEnum::Unknown); +} diff --git a/src/AppInstallerRepositoryCore/Public/winget/InstallerMetadataCollectionContext.h b/src/AppInstallerRepositoryCore/Public/winget/InstallerMetadataCollectionContext.h @@ -8,6 +8,7 @@ #include <winget/ThreadGlobals.h> #include <winget/ARPCorrelation.h> #include <winget/InstalledFilesCorrelation.h> +#include <winget/IconExtraction.h> #include <filesystem> #include <map> @@ -55,6 +56,8 @@ namespace AppInstaller::Repository::Metadata std::optional<Manifest::InstallationMetadataInfo> InstalledFiles; // If std::nullopt, the value is not set before. If the vector is empty, conflicting values are encountered. std::optional<std::vector<Correlation::InstalledStartupLinkFile>> StartupLinkFiles; + // Extracted icons + std::vector<ExtractedIconInfo> Icons; }; // Metadata from previous product revisions. diff --git a/src/AppInstallerRepositoryCore/pch.h b/src/AppInstallerRepositoryCore/pch.h @@ -9,6 +9,7 @@ #include <winhttp.h> #include <Shlwapi.h> #include <Shlobj.h> +#include <msi.h> #pragma warning( push ) #pragma warning ( disable : 6001 6340 6387 6388 28196 ) diff --git a/src/AppInstallerSharedLib/AppInstallerStrings.cpp b/src/AppInstallerSharedLib/AppInstallerStrings.cpp @@ -579,6 +579,23 @@ namespace AppInstaller::Utility return result; } + std::vector<std::uint8_t> ReadEntireStreamAsByteArray(std::istream& stream) + { + std::streampos currentPos = stream.tellg(); + stream.seekg(0, std::ios_base::end); + + auto offset = stream.tellg() - currentPos; + stream.seekg(currentPos); + + // Don't allow use of this API for reading very large streams. + THROW_HR_IF(E_OUTOFMEMORY, offset > static_cast<std::streamoff>(std::numeric_limits<uint32_t>::max())); + std::vector<std::uint8_t> result; + result.resize(static_cast<size_t>(offset)); + stream.read(reinterpret_cast<char*>(result.data()), offset); + + return result; + } + std::wstring ExpandEnvironmentVariables(const std::wstring& input) { if (input.empty()) diff --git a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h @@ -173,6 +173,9 @@ namespace AppInstaller::Utility // Reads the entire stream into a string. std::string ReadEntireStream(std::istream& stream); + // Reads the entire stream into a byte array. + std::vector<std::uint8_t> ReadEntireStreamAsByteArray(std::istream& stream); + // Expands environment variables within the input. std::wstring ExpandEnvironmentVariables(const std::wstring& input); diff --git a/src/WinGetUtil/WinGetUtil.vcxproj b/src/WinGetUtil/WinGetUtil.vcxproj @@ -186,9 +186,9 @@ <ModuleDefinitionFile Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Source.def</ModuleDefinitionFile> <ModuleDefinitionFile Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">Source.def</ModuleDefinitionFile> <ModuleDefinitionFile Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">Source.def</ModuleDefinitionFile> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;msi.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;msi.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;msi.lib;%(AdditionalDependencies)</AdditionalDependencies> <DelayLoadDLLs Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">winsqlite3.dll;icuuc.dll;icuin.dll;%(DelayLoadDLLs)</DelayLoadDLLs> <DelayLoadDLLs Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">winsqlite3.dll;icuuc.dll;icuin.dll;%(DelayLoadDLLs)</DelayLoadDLLs> <DelayLoadDLLs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">winsqlite3.dll;icuuc.dll;icuin.dll;%(DelayLoadDLLs)</DelayLoadDLLs> @@ -215,7 +215,7 @@ <Link> <SubSystem Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Windows</SubSystem> <ModuleDefinitionFile Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Source.def</ModuleDefinitionFile> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;msi.lib;%(AdditionalDependencies)</AdditionalDependencies> <DelayLoadDLLs Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">winsqlite3.dll;icuuc.dll;icuin.dll;%(DelayLoadDLLs)</DelayLoadDLLs> </Link> <Manifest> @@ -261,10 +261,10 @@ <ModuleDefinitionFile Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">Source.def</ModuleDefinitionFile> <ModuleDefinitionFile Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">Source.def</ModuleDefinitionFile> <ModuleDefinitionFile Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Source.def</ModuleDefinitionFile> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|x64'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;msi.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;msi.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;msi.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|x64'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;msi.lib;%(AdditionalDependencies)</AdditionalDependencies> <DelayLoadDLLs Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">winsqlite3.dll;icuuc.dll;icuin.dll;%(DelayLoadDLLs)</DelayLoadDLLs> <DelayLoadDLLs Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">winsqlite3.dll;icuuc.dll;icuin.dll;%(DelayLoadDLLs)</DelayLoadDLLs> <DelayLoadDLLs Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">winsqlite3.dll;icuuc.dll;icuin.dll;%(DelayLoadDLLs)</DelayLoadDLLs>