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:
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.