winget-cli

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

commit db2014fceea5fc5f3f3a7e4ba47a0f1bdcfa3b8f
parent 9d34634d6868b26b606bf77c18d5978fd2eef907
Author: yao-msft <50888816+yao-msft@users.noreply.github.com>
Date:   Fri, 10 Jun 2022 01:11:26 -0700

Use AppsAndFeaturesEntries DisplayVersion info for installed package version mapping (#2213)


Diffstat:
Msrc/AppInstallerCLICore/Workflows/UpdateFlow.cpp | 2+-
Msrc/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj | 11-----------
Msrc/AppInstallerCLIE2ETests/Constants.cs | 9+++++++++
Msrc/AppInstallerCLIE2ETests/ListCommand.cs | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLIE2ETests/TestData/Manifests/TestArpVersionMapping_OppositeOrder_1.0.yaml | 18++++++++++++++++++
Asrc/AppInstallerCLIE2ETests/TestData/Manifests/TestArpVersionMapping_OppositeOrder_2.0.yaml | 18++++++++++++++++++
Asrc/AppInstallerCLIE2ETests/TestData/Manifests/TestArpVersionMapping_SameAsPackageVersion.yaml | 17+++++++++++++++++
Asrc/AppInstallerCLIE2ETests/TestData/Manifests/TestArpVersionMapping_SameOrder_1.0.yaml | 18++++++++++++++++++
Asrc/AppInstallerCLIE2ETests/TestData/Manifests/TestArpVersionMapping_SameOrder_2.0.yaml | 18++++++++++++++++++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 15+++++++++++++++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 15+++++++++++++++
Msrc/AppInstallerCLITests/SQLiteIndex.cpp | 248++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Asrc/AppInstallerCLITests/TestData/Manifest-Bad-ApproximateVersionInArpVersion.yaml | 19+++++++++++++++++++
Asrc/AppInstallerCLITests/TestData/Manifest-Bad-ApproximateVersionInPackageVersion.yaml | 17+++++++++++++++++
Asrc/AppInstallerCLITests/TestData/Manifest-Good-MultipleArpVersionDeclared.yaml | 25+++++++++++++++++++++++++
Asrc/AppInstallerCLITests/TestData/Manifest-Good-NoArpVersionDeclared.yaml | 17+++++++++++++++++
Asrc/AppInstallerCLITests/TestData/Manifest-Good-SingleArpVersionDeclared.yaml | 19+++++++++++++++++++
Msrc/AppInstallerCLITests/TestSource.cpp | 4++++
Msrc/AppInstallerCLITests/Versions.cpp | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/WorkFlow.cpp | 11+++++++----
Msrc/AppInstallerCLITests/YamlManifest.cpp | 18++++++++++++++++++
Msrc/AppInstallerCommonCore/Errors.cpp | 8++++++--
Msrc/AppInstallerCommonCore/Manifest/Manifest.cpp | 41+++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Manifest/ManifestCommon.cpp | 12++++++++++++
Msrc/AppInstallerCommonCore/Manifest/ManifestValidation.cpp | 26+++++++++++++++++++++++++-
Msrc/AppInstallerCommonCore/Public/AppInstallerErrors.h | 3++-
Msrc/AppInstallerCommonCore/Public/AppInstallerVersions.h | 64+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/AppInstallerCommonCore/Public/winget/Manifest.h | 4++++
Msrc/AppInstallerCommonCore/Public/winget/ManifestCommon.h | 3+++
Msrc/AppInstallerCommonCore/Public/winget/ManifestValidation.h | 3+++
Msrc/AppInstallerCommonCore/Versions.cpp | 187+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj | 6++++++
Msrc/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters | 21+++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/ArpVersionValidation.cpp | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/ArpVersionValidation.h | 11+++++++++++
Msrc/AppInstallerRepositoryCore/CompositeSource.cpp | 161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp | 1+
Msrc/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp | 2+-
Msrc/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.cpp | 20++++++++++++++------
Msrc/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.h | 28++++++++++++++++++++++------
Asrc/AppInstallerRepositoryCore/Microsoft/Schema/1_0/VirtualTableBase.h | 20++++++++++++++++++++
Msrc/AppInstallerRepositoryCore/Microsoft/Schema/1_3/HashVirtualTable.h | 2+-
Msrc/AppInstallerRepositoryCore/Microsoft/Schema/1_4/Interface.h | 2+-
Msrc/AppInstallerRepositoryCore/Microsoft/Schema/1_4/Interface_1_4.cpp | 7+++----
Asrc/AppInstallerRepositoryCore/Microsoft/Schema/1_5/ArpVersionVirtualTable.h | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Microsoft/Schema/1_5/Interface.h | 34++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Microsoft/Schema/1_5/Interface_1_5.cpp | 231+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerRepositoryCore/Microsoft/Schema/Version.cpp | 9+++++++--
Msrc/AppInstallerRepositoryCore/PackageDependenciesValidation.cpp | 14+++-----------
Msrc/AppInstallerRepositoryCore/PackageDependenciesValidation.h | 8+++-----
Msrc/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h | 2++
Msrc/AppInstallerRepositoryCore/Rest/RestSource.cpp | 22+++++++++++++++++++++-
Msrc/AppInstallerTestExeInstaller/main.cpp | 45++++++++++++++++++++++++++++++---------------
Msrc/WinGetUtil/Exports.cpp | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/WinGetUtil/Source.def | 3+++
Msrc/WinGetUtil/WinGetUtil.h | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
56 files changed, 1893 insertions(+), 109 deletions(-)

diff --git a/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp b/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp @@ -16,7 +16,7 @@ namespace AppInstaller::CLI::Workflow { bool IsUpdateVersionApplicable(const Utility::Version& installedVersion, const Utility::Version& updateVersion) { - return (installedVersion < updateVersion || updateVersion.IsLatest()); + return installedVersion < updateVersion; } void AddToPackagesToInstallIfNotPresent(std::vector<std::unique_ptr<Execution::Context>>& packagesToInstall, std::unique_ptr<Execution::Context> packageContext) diff --git a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj @@ -23,17 +23,6 @@ </ItemGroup> <ItemGroup> - <None Remove="TestData\ImportFiles\ImportFile-Bad-Invalid.json" /> - <None Remove="TestData\ImportFiles\ImportFile-Bad-UnknownPackage.json" /> - <None Remove="TestData\ImportFiles\ImportFile-Bad-UnknownPackageVersion.json" /> - <None Remove="TestData\ImportFiles\ImportFile-Bad-UnknownSource.json" /> - <None Remove="TestData\ImportFiles\ImportFile-Good.1.0.json" /> - <None Remove="TestData\ImportFiles\ImportFile-Good.2.0.json" /> - <None Remove="TestData\Manifests\TestPortableInstaller.2.0.0.0.yaml" /> - <None Remove="TestData\Manifests\TestPortableInstaller.3.0.0.0_UninstallPrevious.yaml" /> - </ItemGroup> - - <ItemGroup> <Content Include="..\..\doc\admx\DesktopAppInstaller.admx" Link="TestData\DesktopAppInstaller.admx"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content> diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs @@ -149,6 +149,14 @@ namespace AppInstallerCLIE2ETests public const int ERROR_INVALID_TABLE_COLUMN = unchecked((int)0x8A15004E); public const int ERROR_UPGRADE_VERSION_NOT_NEWER = unchecked((int)0x8A15004F); public const int ERROR_UPGRADE_VERSION_UNKNOWN = unchecked((int)0x8A150050); + public const int ERROR_ICU_CONVERSION_ERROR = unchecked((int)0x8A150051); + public const int ERROR_PORTABLE_INSTALL_FAILED = unchecked((int)0x8A150052); + public const int ERROR_PORTABLE_REPARSE_POINT_NOT_SUPPORTED = unchecked((int)0x8A150053); + public const int ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS = unchecked((int)0x8A150054); + public const int ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY = unchecked((int)0x8A150055); + public const int ERROR_INSTALLER_PROHIBITS_ELEVATION = unchecked((int)0x8A150056); + public const int ERROR_PORTABLE_UNINSTALL_FAILED = unchecked((int)0x8A150057); + public const int ERROR_ARP_VERSION_VALIDATION_FAILED = unchecked((int)0x8A150058); public const int ERROR_INSTALL_PACKAGE_IN_USE = unchecked((int)0x8A150101); public const int ERROR_INSTALL_INSTALL_IN_PROGRESS = unchecked((int)0x8A150102); @@ -165,6 +173,7 @@ namespace AppInstallerCLIE2ETests public const int ERROR_INSTALL_ALREADY_INSTALLED = unchecked((int)0x8A15010D); public const int ERROR_INSTALL_DOWNGRADE = unchecked((int)0x8A15010E); public const int ERROR_INSTALL_BLOCKED_BY_POLICY = unchecked((int)0x8A15010F); + public const int ERROR_INSTALL_DEPENDENCIES = unchecked((int)0x8A150110); } } } diff --git a/src/AppInstallerCLIE2ETests/ListCommand.cs b/src/AppInstallerCLIE2ETests/ListCommand.cs @@ -38,5 +38,46 @@ namespace AppInstallerCLIE2ETests Assert.True(result.StdOut.Contains("1.0.0.0")); Assert.True(result.StdOut.Contains("2.0.0.0")); } + + [Test] + public void ListWithArpVersionMapping() + { + // No mapping performed + ArpVersionMappingTest("AppInstallerTest.TestArpVersionSameVersion", "TestArpVersionSameVersion", "0.5", "0.5", "< 1.0"); + + // Partial mapping performed(i.e. only if version falls within arp version range) + ArpVersionMappingTest("AppInstallerTest.TestArpVersionOppositeOrder", "TestArpVersionOppositeOrder", "10.1", "1.0", "10.1"); + ArpVersionMappingTest("AppInstallerTest.TestArpVersionOppositeOrder", "TestArpVersionOppositeOrder", "9.9", "9.9", "> 2.0"); + + // Full mapping performed + ArpVersionMappingTest("AppInstallerTest.TestArpVersionSameOrder", "TestArpVersionSameOrder", "7.0", "< 1.0", "7.0"); + ArpVersionMappingTest("AppInstallerTest.TestArpVersionSameOrder", "TestArpVersionSameOrder", "10.1", "1.0", "10.1"); + ArpVersionMappingTest("AppInstallerTest.TestArpVersionSameOrder", "TestArpVersionSameOrder", "10.7", "< 2.0", "10.7"); + ArpVersionMappingTest("AppInstallerTest.TestArpVersionSameOrder", "TestArpVersionSameOrder", "11.1", "2.0", "11.1"); + ArpVersionMappingTest("AppInstallerTest.TestArpVersionSameOrder", "TestArpVersionSameOrder", "12.0", "> 2.0", "12.0"); + } + + private void ArpVersionMappingTest(string packageIdentifier, string displayNameOverride, string displayVersionOverride, string expectedListVersion, string notExpectedListVersion = "") + { + System.Guid guid = System.Guid.NewGuid(); + string productCode = guid.ToString(); + var installDir = TestCommon.GetRandomTestDir(); + + var result = TestCommon.RunAICLICommand("list", productCode); + Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); + + result = TestCommon.RunAICLICommand("install", $"{packageIdentifier} --override \"/InstallDir {installDir} /ProductID {productCode} /DisplayName {displayNameOverride} /Version {displayVersionOverride}\""); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + + result = TestCommon.RunAICLICommand("list", productCode); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + + Assert.True(result.StdOut.Contains(packageIdentifier)); + Assert.True(result.StdOut.Contains(expectedListVersion)); + if (!string.IsNullOrEmpty(notExpectedListVersion)) + { + Assert.False(result.StdOut.Contains(notExpectedListVersion)); + } + } } } diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestArpVersionMapping_OppositeOrder_1.0.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestArpVersionMapping_OppositeOrder_1.0.yaml @@ -0,0 +1,17 @@ +PackageIdentifier: AppInstallerTest.TestArpVersionOppositeOrder +PackageVersion: 1.0 +PackageName: TestArpVersionOppositeOrder +PackageLocale: en-US +Publisher: Microsoft +License: Test +ShortDescription: E2E test for arp version test. +Installers: + - Architecture: x86 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: exe + InstallerSha256: <EXEHASH> + AppsAndFeaturesEntries: + - DisplayVersion: "10.0" + - DisplayVersion: "10.5" +ManifestType: singleton +ManifestVersion: 1.2.0+ \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestArpVersionMapping_OppositeOrder_2.0.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestArpVersionMapping_OppositeOrder_2.0.yaml @@ -0,0 +1,17 @@ +PackageIdentifier: AppInstallerTest.TestArpVersionOppositeOrder +PackageVersion: 2.0 +PackageName: TestArpVersionOppositeOrder +PackageLocale: en-US +Publisher: Microsoft +License: Test +ShortDescription: E2E test for arp version test. +Installers: + - Architecture: x86 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: exe + InstallerSha256: <EXEHASH> + AppsAndFeaturesEntries: + - DisplayVersion: "9.0" + - DisplayVersion: "9.5" +ManifestType: singleton +ManifestVersion: 1.2.0+ \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestArpVersionMapping_SameAsPackageVersion.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestArpVersionMapping_SameAsPackageVersion.yaml @@ -0,0 +1,16 @@ +PackageIdentifier: AppInstallerTest.TestArpVersionSameVersion +PackageVersion: 1.0 +PackageName: TestArpVersionSameVersion +PackageLocale: en-US +Publisher: Microsoft +License: Test +ShortDescription: E2E test for arp version test. +Installers: + - Architecture: x86 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: exe + InstallerSha256: <EXEHASH> + AppsAndFeaturesEntries: + - DisplayVersion: "1.0" +ManifestType: singleton +ManifestVersion: 1.2.0+ \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestArpVersionMapping_SameOrder_1.0.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestArpVersionMapping_SameOrder_1.0.yaml @@ -0,0 +1,17 @@ +PackageIdentifier: AppInstallerTest.TestArpVersionSameOrder +PackageVersion: 1.0 +PackageName: TestArpVersionSameOrder +PackageLocale: en-US +Publisher: Microsoft +License: Test +ShortDescription: E2E test for arp version test. +Installers: + - Architecture: x86 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: exe + InstallerSha256: <EXEHASH> + AppsAndFeaturesEntries: + - DisplayVersion: "10.0" + - DisplayVersion: "10.5" +ManifestType: singleton +ManifestVersion: 1.2.0+ \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestArpVersionMapping_SameOrder_2.0.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestArpVersionMapping_SameOrder_2.0.yaml @@ -0,0 +1,17 @@ +PackageIdentifier: AppInstallerTest.TestArpVersionSameOrder +PackageVersion: 2.0 +PackageName: TestArpVersionSameOrder +PackageLocale: en-US +Publisher: Microsoft +License: Test +ShortDescription: E2E test for arp version test. +Installers: + - Architecture: x86 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: exe + InstallerSha256: <EXEHASH> + AppsAndFeaturesEntries: + - DisplayVersion: "11.0" + - DisplayVersion: "11.5" +ManifestType: singleton +ManifestVersion: 1.2.0+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -456,6 +456,12 @@ <CopyFileToFolders Include="TestData\Manifest-Bad-VersionMissing.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Bad-ApproximateVersionInArpVersion.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Bad-ApproximateVersionInPackageVersion.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\Manifest-Good-InstallerTypeExe-Silent.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> @@ -501,6 +507,15 @@ <CopyFileToFolders Include="TestData\Manifest-Good-PackageFamilyNameOnExe-Ver1_2.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Good-MultipleArpVersionDeclared.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Good-NoArpVersionDeclared.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Good-SingleArpVersionDeclared.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\Manifest-Encoding-ANSI.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -198,6 +198,12 @@ <None Include="Run-TestsInPackage.ps1" /> </ItemGroup> <ItemGroup> + <CopyFileToFolders Include="TestData\Manifest-Bad-ApproximateVersionInArpVersion.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Bad-ApproximateVersionInPackageVersion.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\Manifest-Bad-AppsAndFeaturesEntriesOnMSIX.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> @@ -393,9 +399,18 @@ <CopyFileToFolders Include="TestData\Manifest-Good-MultiLocale.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Good-MultipleArpVersionDeclared.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Good-NoArpVersionDeclared.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\Manifest-Good-PackageFamilyNameOnExe-Ver1_2.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Good-SingleArpVersionDeclared.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\Manifest-Good-Spaces.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> diff --git a/src/AppInstallerCLITests/SQLiteIndex.cpp b/src/AppInstallerCLITests/SQLiteIndex.cpp @@ -4,6 +4,7 @@ #include "TestCommon.h" #include <SQLiteWrapper.h> #include <PackageDependenciesValidation.h> +#include <ArpVersionValidation.h> #include <Microsoft/SQLiteIndex.h> #include <winget/Manifest.h> #include <AppInstallerStrings.h> @@ -34,7 +35,7 @@ SQLiteIndex CreateTestIndex(const std::string& filePath, std::optional<Schema::V // If no specific version requested, then use generator to run against the last 3 versions. if (!version) { - version = GENERATE(Schema::Version{ 1, 2 }, Schema::Version{ 1, 3 }, Schema::Version::Latest()); + version = GENERATE(Schema::Version{ 1, 2 }, Schema::Version{ 1, 3 }, Schema::Version{ 1, 4 }, Schema::Version::Latest()); } return SQLiteIndex::CreateNew(filePath, version.value()); @@ -72,6 +73,16 @@ Schema::Version TestPrepareForRead(SQLiteIndex& index) return version; } } + else if (index.GetVersion() == Schema::Version{ 1, 5 }) + { + Schema::Version version = GENERATE(Schema::Version{ 1, 2 }, Schema::Version{ 1, 3 }, Schema::Version{ 1, 4 }, Schema::Version{ 1, 5 }); + + if (version != Schema::Version{ 1, 5 }) + { + index.ForceVersion(version); + return version; + } + } return index.GetVersion(); } @@ -304,6 +315,12 @@ bool AreManifestHashesSupported(const SQLiteIndex& index, const Schema::Version& return (index.GetVersion() >= Schema::Version{ 1, 3 } && testVersion >= Schema::Version{ 1, 3 }); } +bool AreArpVersionsSupported(const SQLiteIndex& index, const Schema::Version& testVersion) +{ + UNSCOPED_INFO("Index " << index.GetVersion() << " | Test " << testVersion); + return (index.GetVersion() >= Schema::Version{ 1, 5 } && testVersion >= Schema::Version{ 1, 5 }); +} + std::string GetPropertyStringByKey(const SQLiteIndex& index, SQLite::rowid_t id, PackageVersionProperty property, std::string_view version, std::string_view channel) { auto manifestId = index.GetManifestIdByKey(id, version, channel); @@ -955,8 +972,6 @@ TEST_CASE("SQLiteIndex_RemoveManifest_EnsureConsistentRowId", "[sqliteindex]") // Checking consistency will also uncover issues, but not potentially the same ones as below. REQUIRE(index.CheckConsistency(true)); - - // Repeat search to ensure consistent ids result = index.Search(request); REQUIRE(result.Matches.size() == 1); @@ -2865,3 +2880,229 @@ TEST_CASE("SQLiteIndex_ManifestHash_Missing", "[sqliteindex]") REQUIRE(!hashResult); } + +TEST_CASE("SQLiteIndex_ManifestArpVersion_Present_Add", "[sqliteindex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + SQLiteIndex index = CreateTestIndex(tempFile); + + Manifest manifest; + manifest.Id = "Foo"; + manifest.Version = "Bar"; + manifest.Installers.push_back({}); + manifest.Installers[0].InstallerType = InstallerTypeEnum::Exe; + manifest.Installers[0].AppsAndFeaturesEntries.push_back({}); + manifest.Installers[0].AppsAndFeaturesEntries[0].DisplayVersion = "1.0"; + manifest.Installers[0].AppsAndFeaturesEntries.push_back({}); + manifest.Installers[0].AppsAndFeaturesEntries[1].DisplayVersion = "1.1"; + + index.AddManifest(manifest, "path"); + + Schema::Version testVersion = TestPrepareForRead(index); + + auto results = index.Search({}); + REQUIRE(results.Matches.size() == 1); + + auto arpMin = index.GetPropertyByManifestId(results.Matches[0].first, PackageVersionProperty::ArpMinVersion); + auto arpMax = index.GetPropertyByManifestId(results.Matches[0].first, PackageVersionProperty::ArpMaxVersion); + + if (AreArpVersionsSupported(index, testVersion)) + { + REQUIRE(arpMin); + REQUIRE(arpMin.value() == "1.0"); + REQUIRE(arpMax); + REQUIRE(arpMax.value() == "1.1"); + } + else + { + REQUIRE_FALSE(arpMin); + REQUIRE_FALSE(arpMax); + } +} + +TEST_CASE("SQLiteIndex_ManifestArpVersion_Present_AddThenUpdate", "[sqliteindex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + SQLiteIndex index = CreateTestIndex(tempFile); + + Manifest manifest; + manifest.Id = "Foo"; + manifest.Version = "Bar"; + manifest.Installers.push_back({}); + manifest.Installers[0].InstallerType = InstallerTypeEnum::Exe; + manifest.Installers[0].AppsAndFeaturesEntries.push_back({}); + manifest.Installers[0].AppsAndFeaturesEntries[0].DisplayVersion = "1.0"; + manifest.Installers[0].AppsAndFeaturesEntries.push_back({}); + manifest.Installers[0].AppsAndFeaturesEntries[1].DisplayVersion = "1.1"; + + index.AddManifest(manifest, "path"); + + manifest.Installers[0].AppsAndFeaturesEntries[0].DisplayVersion = "1.1"; + + index.UpdateManifest(manifest, "path"); + + Schema::Version testVersion = TestPrepareForRead(index); + + auto results = index.Search({}); + REQUIRE(results.Matches.size() == 1); + + auto arpMin = index.GetPropertyByManifestId(results.Matches[0].first, PackageVersionProperty::ArpMinVersion); + auto arpMax = index.GetPropertyByManifestId(results.Matches[0].first, PackageVersionProperty::ArpMaxVersion); + + if (AreArpVersionsSupported(index, testVersion)) + { + REQUIRE(arpMin); + REQUIRE(arpMin.value() == "1.1"); + REQUIRE(arpMax); + REQUIRE(arpMax.value() == "1.1"); + } + else + { + REQUIRE_FALSE(arpMin); + REQUIRE_FALSE(arpMax); + } +} + +TEST_CASE("SQLiteIndex_ManifestArpVersion_Empty", "[sqliteindex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + SQLiteIndex index = CreateTestIndex(tempFile); + + Manifest manifest; + manifest.Id = "Foo"; + manifest.Version = "Bar"; + index.AddManifest(manifest, "path"); + + Schema::Version testVersion = TestPrepareForRead(index); + + auto results = index.Search({}); + REQUIRE(results.Matches.size() == 1); + + auto arpMin = index.GetPropertyByManifestId(results.Matches[0].first, PackageVersionProperty::ArpMinVersion); + auto arpMax = index.GetPropertyByManifestId(results.Matches[0].first, PackageVersionProperty::ArpMaxVersion); + + if (AreArpVersionsSupported(index, testVersion)) + { + REQUIRE(arpMin); + REQUIRE(arpMin.value() == ""); + REQUIRE(arpMax); + REQUIRE(arpMax.value() == ""); + } + else + { + REQUIRE_FALSE(arpMin); + REQUIRE_FALSE(arpMax); + } +} + +TEST_CASE("SQLiteIndex_RemoveManifestArpVersionKeepUsedDeleteUnused", "[sqliteindex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + SQLiteIndex index = CreateTestIndex(tempFile, Schema::Version::Latest()); + + Manifest manifest; + manifest.Id = "Foo"; + manifest.Version = "10.0"; + manifest.Installers.push_back({}); + manifest.Installers[0].InstallerType = InstallerTypeEnum::Exe; + manifest.Installers[0].AppsAndFeaturesEntries.push_back({}); + manifest.Installers[0].AppsAndFeaturesEntries[0].DisplayVersion = "1.0"; + manifest.Installers[0].AppsAndFeaturesEntries.push_back({}); + manifest.Installers[0].AppsAndFeaturesEntries[1].DisplayVersion = "1.1"; + + index.AddManifest(manifest, "path"); + + Manifest manifest2; + manifest2.Id = "Foo2"; + manifest2.Version = "1.0"; + manifest2.Installers.push_back({}); + manifest2.Installers[0].InstallerType = InstallerTypeEnum::Exe; + manifest2.Installers[0].AppsAndFeaturesEntries.push_back({}); + manifest2.Installers[0].AppsAndFeaturesEntries[0].DisplayVersion = "10.0"; + + index.AddManifest(manifest2, "path2"); + + // Before removing, "10.0", "1.0" and "1.1" should all exist. + { + Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadOnly); + REQUIRE(Schema::V1_0::VersionTable::SelectIdByValue(connection, "10.0").has_value()); + REQUIRE(Schema::V1_0::VersionTable::SelectIdByValue(connection, "1.0").has_value()); + REQUIRE(Schema::V1_0::VersionTable::SelectIdByValue(connection, "1.1").has_value()); + } + + index.RemoveManifest(manifest); + + // After removing the first manifest, "10.0" and "1.0" should still stay, "1.1" should be removed. + { + Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadOnly); + REQUIRE(Schema::V1_0::VersionTable::SelectIdByValue(connection, "10.0").has_value()); + REQUIRE(Schema::V1_0::VersionTable::SelectIdByValue(connection, "1.0").has_value()); + REQUIRE_FALSE(Schema::V1_0::VersionTable::SelectIdByValue(connection, "1.1").has_value()); + } +} + +TEST_CASE("SQLiteIndex_ManifestArpVersion_CheckConsistency", "[sqliteindex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + SQLiteIndex index = CreateTestIndex(tempFile, Schema::Version::Latest()); + + Manifest manifest; + manifest.Id = "Foo"; + manifest.Version = "10.0"; + manifest.DefaultLocalization.Add<Localization::PackageName>("ArpVersionCheckConsistencyTest"); + manifest.Moniker = "testmoniker"; + manifest.Installers.push_back({}); + manifest.Installers[0].InstallerType = InstallerTypeEnum::Exe; + manifest.Installers[0].AppsAndFeaturesEntries.push_back({}); + manifest.Installers[0].AppsAndFeaturesEntries[0].DisplayVersion = "1.0"; + manifest.Installers[0].AppsAndFeaturesEntries.push_back({}); + manifest.Installers[0].AppsAndFeaturesEntries[1].DisplayVersion = "1.1"; + + index.AddManifest(manifest, "path"); + + REQUIRE(index.CheckConsistency(true)); + + // Add a conflicting one + manifest.Version = "10.1"; + + index.AddManifest(manifest, "path2"); + + REQUIRE_FALSE(index.CheckConsistency(true)); +} + +TEST_CASE("SQLiteIndex_ManifestArpVersion_ValidateManifestAgainstIndex", "[sqliteindex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + SQLiteIndex index = CreateTestIndex(tempFile, Schema::Version::Latest()); + + Manifest manifest; + manifest.Id = "Foo"; + manifest.Version = "10.0"; + manifest.Installers.push_back({}); + manifest.Installers[0].InstallerType = InstallerTypeEnum::Exe; + manifest.Installers[0].AppsAndFeaturesEntries.push_back({}); + manifest.Installers[0].AppsAndFeaturesEntries[0].DisplayVersion = "1.0"; + manifest.Installers[0].AppsAndFeaturesEntries.push_back({}); + manifest.Installers[0].AppsAndFeaturesEntries[1].DisplayVersion = "1.1"; + + index.AddManifest(manifest, "path"); + + // Updating same version should not result in failure. + REQUIRE_NOTHROW(ValidateManifestArpVersion(&index, manifest)); + + // Add different version should result in failure. + manifest.Version = "10.1"; + REQUIRE_THROWS(ValidateManifestArpVersion(&index, manifest)); +}+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/Manifest-Bad-ApproximateVersionInArpVersion.yaml b/src/AppInstallerCLITests/TestData/Manifest-Bad-ApproximateVersionInArpVersion.yaml @@ -0,0 +1,19 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +PackageLocale: en-US +Publisher: Microsoft +Author: Microsoft +PackageName: MSIX SDK +License: MIT License +ShortDescription: This is MSIX SDK + +Installers: + - Architecture: x86 + InstallerType: inno + InstallerUrl: https://www.microsoft.com/msixsdk/setup.exe + InstallerSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + AppsAndFeaturesEntries: + - DisplayVersion: "< 12.0" + +ManifestType: singleton +ManifestVersion: 1.1.0 diff --git a/src/AppInstallerCLITests/TestData/Manifest-Bad-ApproximateVersionInPackageVersion.yaml b/src/AppInstallerCLITests/TestData/Manifest-Bad-ApproximateVersionInPackageVersion.yaml @@ -0,0 +1,17 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: "> 1.7.32" +PackageLocale: en-US +Publisher: Microsoft +Author: Microsoft +PackageName: MSIX SDK +License: MIT License +ShortDescription: This is MSIX SDK + +Installers: + - Architecture: x86 + InstallerType: inno + InstallerUrl: https://www.microsoft.com/msixsdk/setup.exe + InstallerSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + +ManifestType: singleton +ManifestVersion: 1.1.0 diff --git a/src/AppInstallerCLITests/TestData/Manifest-Good-MultipleArpVersionDeclared.yaml b/src/AppInstallerCLITests/TestData/Manifest-Good-MultipleArpVersionDeclared.yaml @@ -0,0 +1,25 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +PackageLocale: en-US +Publisher: Microsoft +Author: Microsoft +PackageName: MSIX SDK +License: MIT License +ShortDescription: This is MSIX SDK + +Installers: + - Architecture: x86 + InstallerType: inno + InstallerUrl: https://www.microsoft.com/msixsdk/setup.exe + InstallerSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + AppsAndFeaturesEntries: + - DisplayVersion: "12.0" + - Architecture: x64 + InstallerType: inno + InstallerUrl: https://www.microsoft.com/msixsdk/setup.exe + InstallerSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + AppsAndFeaturesEntries: + - DisplayVersion: "13.0" + +ManifestType: merged +ManifestVersion: 1.1.0 diff --git a/src/AppInstallerCLITests/TestData/Manifest-Good-NoArpVersionDeclared.yaml b/src/AppInstallerCLITests/TestData/Manifest-Good-NoArpVersionDeclared.yaml @@ -0,0 +1,17 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +PackageLocale: en-US +Publisher: Microsoft +Author: Microsoft +PackageName: MSIX SDK +License: MIT License +ShortDescription: This is MSIX SDK + +Installers: + - Architecture: x86 + InstallerType: inno + InstallerUrl: https://www.microsoft.com/msixsdk/setup.exe + InstallerSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + +ManifestType: singleton +ManifestVersion: 1.1.0 diff --git a/src/AppInstallerCLITests/TestData/Manifest-Good-SingleArpVersionDeclared.yaml b/src/AppInstallerCLITests/TestData/Manifest-Good-SingleArpVersionDeclared.yaml @@ -0,0 +1,19 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +PackageLocale: en-US +Publisher: Microsoft +Author: Microsoft +PackageName: MSIX SDK +License: MIT License +ShortDescription: This is MSIX SDK + +Installers: + - Architecture: x86 + InstallerType: inno + InstallerUrl: https://www.microsoft.com/msixsdk/setup.exe + InstallerSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + AppsAndFeaturesEntries: + - DisplayVersion: "11.0" + +ManifestType: singleton +ManifestVersion: 1.1.0 diff --git a/src/AppInstallerCLITests/TestSource.cpp b/src/AppInstallerCLITests/TestSource.cpp @@ -52,6 +52,10 @@ namespace TestCommon return LocIndString{ Source.lock()->GetIdentifier() }; case PackageVersionProperty::Publisher: return LocIndString{ VersionManifest.DefaultLocalization.Get<AppInstaller::Manifest::Localization::Publisher>() }; + case PackageVersionProperty::ArpMinVersion: + return LocIndString{ VersionManifest.GetArpVersionRange().IsEmpty() ? "" : VersionManifest.GetArpVersionRange().GetMinVersion().ToString() }; + case PackageVersionProperty::ArpMaxVersion: + return LocIndString{ VersionManifest.GetArpVersionRange().IsEmpty() ? "" : VersionManifest.GetArpVersionRange().GetMaxVersion().ToString() }; default: return {}; } diff --git a/src/AppInstallerCLITests/Versions.cpp b/src/AppInstallerCLITests/Versions.cpp @@ -191,3 +191,69 @@ TEST_CASE("VersionUnknownLessThanLatest", "[versions]") { REQUIRE(Version::CreateUnknown() < Version::CreateLatest()); } + +TEST_CASE("ApproximateVersionParse", "[versions]") +{ + Version v1_0{ "1.0" }; + Version v1_0_LessThan{ v1_0, Version::ApproximateComparator::LessThan }; + Version v1_0_GreaterThan{ v1_0, Version::ApproximateComparator::GreaterThan }; + + Version v1_0_LessThanFromString = Version{ "< 1.0" }; + Version v1_0_GreaterThanFromString = Version{ "> 1.0" }; + + REQUIRE_FALSE(v1_0.IsApproximate()); + REQUIRE(v1_0_LessThanFromString.IsApproximate()); + REQUIRE(v1_0_GreaterThanFromString.IsApproximate()); + + REQUIRE(v1_0_LessThan == v1_0_LessThanFromString); + REQUIRE(v1_0_GreaterThan == v1_0_GreaterThanFromString); + + REQUIRE_THROWS(Version{ "< Unknown" }); + REQUIRE_THROWS(Version{ v1_0_LessThan, Version::ApproximateComparator::LessThan }); + REQUIRE_THROWS(Version{ Version::CreateUnknown(), Version::ApproximateComparator::LessThan }); +} + +TEST_CASE("ApproximateVersionCompare", "[versions]") +{ + RequireEqual("< 1.0", "< 1.0"); + RequireEqual("< 1.0", "< 1.0.0"); + RequireEqual("> 1.0", "> 1.0"); + RequireEqual("> 1.0", "> 1.0.0"); + + RequireLessThan("< 1.0", "1.0"); + RequireLessThan("< 1.0", "> 1.0"); + RequireLessThan("1.0", "> 1.0"); + RequireLessThan("0.9", "< 1.0"); + RequireLessThan("> 1.0", "1.1"); + + // With latest + RequireLessThan("< latest", "latest"); + RequireLessThan("latest", "> latest"); + RequireLessThan("9999", "< latest"); +} + +TEST_CASE("VersionRange", "[versions]") +{ + // Create + REQUIRE_NOTHROW(VersionRange{ Version{ "1.0" }, Version{ "2.0" } }); + REQUIRE_NOTHROW(VersionRange{ Version{ "1.0" }, Version{ "1.0" } }); + REQUIRE_THROWS(VersionRange{ Version{ "2.0" }, Version{ "1.0" } }); + + // Overlaps + REQUIRE(VersionRange{ Version{ "1.0" }, Version{ "2.0" } }.Overlaps(VersionRange{ Version{ "2.0" }, Version{ "3.0" } })); + REQUIRE(VersionRange{ Version{ "1.0" }, Version{ "2.0" } }.Overlaps(VersionRange{ Version{ "1.0" }, Version{ "1.0" } })); + REQUIRE(VersionRange{ Version{ "1.0" }, Version{ "2.0" } }.Overlaps(VersionRange{ Version{ "0.5" }, Version{ "1.5" } })); + REQUIRE_FALSE(VersionRange{ Version{ "1.0" }, Version{ "2.0" } }.Overlaps(VersionRange{ Version{ "2.1" }, Version{ "3.0" } })); + REQUIRE_FALSE(VersionRange{ Version{ "1.0" }, Version{ "2.0" } }.Overlaps(VersionRange{})); + + // Empty + REQUIRE(VersionRange{}.IsEmpty()); + REQUIRE_THROWS(VersionRange{}.GetMinVersion()); + REQUIRE_THROWS(VersionRange{}.GetMaxVersion()); + + // Less than compare + REQUIRE_THROWS(VersionRange{ Version{ "0.5" }, Version{ "1.0" } } < VersionRange{ Version{ "1.0" }, Version{ "2.0" } }); + 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/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -184,13 +184,16 @@ namespace if (input.empty() || input == "AppInstallerCliTest.TestMSStoreInstaller") { - auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_MSStore.yaml")); + auto installed = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_MSStore.yaml")); + auto available = installed; + // Override the installed version to not be Latest + installed.Version = "1.0.0.0"; result.Matches.emplace_back( ResultMatch( TestPackage::Make( - manifest, + installed, TestPackage::MetadataMap{ { PackageVersionMetadata::InstalledType, "MSStore" } }, - std::vector<Manifest>{ manifest }, + std::vector<Manifest>{ available }, shared_from_this() ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestMSStoreInstaller"))); @@ -2092,7 +2095,7 @@ TEST_CASE("ExportFlow_ExportAll_WithVersions", "[ExportFlow][workflow]") })); REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) { - return p.Id == "AppInstallerCliTest.TestMSStoreInstaller" && p.VersionAndChannel.GetVersion().ToString() == "Latest"; + return p.Id == "AppInstallerCliTest.TestMSStoreInstaller" && p.VersionAndChannel.GetVersion().ToString() == "1.0.0.0"; })); REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) { diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp @@ -282,6 +282,8 @@ TEST_CASE("ReadBadManifests", "[ManifestValidation]") { "Manifest-Bad-AppsAndFeaturesEntriesOnMSIX.yaml", "The specified installer type does not write to Apps and Features entry." }, { "InstallFlowTest_LicenseAgreement.yaml", "Field usage requires verified publishers.", true }, { "InstallFlowTest_LicenseAgreement.yaml", "Field usage requires verified publishers.", false, GetTestManifestValidateOption(false, true) }, + { "Manifest-Bad-ApproximateVersionInPackageVersion.yaml", "Approximate version not allowed. Field: PackageVersion" }, + { "Manifest-Bad-ApproximateVersionInArpVersion.yaml", "Approximate version not allowed. Field: DisplayVersion" }, }; for (auto const& testCase : TestCases) @@ -836,4 +838,20 @@ TEST_CASE("ManifestLocalizationValidation", "[ManifestValidation]") errors = ValidateManifest(manifest, false); REQUIRE(errors.size() == 1); REQUIRE(errors.at(0).ErrorLevel == ValidationError::Level::Warning); +} + +TEST_CASE("ManifestArpVersionRange", "[ManifestValidation]") +{ + Manifest manifestNoArp = YamlParser::CreateFromPath(TestDataFile("Manifest-Good-NoArpVersionDeclared.yaml")); + REQUIRE(manifestNoArp.GetArpVersionRange().IsEmpty()); + + Manifest manifestSingleArp = YamlParser::CreateFromPath(TestDataFile("Manifest-Good-SingleArpVersionDeclared.yaml")); + auto arpRangeSingleArp = manifestSingleArp.GetArpVersionRange(); + REQUIRE(arpRangeSingleArp.GetMinVersion().ToString() == "11.0"); + REQUIRE(arpRangeSingleArp.GetMaxVersion().ToString() == "11.0"); + + Manifest manifestMultiArp = YamlParser::CreateFromPath(TestDataFile("Manifest-Good-MultipleArpVersionDeclared.yaml")); + 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/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp @@ -182,8 +182,12 @@ namespace AppInstaller return "Portable package from a different source already exists."; case APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY: return "Unable to create symlink, path points to a directory."; + case APPINSTALLER_CLI_ERROR_INSTALLER_PROHIBITS_ELEVATION: + return "The installer cannot be run from an administrator context."; case APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED: return "Failed to uninstall portable package"; + case APPINSTALLER_CLI_ERROR_ARP_VERSION_VALIDATION_FAILED: + return "Failed to validate DisplayVersion values against index."; case APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE: return "Application is currently running.Exit the application then try again."; case APPINSTALLER_CLI_ERROR_INSTALL_INSTALL_IN_PROGRESS: @@ -214,8 +218,8 @@ namespace AppInstaller return "A higher version of this application is already installed."; case APPINSTALLER_CLI_ERROR_INSTALL_BLOCKED_BY_POLICY: return "Organization policies are preventing installation. Contact your admin."; - case APPINSTALLER_CLI_ERROR_INSTALLER_PROHIBITS_ELEVATION: - return "The installer cannot be run from an administrator context."; + case APPINSTALLER_CLI_ERROR_INSTALL_DEPENDENCIES: + return "Failed to install package dependencies."; default: return "Unknown Error Code"; } diff --git a/src/AppInstallerCommonCore/Manifest/Manifest.cpp b/src/AppInstallerCommonCore/Manifest/Manifest.cpp @@ -85,4 +85,45 @@ namespace AppInstaller::Manifest return resultCommands; } + + Utility::VersionRange Manifest::GetArpVersionRange() const + { + bool arpVersionFound = false; + Utility::Version minVersion; + Utility::Version maxVersion; + + for (auto const& installer : Installers) + { + if (DoesInstallerTypeSupportArpVersionRange(installer.InstallerType)) + { + for (auto const& entry : installer.AppsAndFeaturesEntries) + { + if (!entry.DisplayVersion.empty()) + { + Utility::Version arpVersion{ entry.DisplayVersion }; + + if (!arpVersionFound) + { + // This is the first arp version found, populate both min and max version + minVersion = arpVersion; + maxVersion = arpVersion; + arpVersionFound = true; + continue; + } + + if (arpVersion < minVersion) + { + minVersion = arpVersion; + } + else if (arpVersion > maxVersion) + { + maxVersion = arpVersion; + } + } + } + } + } + + return arpVersionFound ? Utility::VersionRange{ minVersion, maxVersion } : Utility::VersionRange{}; + } } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp @@ -448,6 +448,18 @@ namespace AppInstaller::Manifest ); } + bool DoesInstallerTypeSupportArpVersionRange(InstallerTypeEnum installerType) + { + return ( + installerType == InstallerTypeEnum::Exe || + installerType == InstallerTypeEnum::Inno || + installerType == InstallerTypeEnum::Msi || + installerType == InstallerTypeEnum::Nullsoft || + installerType == InstallerTypeEnum::Wix || + installerType == InstallerTypeEnum::Burn + ); + } + bool IsInstallerTypeCompatible(InstallerTypeEnum type1, InstallerTypeEnum type2) { // Unknown type cannot be compatible with any other diff --git a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp @@ -20,7 +20,11 @@ namespace AppInstaller::Manifest try { // Version value should be successfully parsed - Utility::Version test{ manifest.Version }; + Utility::Version testVersion{ manifest.Version }; + if (testVersion.IsApproximate()) + { + resultErrors.emplace_back(ManifestError::ApproximateVersionNotAllowed, "PackageVersion", manifest.Version); + } } catch (const std::exception&) { @@ -198,6 +202,26 @@ namespace AppInstaller::Manifest break; } } + + // Check no approximate version declared for DisplayVersion in AppsAndFeatureEntries + for (auto const& entry : installer.AppsAndFeaturesEntries) + { + if (!entry.DisplayVersion.empty()) + { + try + { + Utility::Version displayVersion{ entry.DisplayVersion }; + if (displayVersion.IsApproximate()) + { + resultErrors.emplace_back(ManifestError::ApproximateVersionNotAllowed, "DisplayVersion", entry.DisplayVersion); + } + } + catch (const std::exception&) + { + resultErrors.emplace_back(ManifestError::InvalidFieldValue, "DisplayVersion", entry.DisplayVersion); + } + } + } } // Validate localizations diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -99,7 +99,8 @@ #define APPINSTALLER_CLI_ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS ((HRESULT)0x8A150054) #define APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY ((HRESULT)0x8A150055) #define APPINSTALLER_CLI_ERROR_INSTALLER_PROHIBITS_ELEVATION ((HRESULT)0x8A150056) -#define APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED ((HRESULT)0x8A150057) +#define APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED ((HRESULT)0x8A150057) +#define APPINSTALLER_CLI_ERROR_ARP_VERSION_VALIDATION_FAILED ((HRESULT)0x8A150058) // Install errors. #define APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE ((HRESULT)0x8A150101) diff --git a/src/AppInstallerCommonCore/Public/AppInstallerVersions.h b/src/AppInstallerCommonCore/Public/AppInstallerVersions.h @@ -11,9 +11,10 @@ namespace AppInstaller::Utility // Creates a comparable version object from a string. // Versions are parsed by: - // 1. Splitting the string based on the given splitChars (or DefaultSplitChars) - // 2. Parsing a leading, positive integer from each split part - // 3. Saving any remaining, non-digits as a supplemental value + // 1. Parse approximate comparator sign if applicable + // 2. Splitting the string based on the given splitChars (or DefaultSplitChars) + // 3. Parsing a leading, positive integer from each split part + // 4. Saving any remaining, non-digits as a supplemental value // // Versions are compared by: // for each part in each version @@ -22,8 +23,20 @@ namespace AppInstaller::Utility // else if integers not equal, return comparison of integers // else if only one side has a non-empty string part, it is less // else if string parts not equal, return comparison of strings + // if all parts are same, use approximate comparator if applicable + // + // Note: approximate to another approximate version is invalid. + // approximate to Unknown is invalid. struct Version { + // Used in approximate version to indicate the relation to the base version. + enum class ApproximateComparator + { + None, + LessThan, + GreaterThan, + }; + // The default characters to split a version string on. constexpr static std::string_view DefaultSplitChars = "."sv; @@ -33,6 +46,9 @@ namespace AppInstaller::Utility Version(std::string(version), splitChars) {} Version(std::string&& version, std::string_view splitChars = DefaultSplitChars); + // Constructing an approximate version from a base version. + Version(Version baseVersion, ApproximateComparator approximateComparator); + // Resets the version's value to the input. void Assign(std::string&& version, std::string_view splitChars = DefaultSplitChars); @@ -75,9 +91,48 @@ namespace AppInstaller::Utility // Gets the part breakdown for a given version; used for tests. const std::vector<Part>& GetParts() const { return m_parts; } + // Returns if the version is an approximate version. + bool IsApproximate() const { return m_approximateComparator != ApproximateComparator::None; } + protected: + + bool IsBaseVersionLatest() const; + bool IsBaseVersionUnknown() const; + // Called by overloaded less than operator implementation when base version already compared and equal, less than determined by approximate comparator. + bool ApproximateCompareLessThan(const Version& other) const; + std::string m_version; std::vector<Part> m_parts; + ApproximateComparator m_approximateComparator = ApproximateComparator::None; + }; + + // Version range represented by a min version and max version, both inclusive. + struct VersionRange + { + VersionRange() { m_isEmpty = true; }; + VersionRange(Version minVersion, Version maxVersion); + + bool IsEmpty() const { return m_isEmpty; } + + // Checks if version ranges overlap. Empty version range does not overlap with any version range. + bool Overlaps(const VersionRange& other) const; + + // Checks if the version range is effectively the same as a single version. + bool IsSameAsSingleVersion(const Version& version) const; + + // Checks if a version is within the version range + bool ContainsVersion(const Version& version) const; + + // < operator will thow if compared with an empty range or an overlapped range + bool operator<(const VersionRange& other) const; + + const Version& GetMinVersion() const; + const Version& GetMaxVersion() const; + + private: + Version m_minVersion; + Version m_maxVersion; + bool m_isEmpty = false; }; // A channel string; existing solely to give a type. @@ -126,4 +181,7 @@ namespace AppInstaller::Utility Version m_version; Channel m_channel; }; + + // Checks if there are overlaps within the list of version ranges + bool HasOverlapInVersionRanges(const std::vector<VersionRange>& ranges); } diff --git a/src/AppInstallerCommonCore/Public/winget/Manifest.h b/src/AppInstallerCommonCore/Public/winget/Manifest.h @@ -3,6 +3,7 @@ #pragma once #include <AppInstallerStrings.h> #include <AppInstallerSHA256.h> +#include <AppInstallerVersions.h> #include <winget/ManifestInstaller.h> #include <winget/ManifestLocalization.h> @@ -45,6 +46,9 @@ namespace AppInstaller::Manifest // Get all commands across installers std::vector<string_t> GetAggregatedCommands() const; + // Gets ARP version range if declared, otherwise an empty range is returned + Utility::VersionRange GetArpVersionRange() const; + // If not empty, the SHA256 hash of the manifest stream itself. Utility::SHA256::HashBuffer StreamSha256; }; diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h @@ -274,6 +274,9 @@ bool HasExtension(std::string_view extension) const; // Gets a value indicating whether the given installer type writes ARP entry. bool DoesInstallerTypeWriteAppsAndFeaturesEntry(InstallerTypeEnum installerType); + // Gets a value indicating whether the given installer type supports ARP version range. + bool DoesInstallerTypeSupportArpVersionRange(InstallerTypeEnum installerType); + // Checks whether 2 installer types are compatible. E.g. inno and exe are update compatible bool IsInstallerTypeCompatible(InstallerTypeEnum type1, InstallerTypeEnum type2); diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h @@ -51,6 +51,9 @@ 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 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."; } struct ValidationError diff --git a/src/AppInstallerCommonCore/Versions.cpp b/src/AppInstallerCommonCore/Versions.cpp @@ -11,22 +11,60 @@ namespace AppInstaller::Utility static constexpr std::string_view s_Version_Part_Latest = "Latest"sv; static constexpr std::string_view s_Version_Part_Unknown = "Unknown"sv; + static constexpr std::string_view s_Approximate_Less_Than = "< "sv; + static constexpr std::string_view s_Approximate_Greater_Than = "> "sv; + Version::Version(std::string&& version, std::string_view splitChars) { Assign(std::move(version), splitChars); } + Version::Version(Version baseVersion, ApproximateComparator approximateComparator) : Version(std::move(baseVersion)) + { + if (approximateComparator == ApproximateComparator::None) + { + return; + } + + THROW_HR_IF(E_INVALIDARG, this->IsApproximate() || this->IsUnknown()); + + m_approximateComparator = approximateComparator; + if (approximateComparator == ApproximateComparator::LessThan) + { + m_version = std::string{ s_Approximate_Less_Than } + m_version; + } + else if (approximateComparator == ApproximateComparator::GreaterThan) + { + m_version = std::string{ s_Approximate_Greater_Than } + m_version; + } + } + void Version::Assign(std::string&& version, std::string_view splitChars) { m_version = std::move(version); + + // Process approximate comparator if applicable + std::string baseVersion = m_version; + if (CaseInsensitiveStartsWith(m_version, s_Approximate_Less_Than)) + { + m_approximateComparator = ApproximateComparator::LessThan; + baseVersion = m_version.substr(s_Approximate_Less_Than.length(), m_version.length() - s_Approximate_Less_Than.length()); + } + else if (CaseInsensitiveStartsWith(m_version, s_Approximate_Greater_Than)) + { + m_approximateComparator = ApproximateComparator::GreaterThan; + baseVersion = m_version.substr(s_Approximate_Greater_Than.length(), m_version.length() - s_Approximate_Greater_Than.length()); + } + + // Then parse the base version size_t pos = 0; - while (pos < m_version.length()) + while (pos < baseVersion.length()) { - size_t newPos = m_version.find_first_of(splitChars, pos); + size_t newPos = baseVersion.find_first_of(splitChars, pos); - size_t length = (newPos == std::string::npos ? m_version.length() : newPos) - pos; - m_parts.emplace_back(m_version.substr(pos, length)); + size_t length = (newPos == std::string::npos ? baseVersion.length() : newPos) - pos; + m_parts.emplace_back(baseVersion.substr(pos, length)); pos += length + 1; } @@ -44,25 +82,36 @@ namespace AppInstaller::Utility break; } } + + THROW_HR_IF(E_INVALIDARG, m_approximateComparator != ApproximateComparator::None && IsBaseVersionUnknown()); } bool Version::operator<(const Version& other) const { // Sort Latest higher than any other values - bool thisIsLatest = IsLatest(); - bool otherIsLatest = other.IsLatest(); + bool thisIsLatest = IsBaseVersionLatest(); + bool otherIsLatest = other.IsBaseVersionLatest(); - if (thisIsLatest || otherIsLatest) + if (thisIsLatest && otherIsLatest) { - // If at least one is latest, this can only be less than if the other is and this is not. + return ApproximateCompareLessThan(other); + } + else if (thisIsLatest || otherIsLatest) + { + // If only one is latest, this can only be less than if the other is and this is not. return (otherIsLatest && !thisIsLatest); } // Sort Unknown lower than any known values - bool thisIsUnknown = IsUnknown(); - bool otherIsUnknown = other.IsUnknown(); + bool thisIsUnknown = IsBaseVersionUnknown(); + bool otherIsUnknown = other.IsBaseVersionUnknown(); - if (thisIsUnknown || otherIsUnknown) + if (thisIsUnknown && otherIsUnknown) + { + // This code path should always return false as we disable approximate version for Unknown for now + return ApproximateCompareLessThan(other); + } + else if (thisIsUnknown || otherIsUnknown) { // If at least one is unknown, this can only be less than if it is and the other is not. return (thisIsUnknown && !otherIsUnknown); @@ -90,8 +139,17 @@ namespace AppInstaller::Utility // else parts are equal, so continue to next part } - // All parts tested were equal, so this is only less if there are more parts in other. - return m_parts.size() < other.m_parts.size(); + // All parts tested were equal + if (m_parts.size() == other.m_parts.size()) + { + return ApproximateCompareLessThan(other); + } + else + { + // Else this is only less if there are more parts in other. + return m_parts.size() < other.m_parts.size(); + } + } bool Version::operator>(const Version& other) const @@ -111,8 +169,13 @@ namespace AppInstaller::Utility bool Version::operator==(const Version& other) const { - if ((IsLatest() && other.IsLatest()) || - (IsUnknown() && other.IsUnknown())) + if (m_approximateComparator != other.m_approximateComparator) + { + return false; + } + + if ((IsBaseVersionLatest() && other.IsBaseVersionLatest()) || + (IsBaseVersionUnknown() && other.IsBaseVersionUnknown())) { return true; } @@ -140,7 +203,7 @@ namespace AppInstaller::Utility bool Version::IsLatest() const { - return (m_parts.size() == 1 && m_parts[0].Integer == 0 && Utility::CaseInsensitiveEquals(m_parts[0].Other, s_Version_Part_Latest)); + return (m_approximateComparator != ApproximateComparator::LessThan && IsBaseVersionLatest()); } Version Version::CreateLatest() @@ -153,7 +216,7 @@ namespace AppInstaller::Utility bool Version::IsUnknown() const { - return (m_parts.size() == 1 && m_parts[0].Integer == 0 && Utility::CaseInsensitiveEquals(m_parts[0].Other, s_Version_Part_Unknown)); + return IsBaseVersionUnknown(); } Version Version::CreateUnknown() @@ -164,6 +227,23 @@ namespace AppInstaller::Utility return result; } + bool Version::IsBaseVersionLatest() const + { + return (m_parts.size() == 1 && m_parts[0].Integer == 0 && Utility::CaseInsensitiveEquals(m_parts[0].Other, s_Version_Part_Latest)); + } + + bool Version::IsBaseVersionUnknown() const + { + return (m_parts.size() == 1 && m_parts[0].Integer == 0 && Utility::CaseInsensitiveEquals(m_parts[0].Other, s_Version_Part_Unknown)); + } + + bool Version::ApproximateCompareLessThan(const Version& other) const + { + // Only true if this is less than, other is not, OR this is none, other is greater than + return (m_approximateComparator == ApproximateComparator::LessThan && other.m_approximateComparator != ApproximateComparator::LessThan) || + (m_approximateComparator == ApproximateComparator::None && other.m_approximateComparator == ApproximateComparator::GreaterThan); + } + Version::Part::Part(const std::string& part) { const char* begin = part.c_str(); @@ -275,4 +355,77 @@ namespace AppInstaller::Utility return m_version < other.m_version; } + + VersionRange::VersionRange(Version minVersion, Version maxVersion) + { + THROW_HR_IF(E_INVALIDARG, minVersion > maxVersion); + m_minVersion = std::move(minVersion); + m_maxVersion = std::move(maxVersion); + } + + bool VersionRange::Overlaps(const VersionRange& other) const + { + // No overlap if either is an empty range. + if (IsEmpty() || other.IsEmpty()) + { + return false; + } + + return m_minVersion <= other.m_maxVersion && m_maxVersion >= other.m_minVersion; + } + + bool VersionRange::IsSameAsSingleVersion(const Version& version) const + { + if (IsEmpty()) + { + return false; + } + + return m_minVersion == version && m_maxVersion == version; + } + + bool VersionRange::ContainsVersion(const Version& version) const + { + if (IsEmpty()) + { + return false; + } + + return version >= m_minVersion && version <= m_maxVersion; + } + + bool VersionRange::operator<(const VersionRange& other) const + { + THROW_HR_IF(E_INVALIDARG, IsEmpty() || other.IsEmpty() || Overlaps(other)); + + return m_minVersion < other.m_minVersion; + } + + const Version& VersionRange::GetMinVersion() const + { + THROW_HR_IF(E_NOT_VALID_STATE, IsEmpty()); + return m_minVersion; + } + + const Version& VersionRange::GetMaxVersion() const + { + THROW_HR_IF(E_NOT_VALID_STATE, IsEmpty()); + return m_maxVersion; + } + + bool HasOverlapInVersionRanges(const std::vector<VersionRange>& ranges) + { + for (size_t i = 0; i < ranges.size(); i++) + { + for (size_t j = i + 1; j < ranges.size(); j++) + { + if (ranges[i].Overlaps(ranges[j])) + { + return true; + } + } + } + + return false; + } } diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -230,6 +230,7 @@ </ClCompile> </ItemDefinitionGroup> <ItemGroup> + <ClInclude Include="ArpVersionValidation.h" /> <ClInclude Include="CompositeSource.h" /> <ClInclude Include="ICU\SQLiteICU.h" /> <ClInclude Include="ISource.h" /> @@ -250,6 +251,7 @@ <ClInclude Include="Microsoft\Schema\1_0\SearchResultsTable.h" /> <ClInclude Include="Microsoft\Schema\1_0\TagsTable.h" /> <ClInclude Include="Microsoft\Schema\1_0\VersionTable.h" /> + <ClInclude Include="Microsoft\Schema\1_0\VirtualTableBase.h" /> <ClInclude Include="Microsoft\Schema\1_1\Interface.h" /> <ClInclude Include="Microsoft\Schema\1_1\ManifestMetadataTable.h" /> <ClInclude Include="Microsoft\Schema\1_1\PackageFamilyNameTable.h" /> @@ -263,6 +265,8 @@ <ClInclude Include="Microsoft\Schema\1_3\Interface.h" /> <ClInclude Include="Microsoft\Schema\1_4\DependenciesTable.h" /> <ClInclude Include="Microsoft\Schema\1_4\Interface.h" /> + <ClInclude Include="Microsoft\Schema\1_5\ArpVersionVirtualTable.h" /> + <ClInclude Include="Microsoft\Schema\1_5\Interface.h" /> <ClInclude Include="Microsoft\Schema\ISQLiteIndex.h" /> <ClInclude Include="Microsoft\Schema\MetadataTable.h" /> <ClInclude Include="Microsoft\Schema\Version.h" /> @@ -303,6 +307,7 @@ <ItemGroup> <ClCompile Include="ARPCorrelation.cpp" /> <ClCompile Include="ARPCorrelationAlgorithms.cpp" /> + <ClCompile Include="ArpVersionValidation.cpp" /> <ClCompile Include="CompositeSource.cpp" /> <ClCompile Include="ICU\SQLiteICU.c"> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader> @@ -333,6 +338,7 @@ <ClCompile Include="Microsoft\Schema\1_3\Interface_1_3.cpp" /> <ClCompile Include="Microsoft\Schema\1_4\DependenciesTable.cpp" /> <ClCompile Include="Microsoft\Schema\1_4\Interface_1_4.cpp" /> + <ClCompile Include="Microsoft\Schema\1_5\Interface_1_5.cpp" /> <ClCompile Include="Microsoft\Schema\MetadataTable.cpp" /> <ClCompile Include="Microsoft\Schema\Version.cpp" /> <ClCompile Include="Microsoft\SQLiteIndex.cpp" /> diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -61,6 +61,9 @@ <Filter Include="Microsoft\Schema\1_4"> <UniqueIdentifier>{dcae9c55-cdd7-4381-8acd-3554896608a5}</UniqueIdentifier> </Filter> + <Filter Include="Microsoft\Schema\1_5"> + <UniqueIdentifier>{e31c8e5b-ed2c-43c8-b91b-db8ec4c52f71}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <ClInclude Include="pch.h"> @@ -270,6 +273,18 @@ <ClInclude Include="Public\winget\ARPCorrelationAlgorithms.h"> <Filter>Header Files</Filter> </ClInclude> + <ClInclude Include="Microsoft\Schema\1_5\ArpVersionVirtualTable.h"> + <Filter>Microsoft\Schema\1_5</Filter> + </ClInclude> + <ClInclude Include="Microsoft\Schema\1_5\Interface.h"> + <Filter>Microsoft\Schema\1_5</Filter> + </ClInclude> + <ClInclude Include="ArpVersionValidation.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="Microsoft\Schema\1_0\VirtualTableBase.h"> + <Filter>Microsoft\Schema\1_0</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -425,6 +440,12 @@ <ClCompile Include="ARPCorrelationAlgorithms.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="Microsoft\Schema\1_5\Interface_1_5.cpp"> + <Filter>Microsoft\Schema\1_5</Filter> + </ClCompile> + <ClCompile Include="ArpVersionValidation.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerRepositoryCore/ArpVersionValidation.cpp b/src/AppInstallerRepositoryCore/ArpVersionValidation.cpp @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" +#include "ArpVersionValidation.h" +#include <winget/ManifestValidation.h> + +namespace AppInstaller::Repository +{ + namespace + { + std::vector<Utility::VersionRange> GetArpVersionRangesByPackageRowId(const Microsoft::SQLiteIndex* index, Microsoft::SQLiteIndex::IdType packageRowId, const Utility::VersionAndChannel& excludeVersionAndChannel = {}) + { + std::vector<Utility::VersionRange> result; + + auto versionKeys = index->GetVersionKeysById(packageRowId); + for (auto const& versionKey : versionKeys) + { + // For manifest update, the manifest to be updated does not need to be checked. + // In unlikely cases if both version 1.0.0 and 1.0 of the same package exist, we compare raw values here as what sqlite index does. + if (versionKey.GetVersion().ToString() == excludeVersionAndChannel.GetVersion().ToString() && + versionKey.GetChannel().ToString() == excludeVersionAndChannel.GetChannel().ToString()) + { + continue; + } + + std::optional<Microsoft::SQLiteIndex::IdType> manifestRowId = index->GetManifestIdByKey(packageRowId, versionKey.GetVersion().ToString(), versionKey.GetChannel().ToString()); + if (manifestRowId) + { + auto arpMinVersion = index->GetPropertyByManifestId(manifestRowId.value(), PackageVersionProperty::ArpMinVersion).value_or(""); + auto arpMaxVersion = index->GetPropertyByManifestId(manifestRowId.value(), PackageVersionProperty::ArpMaxVersion).value_or(""); + + // Either both empty or both not empty + THROW_HR_IF(E_UNEXPECTED, arpMinVersion.empty() != arpMaxVersion.empty()); + + if (!arpMinVersion.empty() && !arpMaxVersion.empty()) + { + result.emplace_back(Utility::VersionRange{ Utility::Version{ std::move(arpMinVersion) }, Utility::Version{ std::move(arpMaxVersion) } }); + } + } + } + + return result; + } + } + + void ValidateManifestArpVersion(const Microsoft::SQLiteIndex* index, const Manifest::Manifest& manifest) + { + try + { + auto manifestArpVersionRange = manifest.GetArpVersionRange(); + if (manifestArpVersionRange.IsEmpty()) + { + return; + } + + SearchRequest request; + request.Filters.emplace_back(PackageMatchField::Id, MatchType::CaseInsensitive, manifest.Id); + auto searchResult = index->Search(request); + if (searchResult.Matches.empty()) + { + return; + } + + auto arpVersionRangesInIndex = GetArpVersionRangesByPackageRowId(index, searchResult.Matches[0].first, { Utility::Version{ manifest.Version }, Utility::Channel{ manifest.Channel } }); + for (auto const& arpInIndex : arpVersionRangesInIndex) + { + if (manifestArpVersionRange.Overlaps(arpInIndex)) + { + std::string errorMsg = Manifest::ManifestError::ArpVersionOverlapWithIndex; + errorMsg.append("[" + arpInIndex.GetMinVersion().ToString() + ", " + arpInIndex.GetMaxVersion().ToString() + "]"); + AICLI_LOG(Repo, Error, << errorMsg); + THROW_EXCEPTION(Manifest::ManifestException( + { Manifest::ValidationError(errorMsg) }, + APPINSTALLER_CLI_ERROR_DEPENDENCIES_VALIDATION_FAILED)); + } + } + } + catch (const Manifest::ManifestException&) + { + // Prevent ManifestException from being wrapped in another ManifestException + throw; + } + catch (...) + { + AICLI_LOG(Repo, Error, << "ValidateManifestArpVersion() encountered internal error."); + THROW_EXCEPTION(Manifest::ManifestException( + { Manifest::ValidationError(Manifest::ManifestError::ArpVersionValidationInternalError) }, + APPINSTALLER_CLI_ERROR_DEPENDENCIES_VALIDATION_FAILED)); + } + } +}+ \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/ArpVersionValidation.h b/src/AppInstallerRepositoryCore/ArpVersionValidation.h @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Microsoft/SQLiteIndex.h" +#include <winget/Manifest.h> + +namespace AppInstaller::Repository +{ + // Validate the manifest arp version range against index. Any validation failures will be thrown as ManifestException for better message back to caller. + void ValidateManifestArpVersion(const Microsoft::SQLiteIndex* index, const Manifest::Manifest& manifest); +} diff --git a/src/AppInstallerRepositoryCore/CompositeSource.cpp b/src/AppInstallerRepositoryCore/CompositeSource.cpp @@ -119,15 +119,144 @@ namespace AppInstaller::Repository return result; } - // A composite package installed version that allows us to override the source of the version. + // TODO: Note: Currently this function assumes the all versions in the available package is from one source. + // If one day we start adding support for available package from multiple sources, this function needs to be revisited. + std::string GetMappedInstalledVersion(const std::string& installedVersion, const std::shared_ptr<IPackage>& availablePackage) + { + // Stores raw versions value strings to run a preliminary check whether version mapping is needed. + std::vector<std::tuple<std::string, std::string, std::string>> rawVersionValues; + auto versionKeys = availablePackage->GetAvailableVersionKeys(); + bool shouldTryPerformMapping = false; + + for (auto const& versionKey : versionKeys) + { + auto availableVersion = availablePackage->GetAvailableVersion(versionKey); + std::string arpMinVersion = availableVersion->GetProperty(PackageVersionProperty::ArpMinVersion); + std::string arpMaxVersion = availableVersion->GetProperty(PackageVersionProperty::ArpMaxVersion); + + if (!arpMinVersion.empty() && !arpMaxVersion.empty()) + { + std::string manifestVersion = versionKey.Version; + + if (!shouldTryPerformMapping && (arpMinVersion != manifestVersion || arpMaxVersion != manifestVersion)) + { + shouldTryPerformMapping = true; + } + + rawVersionValues.emplace_back(std::make_tuple(std::move(manifestVersion), std::move(arpMinVersion), std::move(arpMaxVersion))); + } + } + + if (!shouldTryPerformMapping) + { + return installedVersion; + } + + // Construct a map between manifest version and arp version range. The map is ordered in descending by package version. + std::vector<std::pair<Utility::Version, Utility::VersionRange>> arpVersionMap; + + for (auto& tuple : rawVersionValues) + { + auto&& [manifestVersion, arpMinVersion, arpMaxVersion] = std::move(tuple); + Utility::VersionRange arpVersionRange{ Utility::Version(std::move(arpMinVersion)), Utility::Version(std::move(arpMaxVersion)) }; + arpVersionMap.emplace_back(std::make_pair(Utility::Version{ std::move(manifestVersion) }, std::move(arpVersionRange))); + } + + // Go through the arp version map and determine what mapping should be performed. + // shouldPerformMapping is true when at least 1 arp version range is different from the package version. + bool shouldPerformMapping = false; + bool isArpVersionRangeInDescendingOrder = true; + const Utility::VersionRange* previousVersionRange = nullptr; + + for (auto const& pair : arpVersionMap) + { + // If arp version range is not same as package version, should perform mapping + // This check is still needed to account for 1.0 == 1.0.0 cases + if (!shouldPerformMapping && !pair.second.IsSameAsSingleVersion(pair.first)) + { + shouldPerformMapping = true; + } + + if (!previousVersionRange) + { + // This is the first non empty arp version range + previousVersionRange = &pair.second; + } + else if (isArpVersionRangeInDescendingOrder) + { + // The arp version range should be less than previous range + if (pair.second < *previousVersionRange) + { + previousVersionRange = &pair.second; + } + else + { + isArpVersionRangeInDescendingOrder = false; + } + } + } + + // Now perform arp version mapping + if (shouldPerformMapping) + { + Utility::Version installed{ installedVersion }; + for (auto const& pair : arpVersionMap) + { + // If the installed version is in the arp version range + if (pair.second.ContainsVersion(installed)) + { + return pair.first.ToString(); + } + } + + // At this point, no mapping found. Perform approximate mapping if applicable. + // We'll start from end of the vector because we try to find closest less than version if possible. + if (isArpVersionRangeInDescendingOrder) + { + const Utility::Version* lastGreaterThanVersion = nullptr; + auto it = arpVersionMap.rbegin(); + while (it != arpVersionMap.rend()) + { + const auto& pair = *it; + if (installed < pair.second.GetMinVersion()) + { + return Utility::Version{ pair.first, Utility::Version::ApproximateComparator::LessThan }.ToString(); + } + else + { + lastGreaterThanVersion = &pair.first; + } + + it++; + } + + // No approximate less than version found, approximate greater than version will be returned. + if (lastGreaterThanVersion) + { + return Utility::Version{ *lastGreaterThanVersion, Utility::Version::ApproximateComparator::GreaterThan }.ToString(); + } + } + } + + // return the input installed version if no mapping is performed or found. + return installedVersion; + } + + // A composite package installed version that allows us to override the source or the version. struct CompositeInstalledVersion : public IPackageVersion { - CompositeInstalledVersion(std::shared_ptr<IPackageVersion> baseInstalledVersion, Source trackingSource) : - m_baseInstalledVersion(std::move(baseInstalledVersion)), m_trackingSource(std::move(trackingSource)) + CompositeInstalledVersion(std::shared_ptr<IPackageVersion> baseInstalledVersion, Source trackingSource, std::string overrideVersion = {}) : + m_baseInstalledVersion(std::move(baseInstalledVersion)), m_trackingSource(std::move(trackingSource)), m_overrideVersion(std::move(overrideVersion)) {} Utility::LocIndString GetProperty(PackageVersionProperty property) const override { + // If there is an override version, use it. + if (property == PackageVersionProperty::Version && !m_overrideVersion.empty()) + { + return Utility::LocIndString{ m_overrideVersion }; + } + return m_baseInstalledVersion->GetProperty(property); } @@ -160,6 +289,7 @@ namespace AppInstaller::Repository private: std::shared_ptr<IPackageVersion> m_baseInstalledVersion; Source m_trackingSource; + std::string m_overrideVersion; }; // A composite package for the CompositeSource. @@ -177,6 +307,8 @@ namespace AppInstaller::Repository m_installedChannel = installedVersion->GetProperty(PackageVersionProperty::Channel); } } + + TrySetOverrideInstalledVersion(); } Utility::LocIndString GetProperty(PackageProperty property) const override @@ -206,14 +338,7 @@ namespace AppInstaller::Repository { if (m_installedPackage) { - if (m_trackingSource) - { - return std::make_shared<CompositeInstalledVersion>(m_installedPackage->GetInstalledVersion(), m_trackingSource); - } - else - { - return m_installedPackage->GetInstalledVersion(); - } + return std::make_shared<CompositeInstalledVersion>(m_installedPackage->GetInstalledVersion(), m_trackingSource, m_overrideInstalledVersion); } return {}; @@ -300,6 +425,7 @@ namespace AppInstaller::Repository void SetAvailablePackage(std::shared_ptr<IPackage> availablePackage) { m_availablePackage = std::move(availablePackage); + TrySetOverrideInstalledVersion(); } void SetTracking(Source trackingSource, std::shared_ptr<IPackage> trackingPackage) @@ -309,11 +435,24 @@ namespace AppInstaller::Repository } private: + void TrySetOverrideInstalledVersion() + { + if (m_installedPackage && m_availablePackage) + { + auto installedType = Manifest::ConvertToInstallerTypeEnum(m_installedPackage->GetInstalledVersion()->GetMetadata()[PackageVersionMetadata::InstalledType]); + if (Manifest::DoesInstallerTypeSupportArpVersionRange(installedType)) + { + m_overrideInstalledVersion = GetMappedInstalledVersion(m_installedPackage->GetInstalledVersion()->GetProperty(PackageVersionProperty::Version), m_availablePackage); + } + } + } + std::shared_ptr<IPackage> m_installedPackage; Utility::LocIndString m_installedChannel; std::shared_ptr<IPackage> m_availablePackage; Source m_trackingSource; std::shared_ptr<IPackage> m_trackingPackage; + std::string m_overrideInstalledVersion; }; // The comparator compares the ResultMatch by MatchType first, then Field in a predefined order. diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "SQLiteIndex.h" #include "Schema/MetadataTable.h" +#include "ArpVersionValidation.h" #include <winget/ManifestYamlParser.h> namespace AppInstaller::Repository::Microsoft diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp @@ -49,7 +49,7 @@ namespace AppInstaller::Repository::Microsoft default: // Values coming from the index will always be localized/independent. std::optional<std::string> optValue = GetReferenceSource()->GetIndex().GetPropertyByManifestId(m_manifestId, property); - return LocIndString{ optValue ? optValue.value() :std::string{} }; + return LocIndString{ optValue ? optValue.value() : std::string{} }; } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.cpp @@ -83,17 +83,25 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 SQLite::Statement ManifestTableGetValuesById_Statement( const SQLite::Connection& connection, SQLite::rowid_t id, - std::initializer_list<SQLite::Builder::QualifiedColumn> columns) + std::initializer_list<SQLite::Builder::QualifiedColumn> columns, + std::initializer_list<std::string_view> manifestColumnNames) { + THROW_HR_IF(E_UNEXPECTED, manifestColumnNames.size() != columns.size()); + using QCol = SQLite::Builder::QualifiedColumn; SQLite::Builder::StatementBuilder builder; builder.Select(columns).From(s_ManifestTable_Table_Name); // join tables - for (const QCol& column : columns) + auto columnItr = columns.begin(); + auto manifestColumnNameItr = manifestColumnNames.begin(); + while (columnItr != columns.end()) { - builder.Join(column.Table).On(QCol{ s_ManifestTable_Table_Name, column.Column }, QCol{ column.Table, SQLite::RowIDName }); + builder.Join(columnItr->Table).On(QCol{ s_ManifestTable_Table_Name, *manifestColumnNameItr }, QCol{ columnItr->Table, SQLite::RowIDName }); + + columnItr++; + manifestColumnNameItr++; } builder.Where(QCol{ s_ManifestTable_Table_Name, SQLite::RowIDName }).Equals(id); @@ -261,7 +269,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 return builder.Prepare(connection); } - bool ManifestTableCheckConsistency(const SQLite::Connection& connection, const SQLite::Builder::QualifiedColumn& target, bool log) + bool ManifestTableCheckConsistency(const SQLite::Connection& connection, const SQLite::Builder::QualifiedColumn& target, std::string_view manifestColumnName, bool log) { using QCol = SQLite::Builder::QualifiedColumn; @@ -270,9 +278,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 // Select manifest.rowid, manifest.id, ids.id from manifest left outer join ids on manifest.id = ids.rowid where ids.id is NULL SQLite::Builder::StatementBuilder builder; builder. - Select({ QCol(s_ManifestTable_Table_Name, SQLite::RowIDName), QCol(s_ManifestTable_Table_Name, target.Column) }). + Select({ QCol(s_ManifestTable_Table_Name, SQLite::RowIDName), QCol(s_ManifestTable_Table_Name, manifestColumnName) }). From(s_ManifestTable_Table_Name). - LeftOuterJoin(target.Table).On(QCol(s_ManifestTable_Table_Name, target.Column), QCol(target.Table, SQLite::RowIDName)). + LeftOuterJoin(target.Table).On(QCol(s_ManifestTable_Table_Name, manifestColumnName), QCol(target.Table, SQLite::RowIDName)). Where(target).IsNull(); SQLite::Statement select = builder.Prepare(connection); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.h @@ -3,6 +3,7 @@ #pragma once #include "SQLiteWrapper.h" #include "SQLiteStatementBuilder.h" +#include "Microsoft/Schema/1_0/VirtualTableBase.h" #include <initializer_list> #include <optional> #include <string_view> @@ -13,6 +14,19 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 { namespace details { + template<typename Table> + std::string_view GetManifestTableColumnName() + { + if constexpr (std::is_base_of<VirtualTableBase, Table>()) + { + return Table::ManifestColumnName(); + } + else + { + return Table::ValueName(); + } + } + // Selects a manifest by the given value id. std::optional<SQLite::rowid_t> ManifestTableSelectByValueIds( const SQLite::Connection& connection, @@ -29,7 +43,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 SQLite::Statement ManifestTableGetValuesById_Statement( const SQLite::Connection& connection, SQLite::rowid_t id, - std::initializer_list<SQLite::Builder::QualifiedColumn> columns); + std::initializer_list<SQLite::Builder::QualifiedColumn> columns, + std::initializer_list<std::string_view> manifestColumnNames); // Gets all values for rows that match the given ids. SQLite::Statement ManifestTableGetAllValuesByIds_Statement( @@ -61,7 +76,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 // Checks the consistency of the index to ensure that every referenced row exists. // Returns true if index is consistent; false if it is not. - bool ManifestTableCheckConsistency(const SQLite::Connection& connection, const SQLite::Builder::QualifiedColumn& target, bool log); + bool ManifestTableCheckConsistency(const SQLite::Connection& connection, const SQLite::Builder::QualifiedColumn& target, std::string_view manifestColumnName, bool log); } // Info on the manifest columns. @@ -119,14 +134,14 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 template <typename... Tables> static auto GetIdsById(const SQLite::Connection& connection, SQLite::rowid_t id) { - return details::ManifestTableGetIdsById_Statement(connection, id, { Tables::ValueName()... }).GetRow<typename Tables::id_t...>(); + return details::ManifestTableGetIdsById_Statement(connection, id, { details::GetManifestTableColumnName<Tables>()...}).GetRow<typename Tables::id_t...>(); } // Gets the values requested for the manifest with the given rowid. template <typename... Tables> static auto GetValuesById(const SQLite::Connection& connection, SQLite::rowid_t id) { - return details::ManifestTableGetValuesById_Statement(connection, id, { SQLite::Builder::QualifiedColumn{ Tables::TableName(), Tables::ValueName() }... }).GetRow<typename Tables::value_t...>(); + return details::ManifestTableGetValuesById_Statement(connection, id, { SQLite::Builder::QualifiedColumn{ Tables::TableName(), Tables::ValueName() }... }, { details::GetManifestTableColumnName<Tables>()... }).GetRow<typename Tables::value_t...>(); } // Gets the values for rows that match the given ids. @@ -162,7 +177,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 template <typename Table> static void UpdateValueIdById(SQLite::Connection& connection, SQLite::rowid_t id, const typename Table::id_t& value) { - auto stmt = details::ManifestTableUpdateValueIdById_Statement(connection, Table::ValueName()); + auto stmt = details::ManifestTableUpdateValueIdById_Statement(connection, details::GetManifestTableColumnName<Table>()); stmt.Bind(1, value); stmt.Bind(2, id); stmt.Execute(); @@ -185,7 +200,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 template <typename Table> static bool CheckConsistency(const SQLite::Connection& connection, bool log) { - return details::ManifestTableCheckConsistency(connection, SQLite::Builder::QualifiedColumn{ Table::TableName(), Table::ValueName() }, log); + return details::ManifestTableCheckConsistency( + connection, SQLite::Builder::QualifiedColumn{ Table::TableName(), Table::ValueName() }, details::GetManifestTableColumnName<Table>(), log); } // Determines if the table is empty. diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/VirtualTableBase.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/VirtualTableBase.h @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once + +namespace AppInstaller::Repository::Microsoft::Schema::V1_0 +{ + // Since we have multiple manifest columns pointing to same table now(i.e. versions table), and previous assumption + // is manifest table column always has same name as referenced table column (i.e. manifest.version and versions.version), + // we need to differentiate manifest column name and referenced table column name(i.e. manifest.arp_min_version and versions.version) + // An optional ManifestColumnName() is added to virtual table for the above purpose, and can be used in the future if needed. + // To let the template codes better determine virtual tables, the following struct is created. + + // Struct used as the base for virtual tables. + // Future virtual tables reusing an existing table should derive from this and implement + // static std::string_view ManifestColumnName(); + // in addition to regular table info methods. + struct VirtualTableBase + { + }; +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_3/HashVirtualTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_3/HashVirtualTable.h @@ -22,7 +22,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_3 return "hash"sv; } - // The name of the column. + // The value type of the column. static constexpr SQLite::Builder::Type SQLiteType() { return SQLite::Builder::Type::Blob; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_4/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_4/Interface.h @@ -3,7 +3,6 @@ #pragma once #include "Microsoft/Schema/ISQLiteIndex.h" #include "Microsoft/Schema/1_3/Interface.h" -#include "Microsoft/Schema/1_2/Interface.h" namespace AppInstaller::Repository::Microsoft::Schema::V1_4 { @@ -24,6 +23,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_4 std::set<std::pair<SQLite::rowid_t, Utility::NormalizedString>> GetDependenciesByManifestRowId(const SQLite::Connection& connection, SQLite::rowid_t manifestRowId) const override; std::vector<std::pair<SQLite::rowid_t, Utility::NormalizedString>> GetDependentsById(const SQLite::Connection& connection, AppInstaller::Manifest::string_t packageId) const override; + protected: bool NotNeeded(const SQLite::Connection& connection, std::string_view tableName, std::string_view valueName, SQLite::rowid_t id) const override; }; } \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_4/Interface_1_4.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_4/Interface_1_4.cpp @@ -3,7 +3,6 @@ #include "pch.h" #include "Microsoft/Schema/1_4/Interface.h" #include "Microsoft/Schema/1_0/VersionTable.h" -#include <AppInstallerSHA256.h> #include "Microsoft/Schema/1_4/DependenciesTable.h" @@ -70,7 +69,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_4 DependenciesTable::RemoveDependencies(connection, manifestId); // Removes the manifest. - V1_2::Interface::RemoveManifestById(connection, manifestId); + V1_3::Interface::RemoveManifestById(connection, manifestId); // Remove the versions that are not needed. for (auto minVersion : minVersions) @@ -95,7 +94,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_4 { SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "prepareforpackaging_v1_4"); - V1_2::Interface::PrepareForPackaging(connection, false); + V1_3::Interface::PrepareForPackaging(connection, false); DependenciesTable::PrepareForPackaging(connection); @@ -113,7 +112,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_4 bool Interface::CheckConsistency(const SQLite::Connection& connection, bool log) const { - bool result = V1_2::Interface::CheckConsistency(connection, log); + bool result = V1_3::Interface::CheckConsistency(connection, log); // If the v1.3 index was consistent, or if full logging of inconsistency was requested, check the v1.4 data. if (result || log) diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_5/ArpVersionVirtualTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_5/ArpVersionVirtualTable.h @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "SQLiteStatementBuilder.h" +#include "Microsoft/Schema/1_0/VersionTable.h" +#include "Microsoft/Schema/1_0/VirtualTableBase.h" +#include <string_view> + +using namespace std::string_view_literals; + +namespace AppInstaller::Repository::Microsoft::Schema::V1_5 +{ + // A virtual table used to add Arp min version to ManifestTable, the values are stored in VersionTable. + struct ArpMinVersionVirtualTable : public V1_0::VirtualTableBase + { + // The id type + using id_t = V1_0::VersionTable::id_t; + + // The value type + using value_t = V1_0::VersionTable::value_t; + + // The name of the table. + static constexpr std::string_view TableName() + { + return V1_0::VersionTable::TableName(); + } + + // The value name of the column. + static constexpr std::string_view ValueName() + { + return V1_0::VersionTable::ValueName(); + } + + // The value name of the manifest table column. + static constexpr std::string_view ManifestColumnName() + { + return "arp_min_version"sv; + } + }; + + // A virtual table used to add Arp max version to ManifestTable, the values are stored in VersionTable. + struct ArpMaxVersionVirtualTable : public V1_0::VirtualTableBase + { + // The id type + using id_t = V1_0::VersionTable::id_t; + + // The value type + using value_t = V1_0::VersionTable::value_t; + + // The name of the table. + static constexpr std::string_view TableName() + { + return V1_0::VersionTable::TableName(); + } + + // The value name of the column. + static constexpr std::string_view ValueName() + { + return V1_0::VersionTable::ValueName(); + } + + // The value name of the manifest table column. + static constexpr std::string_view ManifestColumnName() + { + return "arp_max_version"sv; + } + }; +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_5/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_5/Interface.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Microsoft/Schema/ISQLiteIndex.h" +#include "Microsoft/Schema/1_4/Interface.h" + +namespace AppInstaller::Repository::Microsoft::Schema::V1_5 +{ + // Interface to this schema version exposed through ISQLiteIndex. + struct Interface : public V1_4::Interface + { + Interface(Utility::NormalizationVersion normVersion = Utility::NormalizationVersion::Initial); + + // Version 1.0 + Schema::Version GetVersion() const override; + void CreateTables(SQLite::Connection& connection, CreateOptions options) override; + SQLite::rowid_t AddManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::optional<std::filesystem::path>& relativePath) override; + std::pair<bool, SQLite::rowid_t> UpdateManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::optional<std::filesystem::path>& relativePath) override; + void RemoveManifestById(SQLite::Connection& connection, SQLite::rowid_t manifestId) override; + bool CheckConsistency(const SQLite::Connection& connection, bool log) const override; + + protected: + + bool NotNeeded(const SQLite::Connection& connection, std::string_view tableName, std::string_view valueName, SQLite::rowid_t id) const override; + + // Gets a property already knowing that the manifest id is valid. + std::optional<std::string> GetPropertyByManifestIdInternal(const SQLite::Connection& connection, SQLite::rowid_t manifestId, PackageVersionProperty property) const override; + + private: + // Semantic check to validate all arp version ranges within the index + bool ValidateArpVersionConsistency(const SQLite::Connection& connection) const; + }; +}+ \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_5/Interface_1_5.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_5/Interface_1_5.cpp @@ -0,0 +1,230 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Microsoft/Schema/1_5/Interface.h" +#include "Microsoft/Schema/1_5/ArpVersionVirtualTable.h" +#include "Microsoft/Schema/1_0/ManifestTable.h" +#include "Microsoft/Schema/1_0/VersionTable.h" + +namespace AppInstaller::Repository::Microsoft::Schema::V1_5 +{ + Interface::Interface(Utility::NormalizationVersion normVersion) : V1_4::Interface(normVersion) + { + } + + Schema::Version Interface::GetVersion() const + { + return { 1, 5 }; + } + + void Interface::CreateTables(SQLite::Connection& connection, CreateOptions options) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createtables_v1_5"); + + V1_4::Interface::CreateTables(connection, options); + + V1_0::ManifestTable::AddColumn(connection, { ArpMinVersionVirtualTable::ManifestColumnName(), SQLite::Builder::Type::Int64}); + V1_0::ManifestTable::AddColumn(connection, { ArpMaxVersionVirtualTable::ManifestColumnName(), SQLite::Builder::Type::Int64 }); + + savepoint.Commit(); + } + + SQLite::rowid_t Interface::AddManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::optional<std::filesystem::path>& relativePath) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "addmanifest_v1_5"); + + SQLite::rowid_t manifestId = V1_4::Interface::AddManifest(connection, manifest, relativePath); + + auto arpVersionRange = manifest.GetArpVersionRange(); + Manifest::string_t arpMinVersion = arpVersionRange.IsEmpty() ? "" : arpVersionRange.GetMinVersion().ToString(); + Manifest::string_t arpMaxVersion = arpVersionRange.IsEmpty() ? "" : arpVersionRange.GetMaxVersion().ToString(); + SQLite::rowid_t arpMinVersionId = V1_0::VersionTable::EnsureExists(connection, arpMinVersion); + SQLite::rowid_t arpMaxVersionId = V1_0::VersionTable::EnsureExists(connection, arpMaxVersion); + V1_0::ManifestTable::UpdateValueIdById<ArpMinVersionVirtualTable>(connection, manifestId, arpMinVersionId); + V1_0::ManifestTable::UpdateValueIdById<ArpMaxVersionVirtualTable>(connection, manifestId, arpMaxVersionId); + + savepoint.Commit(); + + return manifestId; + } + + std::pair<bool, SQLite::rowid_t> Interface::UpdateManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::optional<std::filesystem::path>& relativePath) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "updatemanifest_v1_5"); + + auto [indexModified, manifestId] = V1_4::Interface::UpdateManifest(connection, manifest, relativePath); + + auto [oldMinVersionId, oldMaxVersionId] = + V1_0::ManifestTable::GetIdsById<ArpMinVersionVirtualTable, ArpMaxVersionVirtualTable>(connection, manifestId); + + auto arpVersionRange = manifest.GetArpVersionRange(); + Manifest::string_t arpMinVersion = arpVersionRange.IsEmpty() ? "" : arpVersionRange.GetMinVersion().ToString(); + Manifest::string_t arpMaxVersion = arpVersionRange.IsEmpty() ? "" : arpVersionRange.GetMaxVersion().ToString(); + + SQLite::rowid_t arpMinVersionId = V1_0::VersionTable::EnsureExists(connection, arpMinVersion); + SQLite::rowid_t arpMaxVersionId = V1_0::VersionTable::EnsureExists(connection, arpMaxVersion); + + // For cleaning up the old entries after update if applicable + bool cleanOldMinVersionId = false; + bool cleanOldMaxVersionId = false; + + if (arpMinVersionId != oldMinVersionId) + { + V1_0::ManifestTable::UpdateValueIdById<ArpMinVersionVirtualTable>(connection, manifestId, arpMinVersionId); + cleanOldMinVersionId = true; + indexModified = true; + } + + if (arpMaxVersionId != oldMaxVersionId) + { + V1_0::ManifestTable::UpdateValueIdById<ArpMaxVersionVirtualTable>(connection, manifestId, arpMaxVersionId); + cleanOldMaxVersionId = true; + indexModified = true; + } + + if (cleanOldMinVersionId && NotNeeded(connection, V1_0::VersionTable::TableName(), V1_0::VersionTable::ValueName(), oldMinVersionId)) + { + V1_0::VersionTable::DeleteById(connection, oldMinVersionId); + } + + if (cleanOldMaxVersionId && oldMaxVersionId != oldMinVersionId && NotNeeded(connection, V1_0::VersionTable::TableName(), V1_0::VersionTable::ValueName(), oldMaxVersionId)) + { + V1_0::VersionTable::DeleteById(connection, oldMaxVersionId); + } + + savepoint.Commit(); + + return { indexModified, manifestId }; + } + + void Interface::RemoveManifestById(SQLite::Connection& connection, SQLite::rowid_t manifestId) + { + // Get the old arp version ids of the values from the manifest table + auto [arpMinVersionId, arpMaxVersionId] = + V1_0::ManifestTable::GetIdsById<ArpMinVersionVirtualTable, ArpMaxVersionVirtualTable>(connection, manifestId); + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "RemoveManifestById_v1_5"); + + // Removes the manifest. + V1_4::Interface::RemoveManifestById(connection, manifestId); + + // Remove the versions that are not needed. + if (NotNeeded(connection, V1_0::VersionTable::TableName(), V1_0::VersionTable::ValueName(), arpMinVersionId)) + { + V1_0::VersionTable::DeleteById(connection, arpMinVersionId); + } + + if (arpMaxVersionId != arpMinVersionId && NotNeeded(connection, V1_0::VersionTable::TableName(), V1_0::VersionTable::ValueName(), arpMaxVersionId)) + { + V1_0::VersionTable::DeleteById(connection, arpMaxVersionId); + } + + savepoint.Commit(); + } + + bool Interface::NotNeeded(const SQLite::Connection& connection, std::string_view tableName, std::string_view valueName, SQLite::rowid_t id) const + { + bool result = V1_4::Interface::NotNeeded(connection, tableName, valueName, id); + + if (result && tableName == V1_0::VersionTable::TableName()) + { + if (valueName != V1_0::VersionTable::ValueName()) + { + result = !V1_0::ManifestTable::IsValueReferenced(connection, V1_0::VersionTable::ValueName(), id) && result; + } + if (valueName != ArpMinVersionVirtualTable::ManifestColumnName()) + { + result = !V1_0::ManifestTable::IsValueReferenced(connection, ArpMinVersionVirtualTable::ManifestColumnName(), id) && result; + } + if (valueName != ArpMaxVersionVirtualTable::ManifestColumnName()) + { + result = !V1_0::ManifestTable::IsValueReferenced(connection, ArpMaxVersionVirtualTable::ManifestColumnName(), id) && result; + } + } + + return result; + } + + bool Interface::CheckConsistency(const SQLite::Connection& connection, bool log) const + { + bool result = V1_4::Interface::CheckConsistency(connection, log); + + // If the v1.4 index was consistent, or if full logging of inconsistency was requested, check the v1.5 data. + if (result || log) + { + result = V1_0::ManifestTable::CheckConsistency<ArpMinVersionVirtualTable>(connection, log) && result; + } + + if (result || log) + { + result = V1_0::ManifestTable::CheckConsistency<ArpMaxVersionVirtualTable>(connection, log) && result; + } + + if (result || log) + { + result = ValidateArpVersionConsistency(connection) && result; + } + + return result; + } + + std::optional<std::string> Interface::GetPropertyByManifestIdInternal(const SQLite::Connection& connection, SQLite::rowid_t manifestId, PackageVersionProperty property) const + { + switch (property) + { + case AppInstaller::Repository::PackageVersionProperty::ArpMinVersion: + return std::get<0>(V1_0::ManifestTable::GetValuesById<ArpMinVersionVirtualTable>(connection, manifestId)); + case AppInstaller::Repository::PackageVersionProperty::ArpMaxVersion: + return std::get<0>(V1_0::ManifestTable::GetValuesById<ArpMaxVersionVirtualTable>(connection, manifestId)); + default: + return V1_4::Interface::GetPropertyByManifestIdInternal(connection, manifestId, property); + } + } + + bool Interface::ValidateArpVersionConsistency(const SQLite::Connection& connection) const + { + try + { + // Search everything + SearchRequest request; + auto searchResult = Search(connection, request); + for (auto const& match : searchResult.Matches) + { + // Get arp version ranges for each package to check + std::vector<Utility::VersionRange> ranges; + auto versionKeys = GetVersionKeysById(connection, match.first); + for (auto const& versionKey : versionKeys) + { + auto manifestRowId = GetManifestIdByKey(connection, match.first, versionKey.GetVersion().ToString(), versionKey.GetChannel().ToString()); + if (manifestRowId) + { + auto arpMinVersion = GetPropertyByManifestId(connection, manifestRowId.value(), PackageVersionProperty::ArpMinVersion).value_or(""); + auto arpMaxVersion = GetPropertyByManifestId(connection, manifestRowId.value(), PackageVersionProperty::ArpMaxVersion).value_or(""); + + // Either both empty or both not empty + THROW_HR_IF(E_UNEXPECTED, arpMinVersion.empty() != arpMaxVersion.empty()); + + if (!arpMinVersion.empty() && !arpMaxVersion.empty()) + { + ranges.emplace_back(Utility::VersionRange{ Utility::Version{ std::move(arpMinVersion) }, Utility::Version{ std::move(arpMaxVersion) } }); + } + } + } + + // Check overlap + if (Utility::HasOverlapInVersionRanges(ranges)) + { + AICLI_LOG(Repo, Error, << "Overlapped Arp version ranges found for package. PackageRowId: " << match.first); + return false; + } + } + + return true; + } + catch (...) + { + AICLI_LOG(Repo, Error, << "ValidateArpVersionConsistency() encountered internal error. Returning false."); + return false; + } + } +}+ \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.cpp @@ -9,6 +9,7 @@ #include "1_2/Interface.h" #include "1_3/Interface.h" #include "1_4/Interface.h" +#include "1_5/Interface.h" namespace AppInstaller::Repository::Microsoft::Schema { @@ -49,11 +50,15 @@ namespace AppInstaller::Repository::Microsoft::Schema { return std::make_unique<V1_3::Interface>(); } - else if (*this == Version{ 1, 4 } || + else if (*this == Version{ 1, 4 }) + { + return std::make_unique<V1_4::Interface>(); + } + else if (*this == Version{ 1, 5 } || this->MajorVersion == 1 || this->IsLatest()) { - return std::make_unique<V1_4::Interface>(); + return std::make_unique<V1_5::Interface>(); } // We do not have the capacity to operate on this schema version diff --git a/src/AppInstallerRepositoryCore/PackageDependenciesValidation.cpp b/src/AppInstallerRepositoryCore/PackageDependenciesValidation.cpp @@ -2,20 +2,12 @@ // Licensed under the MIT License. #pragma once #include "pch.h" -#include <AppInstallerVersions.h> -#include <winget/Manifest.h> +#include "PackageDependenciesValidation.h" #include <winget/ManifestValidation.h> -#include <SQLiteWrapper.h> -#include <PackageDependenciesValidation.h> -#include <Microsoft/Schema/1_4/DependenciesTable.h> -#include "Microsoft/Schema/1_0/ManifestTable.h" #include <winget/DependenciesGraph.h> namespace AppInstaller::Repository { - using namespace Microsoft::Schema::V1_4; - using namespace Microsoft::Schema::V1_0; - namespace { struct DependentManifestInfo @@ -110,7 +102,7 @@ namespace AppInstaller::Repository } }; - bool PackageDependenciesValidation::ValidateManifestDependencies(SQLiteIndex* index, const Manifest::Manifest manifest) + bool PackageDependenciesValidation::ValidateManifestDependencies(SQLiteIndex* index, const Manifest::Manifest& manifest) { using namespace Manifest; @@ -177,7 +169,7 @@ namespace AppInstaller::Repository return true; } - bool PackageDependenciesValidation::VerifyDependenciesStructureForManifestDelete(SQLiteIndex* index, const Manifest::Manifest manifest) + bool PackageDependenciesValidation::VerifyDependenciesStructureForManifestDelete(SQLiteIndex* index, const Manifest::Manifest& manifest) { auto dependentsSet = index->GetDependentsById(manifest.Id); diff --git a/src/AppInstallerRepositoryCore/PackageDependenciesValidation.h b/src/AppInstallerRepositoryCore/PackageDependenciesValidation.h @@ -1,10 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once -#include <Microsoft/SQLiteIndex.h> -#include <AppInstallerVersions.h> +#include "Microsoft/SQLiteIndex.h" #include <winget/Manifest.h> -#include <SQLiteWrapper.h> namespace AppInstaller::Repository { @@ -13,8 +11,8 @@ namespace AppInstaller::Repository struct PackageDependenciesValidation { // Validate the dependencies of the given manifest. - static bool ValidateManifestDependencies(SQLiteIndex* index, const Manifest::Manifest manifest); + static bool ValidateManifestDependencies(SQLiteIndex* index, const Manifest::Manifest& manifest); - static bool VerifyDependenciesStructureForManifestDelete(SQLiteIndex* index, const Manifest::Manifest manifest); + static bool VerifyDependenciesStructureForManifestDelete(SQLiteIndex* index, const Manifest::Manifest& manifest); }; } diff --git a/src/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h b/src/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h @@ -135,6 +135,8 @@ namespace AppInstaller::Repository // Returned in hexadecimal format ManifestSHA256Hash, Publisher, + ArpMinVersion, + ArpMaxVersion, }; // A property of a package version that can have multiple values. diff --git a/src/AppInstallerRepositoryCore/Rest/RestSource.cpp b/src/AppInstallerRepositoryCore/Rest/RestSource.cpp @@ -203,8 +203,28 @@ namespace AppInstaller::Repository::Rest return Utility::LocIndString{ m_versionInfo.VersionAndChannel.GetChannel().ToString() }; case PackageVersionProperty::Publisher: return Utility::LocIndString{ m_package->PackageInfo().Publisher }; + case PackageVersionProperty::ArpMinVersion: + if (m_versionInfo.Manifest) + { + auto arpVersionRange = m_versionInfo.Manifest->GetArpVersionRange(); + return arpVersionRange.IsEmpty() ? Utility::LocIndString{} : Utility::LocIndString{ arpVersionRange.GetMinVersion().ToString() }; + } + else + { + return {}; + } + case PackageVersionProperty::ArpMaxVersion: + if (m_versionInfo.Manifest) + { + auto arpVersionRange = m_versionInfo.Manifest->GetArpVersionRange(); + return arpVersionRange.IsEmpty() ? Utility::LocIndString{} : Utility::LocIndString{ arpVersionRange.GetMaxVersion().ToString() }; + } + else + { + return {}; + } default: - return Utility::LocIndString{}; + return {}; } } diff --git a/src/AppInstallerTestExeInstaller/main.cpp b/src/AppInstallerTestExeInstaller/main.cpp @@ -11,9 +11,10 @@ using namespace std::filesystem; -std::wstring_view registrySubkey = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"; -std::wstring_view defaultProductID = L"{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}"; -std::wstring_view defaultVersion = L"1.0.0.0"; +std::wstring_view RegistrySubkey = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"; +std::wstring_view DefaultProductID = L"{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}"; +std::wstring_view DefaultDisplayName = L"AppInstallerTestExeInstaller"; +std::wstring_view DefaultDisplayVersion = L"1.0.0.0"; path GenerateUninstaller(std::wostream& out, const path& installDirectory, const std::wstring& productID) { @@ -26,14 +27,14 @@ path GenerateUninstaller(std::wostream& out, const path& installDirectory, const uninstallerOutputTextFilePath /= "TestExeUninstalled.txt"; std::wstring registryKey{ L"HKEY_CURRENT_USER\\" }; - registryKey += registrySubkey; + registryKey += RegistrySubkey; if (!productID.empty()) { registryKey += productID; } else { - registryKey += defaultProductID; + registryKey += DefaultProductID; } std::wofstream uninstallerScript(uninstallerPath); @@ -46,18 +47,17 @@ path GenerateUninstaller(std::wostream& out, const path& installDirectory, const return uninstallerPath; } -void WriteToUninstallRegistry(std::wostream& out, const std::wstring& productID, const path& uninstallerPath, const std::wstring& displayVersion) +void WriteToUninstallRegistry(std::wostream& out, const std::wstring& productID, const path& uninstallerPath, const std::wstring& displayName, const std::wstring& displayVersion) { HKEY hkey; LONG lReg; // String inputs to registry must be of wide char type - const wchar_t* displayName = L"AppInstallerTestExeInstaller"; const wchar_t* publisher = L"Microsoft Corporation"; std::wstring uninstallString = uninstallerPath.wstring(); DWORD version = 1; - std::wstring registryKey{ registrySubkey }; + std::wstring registryKey{ RegistrySubkey }; if (!productID.empty()) { @@ -66,7 +66,7 @@ void WriteToUninstallRegistry(std::wostream& out, const std::wstring& productID, } else { - registryKey += defaultProductID; + registryKey += DefaultProductID; out << "Default Product Code used: " << registryKey << std::endl; } @@ -86,7 +86,7 @@ void WriteToUninstallRegistry(std::wostream& out, const std::wstring& productID, out << "Successfully opened registry key" << std::endl; // Set Display Name Property Value - if (LONG res = RegSetValueEx(hkey, L"DisplayName", NULL, REG_SZ, (LPBYTE)displayName, (DWORD)(wcslen(displayName) + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) + if (LONG res = RegSetValueEx(hkey, L"DisplayName", NULL, REG_SZ, (LPBYTE)displayName.c_str(), (DWORD)(displayName.length() + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) { out << "Failed to write DisplayName value. Error Code: " << res << std::endl; } @@ -130,7 +130,8 @@ int wmain(int argc, const wchar_t** argv) path installDirectory = temp_directory_path(); std::wstringstream outContent; std::wstring productCode; - std::wstring version; + std::wstring displayName; + std::wstring displayVersion; int exitCode = 0; // Output to cout by default, but swap to a file if requested @@ -170,12 +171,21 @@ int wmain(int argc, const wchar_t** argv) } } + // Supports custom DisplayName + else if (_wcsicmp(argv[i], L"/DisplayName") == 0) + { + if (++i < argc) + { + displayName = argv[i]; + } + } + // Supports custom version else if (_wcsicmp(argv[i], L"/Version") == 0) { if (++i < argc) { - version = argv[i]; + displayVersion = argv[i]; } } @@ -190,9 +200,14 @@ int wmain(int argc, const wchar_t** argv) } } - if (version.empty()) + if (displayName.empty()) + { + displayName = DefaultDisplayName; + } + + if (displayVersion.empty()) { - version = defaultVersion; + displayVersion = DefaultDisplayVersion; } path outFilePath = installDirectory; @@ -205,7 +220,7 @@ int wmain(int argc, const wchar_t** argv) path uninstallerPath = GenerateUninstaller(*out, installDirectory, productCode); - WriteToUninstallRegistry(*out, productCode, uninstallerPath, version); + WriteToUninstallRegistry(*out, productCode, uninstallerPath, displayName, displayVersion); return exitCode; } diff --git a/src/WinGetUtil/Exports.cpp b/src/WinGetUtil/Exports.cpp @@ -10,8 +10,9 @@ #include <AppInstallerTelemetry.h> #include <Microsoft/SQLiteIndex.h> #include <winget/ManifestYamlParser.h> -#include <PackageDependenciesValidation.h> #include <winget/ThreadGlobals.h> +#include <PackageDependenciesValidation.h> +#include <ArpVersionValidation.h> using namespace AppInstaller::Utility; using namespace AppInstaller::Manifest; @@ -173,7 +174,8 @@ extern "C" THROW_HR_IF(E_INVALIDARG, !index); THROW_HR_IF(E_INVALIDARG, !succeeded); - bool result = reinterpret_cast<SQLiteIndex*>(index)->CheckConsistency(true); + auto sqliteIndex = reinterpret_cast<SQLiteIndex*>(index); + bool result = sqliteIndex->CheckConsistency(true); *succeeded = (result ? TRUE : FALSE); @@ -247,6 +249,136 @@ extern "C" } CATCH_RETURN() + WINGET_UTIL_API WinGetCreateManifest( + WINGET_STRING inputPath, + BOOL* succeeded, + WINGET_MANIFEST_HANDLE* manifest, + WINGET_STRING_OUT* message, + WINGET_STRING mergedManifestPath, + WinGetCreateManifestOption option) try + { + THROW_HR_IF(E_INVALIDARG, !inputPath); + THROW_HR_IF(E_INVALIDARG, !succeeded); + THROW_HR_IF(E_INVALIDARG, !!*manifest); + // ErrorOnVerifiedPublisherFields can only be used with SchemaAndSemanticValidation + THROW_HR_IF(E_INVALIDARG, (WI_IsFlagSet(option, WinGetCreateManifestOption::ReturnErrorOnVerifiedPublisherFields) && WI_IsFlagClear(option, WinGetCreateManifestOption::SchemaAndSemanticValidation))); + + *succeeded = false; + *manifest = nullptr; + + try + { + ManifestValidateOption validateOption; + + if (WI_IsFlagSet(option, WinGetCreateManifestOption::SchemaValidation) || WI_IsFlagSet(option, WinGetCreateManifestOption::SchemaAndSemanticValidation)) + { + validateOption.FullValidation = true; + validateOption.ThrowOnWarning = true; + validateOption.SchemaValidationOnly = WI_IsFlagClear(option, WinGetCreateManifestOption::SchemaAndSemanticValidation); + validateOption.ErrorOnVerifiedPublisherFields = WI_IsFlagSet(option, WinGetCreateManifestOption::ReturnErrorOnVerifiedPublisherFields); + } + + std::unique_ptr<Manifest> result = std::make_unique<Manifest>(YamlParser::CreateFromPath(inputPath, validateOption, mergedManifestPath ? mergedManifestPath : L"")); + + *manifest = static_cast<WINGET_MANIFEST_HANDLE>(result.release()); + } + catch (const ManifestException& e) + { + *succeeded = e.IsWarningOnly(); + if (message) + { + *message = ::SysAllocString(ConvertToUTF16(e.GetManifestErrorMessage()).c_str()); + } + } + + return S_OK; + } + CATCH_RETURN() + + WINGET_UTIL_API WinGetCloseManifest( + WINGET_MANIFEST_HANDLE manifest) try + { + THROW_HR_IF(E_INVALIDARG, !manifest); + + std::unique_ptr<Manifest> toClose{ reinterpret_cast<Manifest*>(manifest) }; + + return S_OK; + } + CATCH_RETURN() + + DEFINE_ENUM_FLAG_OPERATORS(WinGetValidateManifestResult); + + WINGET_UTIL_API WinGetValidateManifestV3( + WINGET_MANIFEST_HANDLE manifest, + WINGET_SQLITE_INDEX_HANDLE index, + WinGetValidateManifestResult* result, + WINGET_STRING_OUT* message, + WinGetValidateManifestOptionV2 option, + WinGetValidateManifestOperationType operationType) try + { + THROW_HR_IF(E_INVALIDARG, !manifest); + THROW_HR_IF(E_INVALIDARG, !result); + // Index should be provided if DependenciesValidation or ArpVersionValidation is to be performed + THROW_HR_IF(E_INVALIDARG, !index && (WI_IsFlagSet(option, WinGetValidateManifestOptionV2::DependenciesValidation) || WI_IsFlagSet(option, WinGetValidateManifestOptionV2::ArpVersionValidation))); + THROW_HR_IF(E_INVALIDARG, option == WinGetValidateManifestOptionV2::None); + + *result = WinGetValidateManifestResult::InternalError; + + std::string validationMessage; + auto validationResult = WinGetValidateManifestResult::Success; + + Manifest* manifestPtr = reinterpret_cast<Manifest*>(manifest); + SQLiteIndex* sqliteIndex = reinterpret_cast<SQLiteIndex*>(index); + + if (WI_IsFlagSet(option, WinGetValidateManifestOptionV2::DependenciesValidation)) + { + try + { + if (operationType == WinGetValidateManifestOperationType::OperationTypeDelete) + { + PackageDependenciesValidation::VerifyDependenciesStructureForManifestDelete(sqliteIndex, *manifestPtr); + } + else + { + PackageDependenciesValidation::ValidateManifestDependencies(sqliteIndex, *manifestPtr); + } + } + catch (const ManifestException& e) + { + WI_SetFlagIf(validationResult, WinGetValidateManifestResult::DependenciesValidationFailure, !e.IsWarningOnly()); + if (message) + { + validationMessage += e.GetManifestErrorMessage(); + } + } + } + + if (WI_IsFlagSet(option, WinGetValidateManifestOptionV2::ArpVersionValidation)) + { + try + { + ValidateManifestArpVersion(sqliteIndex, *manifestPtr); + } + catch (const ManifestException& e) + { + WI_SetFlagIf(validationResult, WinGetValidateManifestResult::ArpVersionValidationFailure, !e.IsWarningOnly()); + if (message) + { + validationMessage += e.GetManifestErrorMessage(); + } + } + } + + *result = validationResult; + if (message) + { + *message = ::SysAllocString(ConvertToUTF16(validationMessage).c_str()); + } + + return S_OK; + } + CATCH_RETURN() + WINGET_UTIL_API WinGetValidateManifestDependencies( WINGET_STRING inputPath, BOOL* succeeded, diff --git a/src/WinGetUtil/Source.def b/src/WinGetUtil/Source.def @@ -15,3 +15,6 @@ EXPORTS WinGetCompareVersions WinGetValidateManifestV2 WinGetValidateManifestDependencies + WinGetCreateManifest + WinGetCloseManifest + WinGetValidateManifestV3 diff --git a/src/WinGetUtil/WinGetUtil.h b/src/WinGetUtil/WinGetUtil.h @@ -7,6 +7,9 @@ extern "C" // A handle to the index. typedef void* WINGET_SQLITE_INDEX_HANDLE; + // A handle to the manifest. + typedef void* WINGET_MANIFEST_HANDLE; + // A string taken in by the utility; in UTF16. typedef wchar_t const* const WINGET_STRING; @@ -24,6 +27,53 @@ extern "C" ErrorOnVerifiedPublisherFields = 0x2, }; + enum WinGetCreateManifestOption + { + // Just create the manifest without any validation + NoValidation = 0, + // Only validate against json schema + SchemaValidation = 0x1, + // Validate against schema and also perform semantic validation + SchemaAndSemanticValidation = 0x2, + + /// Below options are additional validation behaviors if needed + + // Return error on manifest fields that require verified publishers, used during semantic validation + ReturnErrorOnVerifiedPublisherFields = 0x1000, + }; + + enum WinGetValidateManifestOptionV2 + { + // No validation, caller will get E_INVALIDARG + None = 0, + // Dependencies validation against index + DependenciesValidation = 0x1, + // Arp version validation against index + ArpVersionValidation = 0x2, + // Installer validation + InstallerValidation = 0x4, + }; + + enum WinGetValidateManifestOperationType + { + OperationTypeAdd = 0, + OperationTypeUpdate = 1, + OperationTypeDelete = 2, + }; + + enum WinGetValidateManifestResult + { + Success = 0, + + // Each validation step should have an enum for corresponding failure. + DependenciesValidationFailure = 0x1, + ArpVersionValidationFailure = 0x2, + InstallerValidationFailure = 0x4, + + // Internal error meaning validation does not complete as desired. + InternalError = 0x1000, + }; + enum WinGetValidateManifestDependenciesOption { DefaultValidation = 0, @@ -106,6 +156,34 @@ extern "C" WINGET_STRING mergedManifestPath, WinGetValidateManifestOption option); + // Creates a given manifest with optional validation. Returns a bool for operation result and + // a string representing validation errors if validation failed. + // If mergedManifestPath is provided, this method will write a merged manifest + // to the location specified by mergedManifestPath + WINGET_UTIL_API WinGetCreateManifest( + WINGET_STRING inputPath, + BOOL* succeeded, + WINGET_MANIFEST_HANDLE* manifest, + WINGET_STRING_OUT* message, + WINGET_STRING mergedManifestPath, + WinGetCreateManifestOption option); + + // Closes a given manifest. + WINGET_UTIL_API WinGetCloseManifest( + WINGET_MANIFEST_HANDLE manifest); + + // Validates a given manifest. Returns WinGetValidateManifestResult for validation result and + // a string representing validation errors if validation failed. + // If result is 0, it is success. Otherwise, caller can check the result with flags to see + // which phases failed. + WINGET_UTIL_API WinGetValidateManifestV3( + WINGET_MANIFEST_HANDLE manifest, + WINGET_SQLITE_INDEX_HANDLE index, + WinGetValidateManifestResult* result, + WINGET_STRING_OUT* message, + WinGetValidateManifestOptionV2 option, + WinGetValidateManifestOperationType operationType); + // Validates a given manifest with dependencies. Returns a bool for validation result and // a string representing validation errors if validation failed. // If mergedManifestPath is provided, this method will write a merged manifest