winget-cli

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

commit 0ebb5b9636dd4002d092bdbb7e00e164073e5c1a
parent 4950d3361b7ad32886ac158fb4c85641a55c231f
Author: JohnMcPMS <johnmcp@microsoft.com>
Date:   Fri,  5 Aug 2022 16:51:09 -0700

Collect scope in installer metadata (#2415)

Include the installed scope during metadata collection.  This uses the current detection method, which is "HKLM or HKCU?"

When there us a mismatch, the value will be explicitly set to `Unknown`.
Diffstat:
Msrc/AppInstallerCLITests/InstallerMetadataCollectionContext.cpp | 180++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/AppInstallerRepositoryCore/InstallerMetadataCollectionContext.cpp | 95++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/AppInstallerRepositoryCore/Public/winget/InstallerMetadataCollectionContext.h | 12++++++++++--
3 files changed, 269 insertions(+), 18 deletions(-)

diff --git a/src/AppInstallerCLITests/InstallerMetadataCollectionContext.cpp b/src/AppInstallerCLITests/InstallerMetadataCollectionContext.cpp @@ -75,7 +75,7 @@ namespace if (CurrentMetadata) { - json[L"currentMetadata"] = CurrentMetadata->ToJson(Utility::Version{ "1.0" }, 0); + json[L"currentMetadata"] = CurrentMetadata->ToJson(CurrentMetadata->SchemaVersion, 0); } if (SubmissionData) @@ -412,6 +412,7 @@ TEST_CASE("MetadataCollection_NewPackage", "[metadata_collection]") IPackageVersion::Metadata metadata; metadata[PackageVersionMetadata::InstalledType] = Manifest::InstallerTypeToString(Manifest::InstallerTypeEnum::Msi); + metadata[PackageVersionMetadata::InstalledScope] = Manifest::ScopeToString(Manifest::ScopeEnum::User); correlationData->CorrelateForNewlyInstalledResult.Package = std::make_shared<TestPackageVersion>(manifest, metadata); @@ -427,6 +428,7 @@ TEST_CASE("MetadataCollection_NewPackage", "[metadata_collection]") REQUIRE(output.Metadata->InstallerMetadataMap.count(input.InstallerHash.value()) == 1); const auto& entry = output.Metadata->InstallerMetadataMap[input.InstallerHash.value()]; REQUIRE(entry.SubmissionIdentifier == input.SubmissionIdentifier.value()); + REQUIRE(entry.Scope.empty()); REQUIRE(entry.AppsAndFeaturesEntries.size() == 1); REQUIRE(entry.AppsAndFeaturesEntries[0].DisplayName == manifest.DefaultLocalization.Get<Manifest::Localization::PackageName>()); REQUIRE(entry.AppsAndFeaturesEntries[0].Publisher == manifest.DefaultLocalization.Get<Manifest::Localization::Publisher>()); @@ -675,3 +677,179 @@ TEST_CASE("MetadataCollection_Merge_SameInstaller", "[metadata_collection]") REQUIRE(!item.second.AppsAndFeaturesEntries[0].ProductCode.empty()); } } + +TEST_CASE("MetadataCollection_NewPackage_1_1", "[metadata_collection]") +{ + TestInput input(MinimalDefaults); + input.SupportedMetadataVersion = "1.1"; + auto correlationData = std::make_unique<TestARPCorrelationData>(); + + Manifest::Manifest manifest; + manifest.DefaultLocalization.Add<Manifest::Localization::PackageName>("Test Package Name"); + manifest.DefaultLocalization.Add<Manifest::Localization::Publisher>("Test Publisher"); + manifest.Version = "1.2.3"; + manifest.Installers.push_back({}); + manifest.Installers[0].ProductCode = "{guid}"; + + IPackageVersion::Metadata metadata; + metadata[PackageVersionMetadata::InstalledType] = Manifest::InstallerTypeToString(Manifest::InstallerTypeEnum::Msi); + metadata[PackageVersionMetadata::InstalledScope] = Manifest::ScopeToString(Manifest::ScopeEnum::User); + + correlationData->CorrelateForNewlyInstalledResult.Package = std::make_shared<TestPackageVersion>(manifest, metadata); + + InstallerMetadataCollectionContext context = CreateTestContext(std::move(correlationData), input); + TestOutput output = GetOutput(context); + + REQUIRE(output.IsSuccess()); + output.ValidateFieldPresence(); + + REQUIRE(output.Metadata->ProductVersionMin.ToString() == output.Metadata->ProductVersionMax.ToString()); + REQUIRE(output.Metadata->ProductVersionMin.ToString() == manifest.Version); + REQUIRE(output.Metadata->InstallerMetadataMap.size() == 1); + REQUIRE(output.Metadata->InstallerMetadataMap.count(input.InstallerHash.value()) == 1); + const auto& entry = output.Metadata->InstallerMetadataMap[input.InstallerHash.value()]; + REQUIRE(entry.SubmissionIdentifier == input.SubmissionIdentifier.value()); + REQUIRE(entry.Scope == metadata[PackageVersionMetadata::InstalledScope]); + REQUIRE(entry.AppsAndFeaturesEntries.size() == 1); + REQUIRE(entry.AppsAndFeaturesEntries[0].DisplayName == manifest.DefaultLocalization.Get<Manifest::Localization::PackageName>()); + REQUIRE(entry.AppsAndFeaturesEntries[0].Publisher == manifest.DefaultLocalization.Get<Manifest::Localization::Publisher>()); + REQUIRE(entry.AppsAndFeaturesEntries[0].DisplayVersion == manifest.Version); + REQUIRE(entry.AppsAndFeaturesEntries[0].ProductCode == manifest.Installers[0].ProductCode); + REQUIRE(entry.AppsAndFeaturesEntries[0].InstallerType == Manifest::InstallerTypeEnum::Msi); + REQUIRE(output.Metadata->HistoricalMetadataList.empty()); +} + +TEST_CASE("MetadataCollection_NewPackage_NoScope", "[metadata_collection]") +{ + TestInput input(MinimalDefaults); + input.SupportedMetadataVersion = "1.1"; + auto correlationData = std::make_unique<TestARPCorrelationData>(); + + Manifest::Manifest manifest; + manifest.DefaultLocalization.Add<Manifest::Localization::PackageName>("Test Package Name"); + manifest.DefaultLocalization.Add<Manifest::Localization::Publisher>("Test Publisher"); + manifest.Version = "1.2.3"; + manifest.Installers.push_back({}); + manifest.Installers[0].ProductCode = "{guid}"; + + IPackageVersion::Metadata metadata; + metadata[PackageVersionMetadata::InstalledType] = Manifest::InstallerTypeToString(Manifest::InstallerTypeEnum::Msi); + + correlationData->CorrelateForNewlyInstalledResult.Package = std::make_shared<TestPackageVersion>(manifest, metadata); + + InstallerMetadataCollectionContext context = CreateTestContext(std::move(correlationData), input); + TestOutput output = GetOutput(context); + + REQUIRE(output.IsSuccess()); + output.ValidateFieldPresence(); + + REQUIRE(output.Metadata->InstallerMetadataMap.size() == 1); + REQUIRE(output.Metadata->InstallerMetadataMap.count(input.InstallerHash.value()) == 1); + const auto& entry = output.Metadata->InstallerMetadataMap[input.InstallerHash.value()]; + REQUIRE(entry.Scope.empty()); +} + +TEST_CASE("MetadataCollection_SameSubmission_SameInstaller_Scopes", "[metadata_collection]") +{ + std::string version = "1.3.5"; + std::string productCode = "{guid}"; + Manifest::InstallerTypeEnum installerType = Manifest::InstallerTypeEnum::Msi; + std::string currentScope = GENERATE(std::string{}, + Manifest::ScopeToString(Manifest::ScopeEnum::Unknown), + Manifest::ScopeToString(Manifest::ScopeEnum::User), + Manifest::ScopeToString(Manifest::ScopeEnum::Machine)); + std::string newScope{ Manifest::ScopeToString(Manifest::ScopeEnum::User) }; + + INFO(currentScope); + + TestInput input(MinimalDefaults, version, productCode, installerType); + input.SupportedMetadataVersion = "1.1"; + input.CurrentMetadata->SchemaVersion = { "1.1" }; + input.CurrentMetadata->InstallerMetadataMap.begin()->second.Scope = currentScope; + auto correlationData = std::make_unique<TestARPCorrelationData>(); + + Manifest::Manifest manifest; + manifest.DefaultLocalization.Add<Manifest::Localization::PackageName>("Different Language Name"); + // Same publisher + manifest.DefaultLocalization.Add<Manifest::Localization::Publisher>(input.CurrentMetadata->InstallerMetadataMap.begin()->second.AppsAndFeaturesEntries[0].Publisher); + manifest.Version = version; + manifest.Installers.push_back({}); + manifest.Installers[0].ProductCode = productCode; + + IPackageVersion::Metadata metadata; + metadata[PackageVersionMetadata::InstalledType] = Manifest::InstallerTypeToString(installerType); + metadata[PackageVersionMetadata::InstalledScope] = newScope; + + correlationData->CorrelateForNewlyInstalledResult.Package = std::make_shared<TestPackageVersion>(manifest, metadata); + + InstallerMetadataCollectionContext context = CreateTestContext(std::move(correlationData), input); + TestOutput output = GetOutput(context); + + REQUIRE(output.IsSuccess()); + output.ValidateFieldPresence(); + + REQUIRE(output.Metadata->InstallerMetadataMap.size() == 1); + REQUIRE(output.Metadata->InstallerMetadataMap.count(input.InstallerHash.value()) == 1); + const auto& entry = output.Metadata->InstallerMetadataMap[input.InstallerHash.value()]; + + if (currentScope.empty()) + { + REQUIRE(entry.Scope == newScope); + } + else if (currentScope != newScope) + { + // If Unknown, should stay Unknown + // If different, should become Unknown + REQUIRE(entry.Scope == Manifest::ScopeToString(Manifest::ScopeEnum::Unknown)); + } + else + { + // If same, should not change + REQUIRE(entry.Scope == currentScope); + } +} + +TEST_CASE("MetadataCollection_Merge_SameInstaller_Scopes", "[metadata_collection]") +{ + TestMerge mergeData{ MinimalDefaults }; + mergeData.Metadatas->emplace_back(MakeProductMetadata()); + + std::string currentScope = GENERATE(std::string{}, + Manifest::ScopeToString(Manifest::ScopeEnum::Unknown), + Manifest::ScopeToString(Manifest::ScopeEnum::User), + Manifest::ScopeToString(Manifest::ScopeEnum::Machine)); + std::string newScope{ Manifest::ScopeToString(Manifest::ScopeEnum::Machine) }; + + INFO(currentScope); + + mergeData.Metadatas->at(0).SchemaVersion = { "1.1" }; + mergeData.Metadatas->at(0).InstallerMetadataMap.begin()->second.Scope = currentScope; + mergeData.Metadatas->at(1).SchemaVersion = { "1.1" }; + mergeData.Metadatas->at(1).InstallerMetadataMap.begin()->second.Scope = newScope; + + std::wstring mergeResult = InstallerMetadataCollectionContext::Merge(mergeData.ToJSON(), 0, {}); + REQUIRE(!mergeResult.empty()); + + ProductMetadata mergeMetadata; + mergeMetadata.FromJson(web::json::value::parse(mergeResult)); + + REQUIRE(mergeMetadata.InstallerMetadataMap.size() == 1); + for (const auto& item : mergeMetadata.InstallerMetadataMap) + { + if (currentScope.empty()) + { + REQUIRE(item.second.Scope == newScope); + } + else if (currentScope != newScope) + { + // If Unknown, should stay Unknown + // If different, should become Unknown + REQUIRE(item.second.Scope == Manifest::ScopeToString(Manifest::ScopeEnum::Unknown)); + } + else + { + // If same, should not change + REQUIRE(item.second.Scope == currentScope); + } + } +} diff --git a/src/AppInstallerRepositoryCore/InstallerMetadataCollectionContext.cpp b/src/AppInstallerRepositoryCore/InstallerMetadataCollectionContext.cpp @@ -16,8 +16,20 @@ namespace AppInstaller::Repository::Metadata { namespace { - struct ProductMetadataFields_1_0 + struct ProductMetadataFields_1_N { + ProductMetadataFields_1_N(const Version& version) + { + if (::AppInstaller::Utility::Version{ "1.1" } <= version) + { + SchemaVersion = L"1.1"; + Scope = L"scope"; + } + } + + utility::string_t SchemaVersion = L"1.0"; + + // 1.0 utility::string_t ProductVersionMin = L"productVersionMin"; utility::string_t ProductVersionMax = L"productVersionMax"; utility::string_t Metadata = L"metadata"; @@ -40,6 +52,9 @@ namespace AppInstaller::Repository::Metadata utility::string_t Publishers = L"publishers"; utility::string_t ProductCodes = L"productCodes"; utility::string_t UpgradeCodes = L"upgradeCodes"; + + // 1.1 + utility::string_t Scope; }; struct OutputFields_1_0 @@ -140,6 +155,24 @@ namespace AppInstaller::Repository::Metadata entries.emplace_back(std::move(newEntry)); } } + + std::optional<std::string> GetStringFromFutureSchema(const web::json::value& value, const utility::string_t& field) + { + if (field.empty()) + { + return {}; + } + + return AppInstaller::JSON::GetRawStringValueFromJsonNode(value, field); + } + + void SetStringFromFutureSchema(web::json::value& json, const utility::string_t& field, std::string_view value) + { + if (!field.empty()) + { + json[field] = AppInstaller::JSON::GetStringValue(value); + } + } } void ProductMetadata::Clear() @@ -164,8 +197,7 @@ namespace AppInstaller::Repository::Metadata if (SchemaVersion.PartAt(0).Integer == 1) { - // We only have one version currently, so use that as long as the major version is 1 - FromJson_1_0(json); + FromJson_1_N(json); } else { @@ -182,19 +214,19 @@ namespace AppInstaller::Repository::Metadata web::json::value ProductMetadata::ToJson(const Utility::Version& schemaVersion, size_t maximumSizeInBytes) { - AICLI_LOG(Repo, Info, << "Creating metadata JSON version " << schemaVersion.ToString()); + SchemaVersion = schemaVersion; + AICLI_LOG(Repo, Info, << "Creating metadata JSON version " << SchemaVersion.ToString()); using ToJsonFunctionPointer = web::json::value(ProductMetadata::*)(); ToJsonFunctionPointer toJsonFunction = nullptr; - if (schemaVersion.PartAt(0).Integer == 1) + if (SchemaVersion.PartAt(0).Integer == 1) { - // We only have one version currently, so use that as long as the major version is 1 - toJsonFunction = &ProductMetadata::ToJson_1_0; + toJsonFunction = &ProductMetadata::ToJson_1_N; } else { - AICLI_LOG(Repo, Error, << "Don't know how to handle metadata version " << schemaVersion.ToString()); + AICLI_LOG(Repo, Error, << "Don't know how to handle metadata version " << SchemaVersion.ToString()); THROW_HR(HRESULT_FROM_WIN32(ERROR_UNSUPPORTED_TYPE)); } @@ -281,11 +313,11 @@ namespace AppInstaller::Repository::Metadata } } - void ProductMetadata::FromJson_1_0(const web::json::value& json) + void ProductMetadata::FromJson_1_N(const web::json::value& json) { - AICLI_LOG(Repo, Info, << "Parsing metadata JSON 1.0 fields"); + AICLI_LOG(Repo, Info, << "Parsing metadata JSON " << SchemaVersion.ToString() << " fields"); - ProductMetadataFields_1_0 fields; + ProductMetadataFields_1_N fields{ SchemaVersion }; auto productVersionMinString = AppInstaller::JSON::GetRawStringValueFromJsonNode(json, fields.ProductVersionMin); if (productVersionMinString) @@ -330,6 +362,8 @@ namespace AppInstaller::Repository::Metadata THROW_HR_IF(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE, !appsAndFeatures); installerMetadata.AppsAndFeaturesEntries = parser.DeserializeAppsAndFeaturesEntries(appsAndFeatures.value()); + installerMetadata.Scope = GetStringFromFutureSchema(item, fields.Scope).value_or(std::string{}); + InstallerMetadataMap[installerHashString] = std::move(installerMetadata); } } @@ -353,15 +387,15 @@ namespace AppInstaller::Repository::Metadata } } - web::json::value ProductMetadata::ToJson_1_0() + web::json::value ProductMetadata::ToJson_1_N() { - AICLI_LOG(Repo, Info, << "Creating metadata JSON 1.0 fields"); + AICLI_LOG(Repo, Info, << "Creating metadata JSON " << SchemaVersion.ToString() << " fields"); - ProductMetadataFields_1_0 fields; + ProductMetadataFields_1_N fields{ SchemaVersion }; web::json::value result; - result[fields.Version] = web::json::value::string(L"1.0"); + result[fields.Version] = web::json::value::string(fields.SchemaVersion); result[fields.ProductVersionMin] = AppInstaller::JSON::GetStringValue(ProductVersionMin.ToString()); result[fields.ProductVersionMax] = AppInstaller::JSON::GetStringValue(ProductVersionMax.ToString()); @@ -373,6 +407,7 @@ namespace AppInstaller::Repository::Metadata itemValue[fields.InstallerHash] = AppInstaller::JSON::GetStringValue(item.first); itemValue[fields.SubmissionIdentifier] = AppInstaller::JSON::GetStringValue(item.second.SubmissionIdentifier); + SetStringFromFutureSchema(itemValue, fields.Scope, item.second.Scope); web::json::value appsAndFeaturesArray = web::json::value::array(); size_t appsAndFeaturesEntryIndex = 0; @@ -736,6 +771,8 @@ namespace AppInstaller::Repository::Metadata newEntry.Publisher = package->GetProperty(PackageVersionProperty::Publisher).get(); // TODO: Support upgrade code throughout the code base... + Manifest::ScopeEnum scope = Manifest::ConvertToScopeEnum(packageMetadata[PackageVersionMetadata::InstalledScope]); + // Add or update the metadata for the installer hash auto itr = m_outputMetadata.InstallerMetadataMap.find(m_installerHash); @@ -747,10 +784,25 @@ namespace AppInstaller::Repository::Metadata newMetadata.SubmissionIdentifier = m_submissionIdentifier; newMetadata.AppsAndFeaturesEntries.emplace_back(std::move(newEntry)); + if (scope != Manifest::ScopeEnum::Unknown) + { + newMetadata.Scope = Manifest::ScopeToString(scope); + } + m_outputMetadata.InstallerMetadataMap[m_installerHash] = std::move(newMetadata); } else { + if (itr->second.Scope.empty()) + { + itr->second.Scope = Manifest::ScopeToString(scope); + } + // If there is a conflicting scope already present, force it to Unknown + else if (scope != Manifest::ScopeEnum::Unknown && Manifest::ConvertToScopeEnum(itr->second.Scope) != scope) + { + itr->second.Scope = Manifest::ScopeToString(Manifest::ScopeEnum::Unknown); + } + // Existing entry for installer hash, add/update the entry FilterAndAddToEntries(std::move(newEntry), itr->second.AppsAndFeaturesEntries); } @@ -996,6 +1048,19 @@ namespace AppInstaller::Repository::Metadata } else { + if (itr->second.Scope.empty()) + { + itr->second.Scope = installerMetadata.second.Scope; + } + else if (!installerMetadata.second.Scope.empty()) + { + // If there is a conflicting scope already present, force it to Unknown + if (Manifest::ConvertToScopeEnum(itr->second.Scope) != Manifest::ConvertToScopeEnum(installerMetadata.second.Scope)) + { + itr->second.Scope = Manifest::ScopeToString(Manifest::ScopeEnum::Unknown); + } + } + // Merge into existing installer data for (const auto& targetEntry : installerMetadata.second.AppsAndFeaturesEntries) { diff --git a/src/AppInstallerRepositoryCore/Public/winget/InstallerMetadataCollectionContext.h b/src/AppInstallerRepositoryCore/Public/winget/InstallerMetadataCollectionContext.h @@ -39,13 +39,20 @@ namespace AppInstaller::Repository::Metadata // The installer specific metadata that we collect. struct InstallerMetadata { + friend ProductMetadata; + + // 1.0 std::string SubmissionIdentifier; std::vector<Manifest::AppsAndFeaturesEntry> AppsAndFeaturesEntries; + + // 1.1 + std::string Scope; }; // Metadata from previous product revisions. struct HistoricalMetadata { + // 1.0 Utility::Version ProductVersionMin; Utility::Version ProductVersionMax; std::set<std::string> Names; @@ -54,6 +61,7 @@ namespace AppInstaller::Repository::Metadata std::set<std::string> UpgradeCodes; }; + // 1.0 Utility::Version SchemaVersion; Utility::Version ProductVersionMin; Utility::Version ProductVersionMax; @@ -62,8 +70,8 @@ namespace AppInstaller::Repository::Metadata std::vector<HistoricalMetadata> HistoricalMetadataList; private: - void FromJson_1_0(const web::json::value& json); - web::json::value ToJson_1_0(); + void FromJson_1_N(const web::json::value& json); + web::json::value ToJson_1_N(); // Removes the historical data with the oldest version. // Returns true if something was removed; false it not.