winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 9056c0fc3fe4a362ef2644ba080f88698836576b
parent c854548d8889f78a4c8582d065bac2f768017a4d
Author: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com>
Date:   Tue, 21 Jun 2022 17:56:05 -0700

Validate manifest installers (MSIX) (#2215)

* Init manifest installer validation

* Init test cases and misc enhancements

* Adding more unit tests

* Updated manifest validation code

* Updated messages and misc enhancements

* Added UT for version number struct

* Fixed unit test

* Added flag and handle installer error

* Updated validate command default options

* Addressed comment

* Addressed comment

* Addressed comment

* Addressed comments

* Addressed comment

* Check if package is contained within bundle

* Adddressed comment and added treatErrorAsWarning

* Updated UT for versions

* Addressed comments

* Exclude msixbundle for spelling checker

* Addressed comments

* Addressed comments

* Addressed comments

* Addressed comments

* Addressed comments

* Addressed comments

* Updated error message

* Fixed Versions merge conflict

* Addressed comments + added one more UT

* Updated test yaml package id

* Addressed comment

* Addressed comment
Diffstat:
M.github/actions/spelling/excludes.txt | 1+
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 26++++++++++++++++++++++++--
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 24+++++++++++++++++++++++-
Msrc/AppInstallerCLITests/MsixInfo.cpp | 2+-
Asrc/AppInstallerCLITests/MsixManifest.cpp | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/TestCommon.cpp | 16++++++++++++++++
Msrc/AppInstallerCLITests/TestCommon.h | 4++++
Asrc/AppInstallerCLITests/TestData/Installer-Bad-NoSupportedPlatforms.msix | 0
Asrc/AppInstallerCLITests/TestData/Installer-Good.msix | 0
Asrc/AppInstallerCLITests/TestData/Installer-Good.msixbundle | 0
Asrc/AppInstallerCLITests/TestData/Manifest-Bad-InconsistentMsixInstallerFields.yaml | 13+++++++++++++
Asrc/AppInstallerCLITests/TestData/Manifest-Bad-MissingMsixInstallerFields.yaml | 13+++++++++++++
Asrc/AppInstallerCLITests/TestData/Manifest-Bad-NoSupportedPlatforms.yaml | 13+++++++++++++
Asrc/AppInstallerCLITests/TestData/Manifest-Good-MsixInstaller.yaml | 13+++++++++++++
Msrc/AppInstallerCLITests/Versions.cpp | 42++++++++++++++++++++++++++++++++++++++++--
Msrc/AppInstallerCLITests/YamlManifest.cpp | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj | 2++
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters | 6++++++
Msrc/AppInstallerCommonCore/Downloader.cpp | 25+++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Manifest/ManifestValidation.cpp | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Manifest/YamlParser.cpp | 6++++++
Msrc/AppInstallerCommonCore/MsixInfo.cpp | 92++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Asrc/AppInstallerCommonCore/MsixManifest.cpp | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Public/AppInstallerDownloader.h | 3+++
Msrc/AppInstallerCommonCore/Public/AppInstallerMsixInfo.h | 8++++++++
Msrc/AppInstallerCommonCore/Public/AppInstallerVersions.h | 24+++++++++++++++++++++++-
Msrc/AppInstallerCommonCore/Public/winget/ManifestCommon.h | 3++-
Msrc/AppInstallerCommonCore/Public/winget/ManifestValidation.h | 5+++++
Asrc/AppInstallerCommonCore/Public/winget/MsixManifest.h | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Versions.cpp | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/WinGetUtil/Exports.cpp | 1+
Msrc/WinGetUtil/WinGetUtil.h | 1+
32 files changed, 867 insertions(+), 33 deletions(-)

diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt @@ -16,6 +16,7 @@ ignore$ \.mod$ \.mp[34]$ \.msix$ +\.msixbundle$ \.png$ \.vcxitems$ \.vcxproj$ diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -200,6 +200,7 @@ <ClCompile Include="JsonHelper.cpp" /> <ClCompile Include="MsiExecArguments.cpp" /> <ClCompile Include="MsixInfo.cpp" /> + <ClCompile Include="MsixManifest.cpp" /> <ClCompile Include="NameNormalization.cpp" /> <ClCompile Include="PackageCollection.cpp" /> <ClCompile Include="PackageTrackingCatalog.cpp" /> @@ -269,7 +270,7 @@ </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallFlowTest_InstallationNotes.yaml"> <DeploymentContent>true</DeploymentContent> - </CopyFileToFolders> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallFlowTest_Portable.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> @@ -584,7 +585,7 @@ </CopyFileToFolders> <CopyFileToFolders Include="TestData\UpdateFlowTest_Portable.yaml"> <DeploymentContent>true</DeploymentContent> - </CopyFileToFolders> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\InputNames.txt"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> @@ -654,6 +655,27 @@ <CopyFileToFolders Include="TestData\Installer_Exe_DependenciesMultideclaration.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Good-MsixInstaller.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Bad-InconsistentMsixInstallerFields.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Bad-MissingMsixInstallerFields.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Bad-NoSupportedPlatforms.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Installer-Bad-NoSupportedPlatforms.msix"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Installer-Good.msix"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Installer-Good.msixbundle"> + <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 @@ -191,6 +191,9 @@ <ClCompile Include="Correlation.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="MsixManifest.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> @@ -461,7 +464,7 @@ </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallFlowTest_InstallationNotes.yaml"> <Filter>TestData</Filter> - </CopyFileToFolders> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallFlowTest_Portable.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> @@ -609,5 +612,24 @@ <CopyFileToFolders Include="TestData\InputARPData.txt"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Good-MsixInstaller.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Bad-InconsistentMsixInstallerFields.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Bad-MissingMsixInstallerFields.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Bad-NoSupportedPlatforms.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Installer-Good.msix"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Installer-Good.msixbundle"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Installer-Bad-NoSupportedPlatforms.msix" /> </ItemGroup> </Project> \ No newline at end of file diff --git a/src/AppInstallerCLITests/MsixInfo.cpp b/src/AppInstallerCLITests/MsixInfo.cpp @@ -15,7 +15,7 @@ constexpr std::string_view s_MsixFile_1 = "index.1.0.0.0.msix"; constexpr std::string_view s_MsixFile_2 = "index.2.0.0.0.msix"; constexpr std::string_view s_MsixFileSigned_1 = "index.1.0.0.0.signed.msix"; -TEST_CASE("MsixInfo_GetPackageFamilyName", "[msixinfo]") +TEST_CASE("MsixInfo_GetPackageFullName", "[msixinfo]") { TestDataFile index(s_MsixFile_1); Msix::MsixInfo msix(index.GetPath()); diff --git a/src/AppInstallerCLITests/MsixManifest.cpp b/src/AppInstallerCLITests/MsixManifest.cpp @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include <AppInstallerMsixInfo.h> +#include <AppInstallerDownloader.h> +#include <AppInstallerRuntime.h> +#include <winget/MsixManifest.h> + +using namespace TestCommon; +using namespace AppInstaller; +using namespace AppInstaller::Msix; +using namespace Microsoft::WRL; + +namespace +{ + // Input values + constexpr std::string_view installerGoodMsix = "Installer-Good.msix"; + constexpr std::string_view installerGoodMsixBundle = "Installer-Good.msixbundle"; + + // Expected + constexpr std::string_view expectedFamilyName = "FakeInstallerForTesting_125rzkzqaqjwj"; + PackageVersion expectedPackageVersion { 0xAAAABBBBCCCCDDDD }; + constexpr std::string_view expectedWindowsDesktopName = "Windows.Desktop"; + OSVersion expectedWindowsDesktopMinVersion { "10.0.16299.0"}; + OSVersion expectedWindowsUniversalMinVersion { "10.0.0.0"}; +} + +TEST_CASE("MsixManifest_ValidateFieldsParsedFromManifestReader", "[MsixManifest]") +{ + ComPtr<IAppxManifestReader> manifestReader; + REQUIRE(GetMsixPackageManifestReader(installerGoodMsix, &manifestReader)); + + Msix::MsixPackageManifest msixManifest(manifestReader); + REQUIRE(expectedFamilyName == msixManifest.GetIdentity().GetPackageFamilyName()); + REQUIRE(expectedPackageVersion == msixManifest.GetIdentity().GetVersion()); + REQUIRE(2 == msixManifest.GetTargetDeviceFamilies().size()); + REQUIRE(expectedWindowsUniversalMinVersion == msixManifest.GetMinimumOSVersionForSupportedPlatforms().value()); + + auto targets = msixManifest.GetTargetDeviceFamilies(); + auto windowsDesktop = std::find_if(targets.begin(), targets.end(), [](auto& t) { return t.GetMinVersion() == expectedWindowsDesktopMinVersion; }); + REQUIRE(windowsDesktop != targets.end()); + + auto windowsUniversal = std::find_if(targets.begin(), targets.end(), [](auto& t) { return t.GetMinVersion() == expectedWindowsUniversalMinVersion; }); + REQUIRE(windowsUniversal != targets.end()); +} + +TEST_CASE("MsixManifest_ValidateFieldsParsedFromMsix", "[MsixManifest]") +{ + TestDataFile testFile(installerGoodMsix); + MsixInfo msixInfo(testFile.GetPath()); + + auto appPackageManifests = msixInfo.GetAppPackageManifests(); + REQUIRE(1 == appPackageManifests.size()); + + auto &appPackageManifest = appPackageManifests[0]; + REQUIRE(expectedFamilyName == appPackageManifest.GetIdentity().GetPackageFamilyName()); + REQUIRE(expectedPackageVersion == appPackageManifest.GetIdentity().GetVersion()); + REQUIRE(2 == appPackageManifest.GetTargetDeviceFamilies().size()); + REQUIRE(expectedWindowsUniversalMinVersion == appPackageManifest.GetMinimumOSVersionForSupportedPlatforms().value()); + + auto targets = appPackageManifest.GetTargetDeviceFamilies(); + auto windowsDesktop = std::find_if(targets.begin(), targets.end(), [](auto& t) { return t.GetMinVersion() == expectedWindowsDesktopMinVersion; }); + REQUIRE(windowsDesktop != targets.end()); + + auto windowsUniversal = std::find_if(targets.begin(), targets.end(), [](auto& t) { return t.GetMinVersion() == expectedWindowsUniversalMinVersion; }); + REQUIRE(windowsUniversal != targets.end()); +} + +TEST_CASE("MsixManifest_ValidateFieldsParsedFromMsixBundle", "[MsixManifest]") +{ + TestDataFile testFile(installerGoodMsixBundle); + MsixInfo msixInfo(testFile.GetPath()); + + auto appPackageManifests = msixInfo.GetAppPackageManifests(); + REQUIRE(2 == appPackageManifests.size()); + + for (auto& appPackageManifest : appPackageManifests) + { + REQUIRE(expectedFamilyName == appPackageManifest.GetIdentity().GetPackageFamilyName()); + REQUIRE(expectedPackageVersion == appPackageManifest.GetIdentity().GetVersion()); + REQUIRE(1 == appPackageManifest.GetTargetDeviceFamilies().size()); + REQUIRE(expectedWindowsDesktopName == appPackageManifest.GetTargetDeviceFamilies().front().GetName()); + REQUIRE(expectedWindowsDesktopMinVersion == appPackageManifest.GetTargetDeviceFamilies().front().GetMinVersion()); + REQUIRE(expectedWindowsDesktopMinVersion == appPackageManifest.GetMinimumOSVersionForSupportedPlatforms().value()); + } +} diff --git a/src/AppInstallerCLITests/TestCommon.cpp b/src/AppInstallerCLITests/TestCommon.cpp @@ -6,6 +6,7 @@ #include <winget/GroupPolicy.h> #include <winget/UserSettings.h> #include <AppInstallerMsixInfo.h> +#include <AppInstallerDownloader.h> namespace TestCommon { @@ -285,4 +286,19 @@ namespace TestCommon return false; } + + bool GetMsixPackageManifestReader(std::string_view testFileName, IAppxManifestReader** manifestReader) + { + // Locate test file + TestDataFile testFile(testFileName); + auto path = testFile.GetPath().u8string(); + + // Get the stream for the test file + auto stream = AppInstaller::Utility::GetReadOnlyStreamFromURI(path); + + // Get manifest from package reader + Microsoft::WRL::ComPtr<IAppxPackageReader> packageReader; + return AppInstaller::Msix::GetPackageReader(stream.Get(), &packageReader) + && SUCCEEDED(packageReader->GetManifest(manifestReader)); + } } diff --git a/src/AppInstallerCLITests/TestCommon.h b/src/AppInstallerCLITests/TestCommon.h @@ -3,6 +3,7 @@ #pragma once #include <AppInstallerLogging.h> #include <AppInstallerProgress.h> +#include <AppxPackaging.h> #include <winget/UserSettings.h> #include <wil/result.h> @@ -135,4 +136,7 @@ namespace TestCommon // tests calling these functions should skip when not running with admin. bool InstallCertFromSignedPackage(const std::filesystem::path& package); bool UninstallCertFromSignedPackage(const std::filesystem::path& package); + + // Get manifest reader from a msix file path + bool GetMsixPackageManifestReader(std::string_view testFileName, IAppxManifestReader** manifestReader); } diff --git a/src/AppInstallerCLITests/TestData/Installer-Bad-NoSupportedPlatforms.msix b/src/AppInstallerCLITests/TestData/Installer-Bad-NoSupportedPlatforms.msix Binary files differ. diff --git a/src/AppInstallerCLITests/TestData/Installer-Good.msix b/src/AppInstallerCLITests/TestData/Installer-Good.msix Binary files differ. diff --git a/src/AppInstallerCLITests/TestData/Installer-Good.msixbundle b/src/AppInstallerCLITests/TestData/Installer-Good.msixbundle Binary files differ. diff --git a/src/AppInstallerCLITests/TestData/Manifest-Bad-InconsistentMsixInstallerFields.yaml b/src/AppInstallerCLITests/TestData/Manifest-Bad-InconsistentMsixInstallerFields.yaml @@ -0,0 +1,13 @@ +PackageIdentifier: AppInstallerCliTest.InconsistentMsixInstallerFields +PackageVersion: 1.2.3.4 # Mismatching value with msix installer +PackageLocale: es-MX +PackageName: es-MX package name +Publisher: es-MX publisher +PackageFamilyName: FakeInstallerForTesting_Bad # Mismatching value with msix installer +MinimumOSVersion: 5.6.7.8 # Mismatching value with msix installer +InstallerType: msix +Installers: + - Architecture: x64 + InstallerUrl: Installer-Good.msix +ManifestType: merged +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/TestData/Manifest-Bad-MissingMsixInstallerFields.yaml b/src/AppInstallerCLITests/TestData/Manifest-Bad-MissingMsixInstallerFields.yaml @@ -0,0 +1,13 @@ +PackageIdentifier: AppInstallerCliTest.MissingMsixInstallerFields +PackageVersion: 43690.48059.52428.56797 +PackageLocale: es-MX +PackageName: es-MX package name +Publisher: es-MX publisher +# PackageFamilyName: FakeInstallerForTesting_125rzkzqaqjwj # Missing field +# MinimumOSVersion: 10.0.16299.0 # Missing field +InstallerType: msix +Installers: + - Architecture: x64 + InstallerUrl: Installer-Good.msix +ManifestType: merged +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/TestData/Manifest-Bad-NoSupportedPlatforms.yaml b/src/AppInstallerCLITests/TestData/Manifest-Bad-NoSupportedPlatforms.yaml @@ -0,0 +1,13 @@ +PackageIdentifier: AppInstallerCliTest.NoSupportedPlatforms +PackageVersion: 43690.48059.52428.56797 +PackageLocale: es-MX +PackageName: es-MX package name +Publisher: es-MX publisher +PackageFamilyName: FakeInstallerForTesting_125rzkzqaqjwj +MinimumOSVersion: 10.0.16299.0 +InstallerType: msix +Installers: + - Architecture: x64 + InstallerUrl: Installer-Bad-NoSupportedPlatforms.msix +ManifestType: merged +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/TestData/Manifest-Good-MsixInstaller.yaml b/src/AppInstallerCLITests/TestData/Manifest-Good-MsixInstaller.yaml @@ -0,0 +1,13 @@ +PackageIdentifier: AppInstallerCliTest.GoodMsixInstaller +PackageVersion: 43690.48059.52428.56797 +PackageLocale: es-MX +PackageName: es-MX package name +Publisher: es-MX publisher +PackageFamilyName: FakeInstallerForTesting_125rzkzqaqjwj +MinimumOSVersion: 10.0.0.0 +InstallerType: msix +Installers: + - Architecture: x64 + InstallerUrl: Installer-Good.msix +ManifestType: merged +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/Versions.cpp b/src/AppInstallerCLITests/Versions.cpp @@ -192,6 +192,45 @@ TEST_CASE("VersionUnknownLessThanLatest", "[versions]") REQUIRE(Version::CreateUnknown() < Version::CreateLatest()); } +TEST_CASE("UInt64Version_Success_FourParts", "[versions]") +{ + Version expectedVersion("1.2.3.4"); + UInt64Version versionNumberFromNumber(0x0001000200030004); + UInt64Version versionNumberFromString("1.2.3.4"); + REQUIRE(expectedVersion == versionNumberFromNumber); + REQUIRE(expectedVersion == versionNumberFromString); + REQUIRE(expectedVersion.ToString() == versionNumberFromNumber.ToString()); + REQUIRE(expectedVersion.ToString() == versionNumberFromString.ToString()); +} + +TEST_CASE("UInt64Version_Success_LessThanFourParts", "[versions]") +{ + UInt64Version versionNumberFromNumber(0x0001000200030000); + UInt64Version versionNumberFromString("1.2.3"); + REQUIRE(versionNumberFromNumber == versionNumberFromString); +} + +TEST_CASE("UInt64Version_Success_NoOverflow", "[versions]") +{ + REQUIRE_NOTHROW(UInt64Version("65535.65535.65535.65535")); // 65535 => 0xffff + REQUIRE_NOTHROW(UInt64Version(0xffffffffffffffff)); +} + +TEST_CASE("UInt64Version_Fail_Overflow", "[versions]") +{ + REQUIRE_THROWS(UInt64Version("1.0.0.65536")); // 65536 => 0x10000 +} + +TEST_CASE("UInt64Version_Fail_MoreThanFourParts", "[versions]") +{ + REQUIRE_THROWS(UInt64Version("1.0.0.0.1")); +} + +TEST_CASE("UInt64Version_Fail_NonNumeric", "[versions]") +{ + REQUIRE_THROWS(UInt64Version("1.0.0.a")); +} + TEST_CASE("ApproximateVersionParse", "[versions]") { Version v1_0{ "1.0" }; @@ -256,4 +295,4 @@ TEST_CASE("VersionRange", "[versions]") REQUIRE_THROWS(VersionRange{} < VersionRange{ Version{ "1.0" }, Version{ "2.0" } }); REQUIRE(VersionRange{ Version{ "0.5" }, Version{ "1.0" } } < VersionRange{ Version{ "1.5" }, Version{ "2.0" } }); REQUIRE_FALSE(VersionRange{ Version{ "1.5" }, Version{ "2.0" } } < VersionRange{ Version{ "0.5" }, Version{ "1.0" } }); -}- \ No newline at end of file +} diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp @@ -843,6 +843,102 @@ TEST_CASE("ManifestLocalizationValidation", "[ManifestValidation]") REQUIRE(errors.at(0).ErrorLevel == ValidationError::Level::Warning); } +TEST_CASE("ReadManifestAndValidateMsixInstallers_Success", "[ManifestValidation]") +{ + TestDataFile testFile("Manifest-Good-MsixInstaller.yaml"); + Manifest manifest = YamlParser::CreateFromPath(testFile); + + // Update the installer path for testing + REQUIRE(1 == manifest.Installers.size()); + TestDataFile msixFile(manifest.Installers[0].Url.c_str()); + manifest.Installers[0].Url = msixFile.GetPath().u8string(); + + auto errors = ValidateManifestInstallers(manifest); + REQUIRE(0 == errors.size()); +} + +TEST_CASE("ReadManifestAndValidateMsixInstallers_InconsistentFields", "[ManifestValidation]") +{ + TestDataFile testFile("Manifest-Bad-InconsistentMsixInstallerFields.yaml"); + Manifest manifest = YamlParser::CreateFromPath(testFile); + + // Update the installer path for testing + REQUIRE(1 == manifest.Installers.size()); + TestDataFile msixFile(manifest.Installers[0].Url.c_str()); + manifest.Installers[0].Url = msixFile.GetPath().u8string(); + + auto errors = ValidateManifestInstallers(manifest); + REQUIRE(3 == errors.size()); + + // Package family name + REQUIRE(ValidationError::Level::Error == errors[0].ErrorLevel); + REQUIRE(ManifestError::InstallerMsixInconsistencies == errors[0].Message); + REQUIRE("PackageFamilyName" == errors[0].Field); + REQUIRE("FakeInstallerForTesting_125rzkzqaqjwj" == errors[0].Value); + + // Package version + REQUIRE(ValidationError::Level::Error == errors[1].ErrorLevel); + REQUIRE(ManifestError::InstallerMsixInconsistencies == errors[1].Message); + REQUIRE("PackageVersion" == errors[1].Field); + REQUIRE("43690.48059.52428.56797" == errors[1].Value); + + // Min OS version + REQUIRE(ValidationError::Level::Error == errors[2].ErrorLevel); + REQUIRE(ManifestError::InstallerMsixInconsistencies == errors[2].Message); + REQUIRE("MinimumOSVersion" == errors[2].Field); + REQUIRE("10.0.0.0" == errors[2].Value); +} + +TEST_CASE("ReadManifestAndValidateMsixInstallers_NoSupportedPlatforms", "[ManifestValidation]") +{ + auto testFileName = "Manifest-Bad-NoSupportedPlatforms.yaml"; + TestDataFile testFile(testFileName); + Manifest manifest = YamlParser::CreateFromPath(testFile); + + // Update the installer path for testing + REQUIRE(1 == manifest.Installers.size()); + TestDataFile msixFile(manifest.Installers[0].Url.c_str()); + manifest.Installers[0].Url = msixFile.GetPath().u8string(); + + auto errors = ValidateManifestInstallers(manifest); + REQUIRE(1 == errors.size()); + + REQUIRE(ValidationError::Level::Error == errors[0].ErrorLevel); + REQUIRE(ManifestError::NoSupportedPlatforms == errors[0].Message); + REQUIRE("InstallerUrl" == errors[0].Field); + REQUIRE(manifest.Installers.front().Url == errors[0].Value); +} + +TEST_CASE("ReadManifestAndValidateMsixInstallers_MissingFields", "[ManifestValidation]") +{ + TestDataFile testFile("Manifest-Bad-MissingMsixInstallerFields.yaml"); + Manifest manifest = YamlParser::CreateFromPath(testFile); + + // Update the installer path for testing + REQUIRE(1 == manifest.Installers.size()); + TestDataFile msixFile(manifest.Installers[0].Url.c_str()); + manifest.Installers[0].Url = msixFile.GetPath().u8string(); + + for (bool treatErrorAsWarning : { false, true }) + { + auto errors = ValidateManifestInstallers(manifest, treatErrorAsWarning); + auto expectedLevel = treatErrorAsWarning ? ValidationError::Level::Warning : ValidationError::Level::Error; + REQUIRE(2 == errors.size()); + + // Package family name + REQUIRE(expectedLevel == errors[0].ErrorLevel); + REQUIRE(ManifestError::OptionalFieldMissing == errors[0].Message); + REQUIRE("PackageFamilyName" == errors[0].Field); + REQUIRE("FakeInstallerForTesting_125rzkzqaqjwj" == errors[0].Value); + + // Min OS version + REQUIRE(expectedLevel == errors[1].ErrorLevel); + REQUIRE(ManifestError::OptionalFieldMissing == errors[1].Message); + REQUIRE("MinimumOSVersion" == errors[1].Field); + REQUIRE("10.0.0.0" == errors[1].Value); + } +} + TEST_CASE("ManifestArpVersionRange", "[ManifestValidation]") { Manifest manifestNoArp = YamlParser::CreateFromPath(TestDataFile("Manifest-Good-NoArpVersionDeclared.yaml")); @@ -857,4 +953,4 @@ TEST_CASE("ManifestArpVersionRange", "[ManifestValidation]") auto arpRangeMultiArp = manifestMultiArp.GetArpVersionRange(); REQUIRE(arpRangeMultiArp.GetMinVersion().ToString() == "12.0"); REQUIRE(arpRangeMultiArp.GetMaxVersion().ToString() == "13.0"); -}- \ No newline at end of file +} diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -278,6 +278,7 @@ </ItemDefinitionGroup> <ItemGroup> <ClInclude Include="DODownloader.h" /> + <ClInclude Include="Public\winget\MsixManifest.h" /> <ClInclude Include="Public\winget\AdminSettings.h" /> <ClInclude Include="Public\winget\Debugging.h" /> <ClInclude Include="Public\winget\DependenciesGraph.h" /> @@ -379,6 +380,7 @@ <ClCompile Include="MsixInfo.cpp"> <ExcludedFromBuild Condition="'$(Configuration)'=='Fuzzing'">true</ExcludedFromBuild> </ClCompile> + <ClCompile Include="MsixManifest.cpp" /> <ClCompile Include="NameNormalization.cpp" /> <ClCompile Include="Regex.cpp" /> <ClCompile Include="Registry.cpp" /> diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -198,6 +198,9 @@ <ClInclude Include="Public\winget\ManagedFile.h"> <Filter>Public\winget</Filter> </ClInclude> + <ClInclude Include="Public\winget\MsixManifest.h"> + <Filter>Header Files</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -344,6 +347,9 @@ <ClCompile Include="ManagedFile.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="MsixManifest.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCommonCore/Downloader.cpp b/src/AppInstallerCommonCore/Downloader.cpp @@ -12,6 +12,7 @@ #include "Public/winget/UserSettings.h" #include "Public/winget/Filesystem.h" #include "DODownloader.h" +#include "HttpStream/HttpRandomAccessStream.h" using namespace AppInstaller::Runtime; using namespace AppInstaller::Settings; @@ -365,4 +366,28 @@ namespace AppInstaller::Utility return aesSaveResult; } + + Microsoft::WRL::ComPtr<IStream> GetReadOnlyStreamFromURI(std::string_view uriStr) + { + Microsoft::WRL::ComPtr<IStream> inputStream; + if (Utility::IsUrlRemote(uriStr)) + { + // Get an IStream from the input uri and try to create package or bundler reader. + winrt::Windows::Foundation::Uri uri(Utility::ConvertToUTF16(uriStr)); + auto randomAccessStream = HttpStream::HttpRandomAccessStream::CreateAsync(uri).get(); + + ::IUnknown* rasAsIUnknown = (::IUnknown*)winrt::get_abi(randomAccessStream); + THROW_IF_FAILED(CreateStreamOverRandomAccessStream( + rasAsIUnknown, + IID_PPV_ARGS(inputStream.ReleaseAndGetAddressOf()))); + } + else + { + std::filesystem::path path(Utility::ConvertToUTF16(uriStr)); + THROW_IF_FAILED(SHCreateStreamOnFileEx(path.c_str(), + STGM_READ | STGM_SHARE_DENY_WRITE | STGM_FAILIFTHERE, 0, FALSE, nullptr, &inputStream)); + } + + return inputStream; + } } diff --git a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp @@ -2,6 +2,8 @@ // Licensed under the MIT License. #include "pch.h" #include "AppInstallerLogging.h" +#include "AppInstallerMsixInfo.h" +#include "winget/MsixManifest.h" #include "winget/ManifestValidation.h" #include "winget/Locale.h" @@ -258,4 +260,108 @@ namespace AppInstaller::Manifest return resultErrors; } + + // Validate msix and msixbundle installer manifest + std::vector<ValidationError> ValidateMsixManifest( + const Msix::PackageVersion& packageVersion, + const ManifestInstaller& installer, + Msix::MsixPackageManifestCache& msixManifestsCache, + bool treatErrorAsWarning) + { + std::vector<ValidationError> errors; + std::optional<Msix::OSVersion> installerMinOSVersion; + + try + { + if (!installer.MinOSVersion.empty()) + { + installerMinOSVersion = std::make_optional<Msix::OSVersion>(installer.MinOSVersion); + } + } + catch (const std::exception&) + { + errors.emplace_back(ManifestError::InvalidFieldValue, "MinimumOSVersion", installer.MinOSVersion); + } + + std::vector<Msix::MsixPackageManifest> msixManifests; + try + { + msixManifests = msixManifestsCache.GetAppPackageManifests(installer.Url); + } + catch (...) + { + errors.emplace_back(ManifestError::InstallerFailedToProcess, "InstallerUrl", installer.Url); + } + + for (auto msixManifest : msixManifests) + { + // Validate package family name + auto msixManifestIdentity = msixManifest.GetIdentity(); + auto msixPackageFamilyName = msixManifestIdentity.GetPackageFamilyName(); + if (!installer.PackageFamilyName.empty()) + { + if (installer.PackageFamilyName != msixPackageFamilyName) + { + errors.emplace_back(ManifestError::InstallerMsixInconsistencies, "PackageFamilyName", msixPackageFamilyName); + } + } + // Yaml manifest missing package family name + else + { + errors.emplace_back( + ManifestError::OptionalFieldMissing, + "PackageFamilyName", + msixPackageFamilyName, + treatErrorAsWarning ? ValidationError::Level::Warning : ValidationError::Level::Error); + } + + // Validate package version + auto msixVersion = msixManifestIdentity.GetVersion(); + if (msixVersion != packageVersion) + { + errors.emplace_back(ManifestError::InstallerMsixInconsistencies, "PackageVersion", msixVersion.ToString()); + } + + // Validate min OS version + auto targetMinOSVersion = msixManifest.GetMinimumOSVersionForSupportedPlatforms(); + if (!targetMinOSVersion.has_value()) + { + errors.emplace_back(ManifestError::NoSupportedPlatforms, "InstallerUrl", installer.Url); + } + else if (installerMinOSVersion.has_value()) + { + if (targetMinOSVersion.value() != installerMinOSVersion.value()) + { + errors.emplace_back(ManifestError::InstallerMsixInconsistencies, "MinimumOSVersion", targetMinOSVersion.value().ToString()); + } + } + else + { + errors.emplace_back( + ManifestError::OptionalFieldMissing, + "MinimumOSVersion", + targetMinOSVersion.value().ToString(), + treatErrorAsWarning ? ValidationError::Level::Warning : ValidationError::Level::Error); + } + } + + return errors; + } + + std::vector<ValidationError> ValidateManifestInstallers(const Manifest& manifest, bool treatErrorAsWarning) + { + std::vector<ValidationError> errors; + Msix::MsixPackageManifestCache msixManifestsCache; + for (const auto& installer : manifest.Installers) + { + // Installer msix or msixbundle + if (installer.InstallerType == InstallerTypeEnum::Msix) + { + auto installerErrors = ValidateMsixManifest(Msix::PackageVersion(manifest.Version), installer, msixManifestsCache, treatErrorAsWarning); + std::move(installerErrors.begin(), installerErrors.end(), std::inserter(errors, errors.end())); + } + } + + return errors; + } } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Manifest/YamlParser.cpp b/src/AppInstallerCommonCore/Manifest/YamlParser.cpp @@ -439,6 +439,12 @@ namespace AppInstaller::Manifest::YamlParser std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); } + if (validateOption.InstallerValidation) + { + errors = ValidateManifestInstallers(manifest); + std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); + } + // Output merged manifest if requested if (!mergedManifestPath.empty()) { diff --git a/src/AppInstallerCommonCore/MsixInfo.cpp b/src/AppInstallerCommonCore/MsixInfo.cpp @@ -6,7 +6,7 @@ #include "Public/AppInstallerDownloader.h" #include "Public/AppInstallerLogging.h" #include "Public/AppInstallerStrings.h" - +#include "Public/AppInstallerDownloader.h" using namespace winrt::Windows::Storage::Streams; using namespace Microsoft::WRL; @@ -504,24 +504,7 @@ namespace AppInstaller::Msix MsixInfo::MsixInfo(std::string_view uriStr) { - if (Utility::IsUrlRemote(uriStr)) - { - // Get an IStream from the input uri and try to create package or bundler reader. - winrt::Windows::Foundation::Uri uri(Utility::ConvertToUTF16(uriStr)); - IRandomAccessStream randomAccessStream = HttpRandomAccessStream::CreateAsync(uri).get(); - - ::IUnknown* rasAsIUnknown = (::IUnknown*)winrt::get_abi(randomAccessStream); - THROW_IF_FAILED(CreateStreamOverRandomAccessStream( - rasAsIUnknown, - IID_PPV_ARGS(m_stream.ReleaseAndGetAddressOf()))); - } - else - { - std::filesystem::path path(Utility::ConvertToUTF16(uriStr)); - THROW_IF_FAILED(SHCreateStreamOnFileEx(path.c_str(), - STGM_READ | STGM_SHARE_DENY_WRITE | STGM_FAILIFTHERE, 0, FALSE, nullptr, &m_stream)); - } - + m_stream = Utility::GetReadOnlyStreamFromURI(uriStr); if (GetBundleReader(m_stream.Get(), &m_bundleReader)) { m_isBundle = true; @@ -608,6 +591,77 @@ namespace AppInstaller::Msix return Utility::ConvertToUTF8(GetPackageFullNameWide()); } + std::vector<ComPtr<IAppxPackageReader>> MsixInfo::GetAppPackages() const + { + if (!m_isBundle) + { + return { m_packageReader }; + } + + std::vector<ComPtr<IAppxPackageReader>> packages; + + ComPtr<IAppxBundleManifestReader> manifestReader; + THROW_IF_FAILED(m_bundleReader->GetManifest(&manifestReader)); + + ComPtr<IAppxBundleManifestPackageInfoEnumerator> packageInfoItems; + THROW_IF_FAILED(manifestReader->GetPackageInfoItems(&packageInfoItems)); + + BOOL hasCurrent = FALSE; + THROW_IF_FAILED(packageInfoItems->GetHasCurrent(&hasCurrent)); + while (hasCurrent) + { + ComPtr<IAppxBundleManifestPackageInfo> packageInfo; + THROW_IF_FAILED(packageInfoItems->GetCurrent(&packageInfo)); + + APPX_BUNDLE_PAYLOAD_PACKAGE_TYPE packageType; + THROW_IF_FAILED(packageInfo->GetPackageType(&packageType)); + + UINT64 offset; + THROW_IF_FAILED(packageInfo->GetOffset(&offset)); + const bool isContained = offset != 0; + + if (isContained && packageType == APPX_BUNDLE_PAYLOAD_PACKAGE_TYPE::APPX_BUNDLE_PAYLOAD_PACKAGE_TYPE_APPLICATION) + { + wil::unique_cotaskmem_string fileName; + THROW_IF_FAILED(packageInfo->GetFileName(&fileName)); + + ComPtr<IAppxFile> packageFile; + THROW_IF_FAILED(m_bundleReader->GetPayloadPackage(fileName.get(), &packageFile)); + + ComPtr<IStream> stream; + THROW_IF_FAILED(packageFile->GetStream(&stream)); + + ComPtr<IAppxPackageReader> packageReader; + if (GetPackageReader(stream.Get(), &packageReader)) + { + packages.emplace_back(std::move(packageReader)); + } + else + { + AICLI_LOG(Core, Warning, << "Could not get package reader for bundle payload."); + } + } + + THROW_IF_FAILED(packageInfoItems->MoveNext(&hasCurrent)); + } + + return packages; + } + + std::vector<MsixPackageManifest> MsixInfo::GetAppPackageManifests() const + { + std::vector<MsixPackageManifest> manifests; + auto packages = GetAppPackages(); + for (const auto& package : packages) + { + ComPtr<IAppxManifestReader> manifestReader; + THROW_IF_FAILED(package->GetManifest(&manifestReader)); + manifests.emplace_back(std::move(manifestReader)); + } + + return manifests; + } + bool MsixInfo::IsNewerThan(const std::filesystem::path& otherPackage) { THROW_HR_IF(E_NOT_VALID_STATE, m_isBundle); diff --git a/src/AppInstallerCommonCore/MsixManifest.cpp b/src/AppInstallerCommonCore/MsixManifest.cpp @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include <pch.h> +#include <AppInstallerMsixInfo.h> +#include <winget/MsixManifest.h> + +using namespace Microsoft::WRL; + +namespace AppInstaller::Msix +{ + bool IsSupportedPlatform(MsixPackageManifestTargetDeviceFamily::Platform platform) + { + return platform == MsixPackageManifestTargetDeviceFamily::Platform::WindowsDesktop + || platform == MsixPackageManifestTargetDeviceFamily::Platform::WindowsUniversal; + } + + Utility::NormalizedString MsixPackageManifestIdentity::GetPackageFamilyName() const + { + wil::unique_cotaskmem_string familyName; + THROW_IF_FAILED(m_packageId->GetPackageFamilyName(&familyName)); + return { familyName.get() }; + } + + PackageVersion MsixPackageManifestIdentity::GetVersion() const + { + UINT64 version = 0; + THROW_IF_FAILED(m_packageId->GetVersion(&version)); + return PackageVersion{ version }; + } + + MsixPackageManifestIdentity MsixPackageManifest::GetIdentity() const + { + ComPtr<IAppxManifestPackageId> manifestPackageId; + THROW_IF_FAILED(m_manifestReader->GetPackageId(&manifestPackageId)); + return MsixPackageManifestIdentity{ std::move(manifestPackageId) }; + } + + std::optional<OSVersion> MsixPackageManifest::GetMinimumOSVersionForSupportedPlatforms() const + { + std::optional<OSVersion> minOSVersion; + auto targetDeviceFamilies = GetTargetDeviceFamilies(); + + for (const auto& targetDeviceFamily : targetDeviceFamilies) + { + if (IsSupportedPlatform(targetDeviceFamily.GetPlatform())) + { + auto targetDeviceFamilyMinVersion = targetDeviceFamily.GetMinVersion(); + if (!minOSVersion.has_value() || targetDeviceFamilyMinVersion < minOSVersion.value()) + { + minOSVersion = targetDeviceFamilyMinVersion; + } + } + } + + return minOSVersion; + } + + std::vector<MsixPackageManifestTargetDeviceFamily> MsixPackageManifest::GetTargetDeviceFamilies() const + { + std::vector<MsixPackageManifestTargetDeviceFamily> targetDeviceFamilies; + ComPtr<IAppxManifestReader3> manifestReader3; + THROW_IF_FAILED(m_manifestReader.As(&manifestReader3)); + + ComPtr<IAppxManifestTargetDeviceFamiliesEnumerator> targetDeviceFamiliesIter; + THROW_IF_FAILED(manifestReader3->GetTargetDeviceFamilies(&targetDeviceFamiliesIter)); + + BOOL hasCurrent = FALSE; + THROW_IF_FAILED(targetDeviceFamiliesIter->GetHasCurrent(&hasCurrent)); + while (hasCurrent) + { + ComPtr<IAppxManifestTargetDeviceFamily> targetDeviceFamily; + THROW_IF_FAILED(targetDeviceFamiliesIter->GetCurrent(&targetDeviceFamily)); + + targetDeviceFamilies.emplace_back(std::move(targetDeviceFamily)); + + THROW_IF_FAILED(targetDeviceFamiliesIter->MoveNext(&hasCurrent)); + } + + return targetDeviceFamilies; + } + + std::string MsixPackageManifestTargetDeviceFamily::GetName() const + { + wil::unique_cotaskmem_string name; + THROW_IF_FAILED(m_targetDeviceFamily->GetName(&name)); + return Utility::ConvertToUTF8(name.get()); + } + + OSVersion MsixPackageManifestTargetDeviceFamily::GetMinVersion() const + { + UINT64 minVersion = 0; + THROW_IF_FAILED(m_targetDeviceFamily->GetMinVersion(&minVersion)); + return OSVersion{ minVersion }; + } + + MsixPackageManifestTargetDeviceFamily::Platform MsixPackageManifestTargetDeviceFamily::GetPlatform() const + { + auto name = GetName(); + if (Utility::CaseInsensitiveEquals(name, WindowsDesktopName)) + { + return WindowsDesktop; + } + + if (Utility::CaseInsensitiveEquals(name, WindowsUniversalName)) + { + return WindowsUniversal; + } + + return Other; + } + + const std::vector<MsixPackageManifest>& MsixPackageManifestCache::GetAppPackageManifests(std::string url) + { + // If an installer url has already been processed, then use the cached result + auto installerIter = m_msixManifests.find(url); + if (installerIter != m_msixManifests.end()) + { + return installerIter->second; + } + + MsixInfo msixInfo(url); + + // Cache installer url result + m_msixManifests[url] = msixInfo.GetAppPackageManifests(); + return m_msixManifests[url]; + } +} diff --git a/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h b/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h @@ -74,4 +74,7 @@ namespace AppInstaller::Utility // If IAttachmentExecute::Save is successfully invoked and the scan failed, the failure HRESULT is returned. // zoneIfScanFailure: URLZONE to apply if IAttachmentExecute::Save scan failed. HRESULT ApplyMotwUsingIAttachmentExecuteIfApplicable(const std::filesystem::path& filePath, const std::string& source, URLZONE zoneIfScanFailure); + + // Function to read-only create a stream from a uri string (url address or file system path) + Microsoft::WRL::ComPtr<IStream> GetReadOnlyStreamFromURI(std::string_view uriStr); } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerMsixInfo.h b/src/AppInstallerCommonCore/Public/AppInstallerMsixInfo.h @@ -3,6 +3,8 @@ #pragma once #include "AppInstallerProgress.h" #include "winget/ManagedFile.h" +#include "winget/Manifest.h" +#include "winget/MsixManifest.h" #include <AppxPackaging.h> @@ -85,11 +87,17 @@ namespace AppInstaller::Msix // Writes the package file to the given file handle. void WriteToFileHandle(std::string_view packageFile, HANDLE target, IProgressCallback& progress); + // Get application package manifests from msix and msixbundle. + std::vector<MsixPackageManifest> GetAppPackageManifests() const; + private: bool m_isBundle; Microsoft::WRL::ComPtr<IStream> m_stream; Microsoft::WRL::ComPtr<IAppxBundleReader> m_bundleReader; Microsoft::WRL::ComPtr<IAppxPackageReader> m_packageReader; + + // Get application packages. + std::vector<Microsoft::WRL::ComPtr<IAppxPackageReader>> GetAppPackages() const; }; struct GetCertContextResult diff --git a/src/AppInstallerCommonCore/Public/AppInstallerVersions.h b/src/AppInstallerCommonCore/Public/AppInstallerVersions.h @@ -50,7 +50,7 @@ namespace AppInstaller::Utility Version(Version baseVersion, ApproximateComparator approximateComparator); // Resets the version's value to the input. - void Assign(std::string&& version, std::string_view splitChars = DefaultSplitChars); + virtual void Assign(std::string&& version, std::string_view splitChars = DefaultSplitChars); // Gets the full version string used to construct the Version. const std::string& ToString() const { return m_version; } @@ -77,6 +77,7 @@ namespace AppInstaller::Utility // An individual version part in between split characters. struct Part { + Part(uint64_t integer) : Integer(integer) {} Part(const std::string& part); Part(uint64_t integer, std::string other); @@ -104,6 +105,27 @@ namespace AppInstaller::Utility std::string m_version; std::vector<Part> m_parts; ApproximateComparator m_approximateComparator = ApproximateComparator::None; + + // Remove trailing empty parts (0 or empty) + void Trim(); + }; + + // Four parts version number: 16-bits.16-bits.16-bits.16-bits + struct UInt64Version : public Version + { + UInt64Version() = default; + UInt64Version(UINT64 version); + UInt64Version(std::string&& version, std::string_view splitChars = DefaultSplitChars); + UInt64Version(const std::string& version, std::string_view splitChars = DefaultSplitChars) : + UInt64Version(std::string(version), splitChars) {} + + void Assign(std::string&& version, std::string_view splitChars = DefaultSplitChars) override; + void Assign(UINT64 version); + + UINT64 Major() const { return m_parts.size() > 0 ? m_parts[0].Integer : 0; } + UINT64 Minor() const { return m_parts.size() > 1 ? m_parts[1].Integer : 0; } + UINT64 Build() const { return m_parts.size() > 2 ? m_parts[2].Integer : 0; } + UINT64 Revision() const { return m_parts.size() > 3 ? m_parts[3].Integer : 0; } }; // Version range represented by a min version and max version, both inclusive. diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h @@ -35,6 +35,7 @@ namespace AppInstaller::Manifest { bool SchemaValidationOnly = false; bool ErrorOnVerifiedPublisherFields = false; + bool InstallerValidation = false; // Options not exposed in winget util bool FullValidation = false; @@ -57,7 +58,7 @@ namespace AppInstaller::Manifest bool HasExtension() const; -bool HasExtension(std::string_view extension) const; + bool HasExtension(std::string_view extension) const; private: std::vector<Version> m_extensions; diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h @@ -51,6 +51,10 @@ namespace AppInstaller::Manifest const char* const ExceededAppsAndFeaturesEntryLimit = "Only zero or one entry for Apps and Features may be specified for InstallerType portable."; const char* const ExceededCommandsLimit = "Only zero or one value for Commands may be specified for InstallerType portable."; const char* const ScopeNotSupported = "Scope is not supported for InstallerType portable."; + const char* const InstallerMsixInconsistencies = "Inconsistent value in the manifest."; + const char* const OptionalFieldMissing = "Optional field missing."; + const char* const InstallerFailedToProcess = "Failed to process installer."; + const char* const NoSupportedPlatforms = "No supported platforms."; const char* const ApproximateVersionNotAllowed = "Approximate version not allowed."; const char* const ArpVersionOverlapWithIndex = "DisplayVersion declared in the manifest has overlap with existing DisplayVersion range in the index. Existing DisplayVersion range in index: "; const char* const ArpVersionValidationInternalError = "Internal error while validating DisplayVersion against index."; @@ -217,4 +221,5 @@ namespace AppInstaller::Manifest // fullValidation: bool to set if manifest validation should perform extra validation that is not required for reading a manifest. std::vector<ValidationError> ValidateManifest(const Manifest& manifest, bool fullValidation = true); std::vector<ValidationError> ValidateManifestLocalization(const ManifestLocalization& localization, bool treatErrorAsWarning = false); + std::vector<ValidationError> ValidateManifestInstallers(const Manifest& manifest, bool treatErrorAsWarning = false); } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/MsixManifest.h b/src/AppInstallerCommonCore/Public/winget/MsixManifest.h @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once + +#include "AppInstallerStrings.h" +#include "AppInstallerVersions.h" + +#include <AppxPackaging.h> +#include <wrl/client.h> + +namespace AppInstaller::Msix +{ + using PackageVersion = Utility::UInt64Version; + using OSVersion = Utility::UInt64Version; + + // Package identity for an MSIX manifest + struct MsixPackageManifestIdentity + { + MsixPackageManifestIdentity(Microsoft::WRL::ComPtr<IAppxManifestPackageId> packageId) + : m_packageId(packageId) {} + + Utility::NormalizedString GetPackageFamilyName() const; + PackageVersion GetVersion() const; + private: + Microsoft::WRL::ComPtr<IAppxManifestPackageId> m_packageId; + }; + + // Target device family for an MSIX manifest + struct MsixPackageManifestTargetDeviceFamily + { + // Target device family names + static constexpr std::string_view WindowsDesktopName = "Windows.Desktop"; + static constexpr std::string_view WindowsUniversalName = "Windows.Universal"; + + enum Platform + { + WindowsDesktop, + WindowsUniversal, + Other, + }; + + MsixPackageManifestTargetDeviceFamily(Microsoft::WRL::ComPtr<IAppxManifestTargetDeviceFamily> targetDeviceFamily) + : m_targetDeviceFamily(targetDeviceFamily) {} + + std::string GetName() const; + OSVersion GetMinVersion() const; + Platform GetPlatform() const; + private: + Microsoft::WRL::ComPtr<IAppxManifestTargetDeviceFamily> m_targetDeviceFamily; + }; + + // MSIX manifest + struct MsixPackageManifest + { + MsixPackageManifest(Microsoft::WRL::ComPtr<IAppxManifestReader> manifestReader) + : m_manifestReader(manifestReader) {} + + std::vector<MsixPackageManifestTargetDeviceFamily> GetTargetDeviceFamilies() const; + MsixPackageManifestIdentity GetIdentity() const; + std::optional<OSVersion> GetMinimumOSVersionForSupportedPlatforms() const; + private: + Microsoft::WRL::ComPtr<IAppxManifestReader> m_manifestReader; + }; + + // MSIX manifest cache + struct MsixPackageManifestCache + { + // Construct MSIX manifest or fetch it from cache + const std::vector<MsixPackageManifest>& GetAppPackageManifests(std::string url); + private: + // Map installer url to a vector of msix package manifests + std::map<std::string, std::vector<MsixPackageManifest>> m_msixManifests; + }; +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Versions.cpp b/src/AppInstallerCommonCore/Versions.cpp @@ -69,7 +69,14 @@ namespace AppInstaller::Utility pos += length + 1; } - // Remove trailing empty versions (0 or empty) + // Trim version parts + Trim(); + + THROW_HR_IF(E_INVALIDARG, m_approximateComparator != ApproximateComparator::None && IsBaseVersionUnknown()); + } + + void Version::Trim() + { while (!m_parts.empty()) { const Part& part = m_parts.back(); @@ -79,11 +86,9 @@ namespace AppInstaller::Utility } else { - break; + return; } } - - THROW_HR_IF(E_INVALIDARG, m_approximateComparator != ApproximateComparator::None && IsBaseVersionUnknown()); } bool Version::operator<(const Version& other) const @@ -356,6 +361,56 @@ namespace AppInstaller::Utility return m_version < other.m_version; } + UInt64Version::UInt64Version(UINT64 version) + { + Assign(version); + } + + void UInt64Version::Assign(UINT64 version) + { + const UINT64 mask16 = (1 << 16) - 1; + UINT64 revision = version & mask16; + UINT64 build = (version >> 0x10) & mask16; + UINT64 minor = (version >> 0x20) & mask16; + UINT64 major = (version >> 0x30) & mask16; + + // Construct a string representation of the provided version + std::stringstream ssVersion; + ssVersion << major + << Version::DefaultSplitChars << minor + << Version::DefaultSplitChars << build + << Version::DefaultSplitChars << revision; + m_version = ssVersion.str(); + + // Construct the 4 parts + m_parts = { major, minor, build, revision }; + + // Trim version parts + Trim(); + } + + UInt64Version::UInt64Version(std::string&& version, std::string_view splitChars) + { + Assign(std::move(version), splitChars); + } + + void UInt64Version::Assign(std::string&& version, std::string_view splitChars) + { + Version::Assign(std::move(version), splitChars); + + // After trimming trailing parts (0 or empty), + // at most 4 parts must be present + THROW_HR_IF(E_INVALIDARG, m_parts.size() > 4); + for (const auto& part : m_parts) + { + // Check for non-empty Other part + THROW_HR_IF(E_INVALIDARG, !part.Other.empty()); + + // Check for overflow Integer part + THROW_HR_IF(E_INVALIDARG, part.Integer >> 16 != 0); + } + } + VersionRange::VersionRange(Version minVersion, Version maxVersion) { THROW_HR_IF(E_INVALIDARG, minVersion > maxVersion); diff --git a/src/WinGetUtil/Exports.cpp b/src/WinGetUtil/Exports.cpp @@ -231,6 +231,7 @@ extern "C" validateOption.ThrowOnWarning = true; validateOption.SchemaValidationOnly = WI_IsFlagSet(option, WinGetValidateManifestOption::SchemaValidationOnly); validateOption.ErrorOnVerifiedPublisherFields = WI_IsFlagSet(option, WinGetValidateManifestOption::ErrorOnVerifiedPublisherFields); + validateOption.InstallerValidation = WI_IsFlagSet(option, WinGetValidateManifestOption::InstallerValidations); (void)YamlParser::CreateFromPath(inputPath, validateOption, mergedManifestPath ? mergedManifestPath : L""); diff --git a/src/WinGetUtil/WinGetUtil.h b/src/WinGetUtil/WinGetUtil.h @@ -25,6 +25,7 @@ extern "C" Default = 0, SchemaValidationOnly = 0x1, ErrorOnVerifiedPublisherFields = 0x2, + InstallerValidations = 0x4, }; enum WinGetCreateManifestOption