ARPCorrelation.cpp (13329B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "winget/ARPCorrelation.h" 5 #include "winget/ARPCorrelationAlgorithms.h" 6 #include "winget/Manifest.h" 7 #include "winget/NameNormalization.h" 8 #include "winget/RepositorySearch.h" 9 #include "winget/RepositorySource.h" 10 11 using namespace AppInstaller::Manifest; 12 using namespace AppInstaller::Repository; 13 using namespace AppInstaller::Utility; 14 15 namespace AppInstaller::Repository::Correlation 16 { 17 namespace 18 { 19 constexpr double MatchingThreshold = 0.5; 20 constexpr double MinimumDifferentiationThreshold = 0.05; 21 22 IARPMatchConfidenceAlgorithm& InstanceInternal(std::optional<IARPMatchConfidenceAlgorithm*> algorithmOverride = {}) 23 { 24 static WordsEditDistanceMatchConfidenceAlgorithm s_algorithm; 25 static IARPMatchConfidenceAlgorithm* s_override = nullptr; 26 27 if (algorithmOverride.has_value()) 28 { 29 s_override = algorithmOverride.value(); 30 } 31 32 if (s_override) 33 { 34 return *s_override; 35 } 36 else 37 { 38 return s_algorithm; 39 } 40 } 41 } 42 43 IARPMatchConfidenceAlgorithm& IARPMatchConfidenceAlgorithm::Instance() 44 { 45 return InstanceInternal(); 46 } 47 48 #ifndef AICLI_DISABLE_TEST_HOOKS 49 void IARPMatchConfidenceAlgorithm::OverrideInstance(IARPMatchConfidenceAlgorithm* algorithmOverride) 50 { 51 InstanceInternal(algorithmOverride); 52 } 53 54 void IARPMatchConfidenceAlgorithm::ResetInstance() 55 { 56 InstanceInternal(nullptr); 57 } 58 #endif 59 60 // Find the best match using heuristics 61 ARPHeuristicsCorrelationResult FindARPEntryForNewlyInstalledPackageWithHeuristics( 62 const Manifest::Manifest& manifest, 63 const std::vector<ARPEntry>& arpEntries) 64 { 65 // TODO: In the future we can make different passes with different algorithms until we find a match 66 return FindARPEntryForNewlyInstalledPackageWithHeuristics(manifest, arpEntries, IARPMatchConfidenceAlgorithm::Instance()); 67 } 68 69 ARPHeuristicsCorrelationResult FindARPEntryForNewlyInstalledPackageWithHeuristics( 70 const AppInstaller::Manifest::Manifest& manifest, 71 const std::vector<ARPEntry>& arpEntries, 72 IARPMatchConfidenceAlgorithm& algorithm) 73 { 74 if (arpEntries.empty()) 75 { 76 AICLI_LOG(Repo, Warning, << "Empty ARP entries given"); 77 return {}; 78 } 79 80 AICLI_LOG(Repo, Verbose, << "Looking for best match in ARP for manifest " << manifest.Id); 81 82 algorithm.Init(manifest); 83 84 ARPHeuristicsCorrelationResult result; 85 result.Measures.reserve(arpEntries.size()); 86 87 for (const auto& arpEntry : arpEntries) 88 { 89 auto score = algorithm.ComputeConfidence(arpEntry); 90 AICLI_LOG(Repo, Verbose, << "Match confidence for " << arpEntry.Entry->GetProperty(PackageProperty::Id) << ": " << score); 91 92 result.Measures.emplace_back(CorrelationMeasure{ score, arpEntry.Entry->GetLatestVersion() }); 93 } 94 95 std::sort(result.Measures.begin(), result.Measures.end(), [](const CorrelationMeasure& a, const CorrelationMeasure& b) { return a.Measure > b.Measure; }); 96 97 if (result.Measures[0].Measure < MatchingThreshold) 98 { 99 AICLI_LOG(Repo, Verbose, << "Maximum score [" << result.Measures[0].Measure << "] is lower than threshold [" << MatchingThreshold << "]"); 100 result.Reason = "maximum score below threshold"; 101 } 102 else if (result.Measures.size() >= 2 && (result.Measures[0].Measure - result.Measures[1].Measure) < MinimumDifferentiationThreshold) 103 { 104 AICLI_LOG(Repo, Verbose, << "Top two scores, [" << result.Measures[0].Measure << "] and [" << result.Measures[1].Measure << "] are not significantly different [" << MinimumDifferentiationThreshold << "]"); 105 result.Reason = "top two scores are not significantly different"; 106 } 107 else 108 { 109 AICLI_LOG(Repo, Verbose, << "Best match is " << result.Measures[0].Package->GetProperty(PackageVersionProperty::Id)); 110 result.Package = result.Measures[0].Package; 111 result.Reason = "heuristics match"; 112 } 113 114 return result; 115 } 116 117 void ARPCorrelationData::CapturePreInstallSnapshot() 118 { 119 ProgressCallback empty; 120 Repository::Source preInstallARP = Repository::Source(PredefinedSource::ARP); 121 preInstallARP.Open(empty); 122 123 for (const auto& entry : preInstallARP.Search({}).Matches) 124 { 125 auto installed = entry.Package->GetInstalled()->GetLatestVersion(); 126 if (installed) 127 { 128 m_preInstallSnapshot.emplace_back(std::make_tuple( 129 installed->GetProperty(PackageVersionProperty::Id), 130 installed->GetProperty(PackageVersionProperty::Version), 131 installed->GetProperty(PackageVersionProperty::Channel))); 132 } 133 } 134 135 std::sort(m_preInstallSnapshot.begin(), m_preInstallSnapshot.end()); 136 } 137 138 void ARPCorrelationData::CapturePostInstallSnapshot() 139 { 140 ProgressCallback empty; 141 m_postInstallSnapshotSource = Repository::Source(PredefinedSource::ARP); 142 m_postInstallSnapshotSource.Open(empty); 143 144 for (auto& entry : m_postInstallSnapshotSource.Search({}).Matches) 145 { 146 auto installed = entry.Package->GetInstalled()->GetLatestVersion(); 147 148 if (installed) 149 { 150 auto entryKey = std::make_tuple( 151 installed->GetProperty(PackageVersionProperty::Id), 152 installed->GetProperty(PackageVersionProperty::Version), 153 installed->GetProperty(PackageVersionProperty::Channel)); 154 155 auto itr = std::lower_bound(m_preInstallSnapshot.begin(), m_preInstallSnapshot.end(), entryKey); 156 m_postInstallSnapshot.emplace_back(entry.Package->GetInstalled(), itr == m_preInstallSnapshot.end() || *itr != entryKey); 157 } 158 } 159 } 160 161 ARPCorrelationResult ARPCorrelationData::CorrelateForNewlyInstalled(const Manifest::Manifest& manifest, const ARPCorrelationSettings& settings) 162 { 163 AICLI_LOG(Repo, Verbose, << "Finding ARP entry matching newly installed package"); 164 165 // Also attempt to find the entry based on the manifest data 166 167 SearchRequest manifestSearchRequest; 168 AppInstaller::Manifest::Manifest::string_t defaultPublisher; 169 if (manifest.DefaultLocalization.Contains(Localization::Publisher)) 170 { 171 defaultPublisher = manifest.DefaultLocalization.Get<Localization::Publisher>(); 172 } 173 174 // The default localization must contain the name or we cannot do this lookup 175 if (manifest.DefaultLocalization.Contains(Localization::PackageName)) 176 { 177 AppInstaller::Manifest::Manifest::string_t defaultName = manifest.DefaultLocalization.Get<Localization::PackageName>(); 178 manifestSearchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::NormalizedNameAndPublisher, MatchType::Exact, defaultName, defaultPublisher)); 179 180 for (const auto& loc : manifest.Localizations) 181 { 182 if (loc.Contains(Localization::PackageName) || loc.Contains(Localization::Publisher)) 183 { 184 manifestSearchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::NormalizedNameAndPublisher, MatchType::Exact, 185 loc.Contains(Localization::PackageName) ? loc.Get<Localization::PackageName>() : defaultName, 186 loc.Contains(Localization::Publisher) ? loc.Get<Localization::Publisher>() : defaultPublisher)); 187 } 188 } 189 } 190 191 std::set<std::string> productCodes; 192 std::set<std::string> upgradeCodes; 193 for (const auto& installer : manifest.Installers) 194 { 195 if (!installer.ProductCode.empty()) 196 { 197 // Add each ProductCode only once 198 if (productCodes.insert(installer.ProductCode).second) 199 { 200 manifestSearchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::Exact, installer.ProductCode)); 201 } 202 } 203 204 for (const auto& appsAndFeaturesEntry : installer.AppsAndFeaturesEntries) 205 { 206 if (!appsAndFeaturesEntry.DisplayName.empty()) 207 { 208 manifestSearchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::NormalizedNameAndPublisher, MatchType::Exact, 209 appsAndFeaturesEntry.DisplayName, 210 appsAndFeaturesEntry.Publisher.empty() ? defaultPublisher : appsAndFeaturesEntry.Publisher)); 211 } 212 213 // Add each ProductCode and UpgradeCode only once; 214 if (!appsAndFeaturesEntry.ProductCode.empty() && productCodes.insert(appsAndFeaturesEntry.ProductCode).second) 215 { 216 manifestSearchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::Exact, appsAndFeaturesEntry.ProductCode)); 217 } 218 if (!appsAndFeaturesEntry.UpgradeCode.empty() && upgradeCodes.insert(appsAndFeaturesEntry.UpgradeCode).second) 219 { 220 manifestSearchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::UpgradeCode, MatchType::Exact, appsAndFeaturesEntry.UpgradeCode)); 221 } 222 } 223 } 224 225 SearchResult findByManifest; 226 227 // Don't execute this search if it would just find everything 228 if (!manifestSearchRequest.IsForEverything()) 229 { 230 findByManifest = m_postInstallSnapshotSource.Search(manifestSearchRequest); 231 } 232 233 // Cross reference the changes with the search results 234 std::vector<std::shared_ptr<IPackage>> packagesInBoth; 235 236 for (const auto& change : m_postInstallSnapshot) 237 { 238 if (change.IsNewOrUpdated) 239 { 240 for (const auto& byManifest : findByManifest.Matches) 241 { 242 if (change.Entry->IsSame(byManifest.Package->GetInstalled().get())) 243 { 244 packagesInBoth.emplace_back(change.Entry); 245 break; 246 } 247 } 248 } 249 } 250 251 // We now have all of the package changes; time to report them. 252 // 253 // The set of cases we could have for finding packages based on the manifest: 254 // 0 packages :: The manifest data does not match the ARP information. 255 // 1 package :: Golden path; this should be what we installed. 256 // 2+ packages :: The data in the manifest is either too broad or we have 257 // a problem with our name normalization. 258 259 // Find the package that we are going to log 260 ARPCorrelationResult result; 261 // TODO: Find a good way to consider the other heuristics in these stats. 262 result.ChangesToARP = std::count_if(m_postInstallSnapshot.begin(), m_postInstallSnapshot.end(), [](const ARPEntry& e) { return e.IsNewOrUpdated; }); 263 result.MatchesInARP = findByManifest.Matches.size(); 264 result.CountOfIntersectionOfChangesAndMatches = packagesInBoth.size(); 265 266 // If there is only a single common package (changed and matches), it is almost certainly the correct one. 267 if (settings.AllowNormalization && packagesInBoth.size() == 1) 268 { 269 result.Package = packagesInBoth[0]->GetLatestVersion(); 270 result.Reason = "normalization match and new/changed"; 271 } 272 // If it wasn't changed but we still find a match, that is the best thing to report. 273 else if (settings.AllowNormalization && findByManifest.Matches.size() == 1) 274 { 275 result.Package = findByManifest.Matches[0].Package->GetInstalled()->GetLatestVersion(); 276 result.Reason = "normalization match (not new/changed)"; 277 } 278 else if (settings.AllowSingleChange && result.ChangesToARP == 1) 279 { 280 result.Package = std::find_if(m_postInstallSnapshot.begin(), m_postInstallSnapshot.end(), [](const ARPEntry& e) { return e.IsNewOrUpdated; })->Entry->GetLatestVersion(); 281 result.Reason = "only new/changed value"; 282 } 283 else 284 { 285 // We were not able to find an exact match, so we now run some heuristics 286 // to try and match the package with some ARP entry by assigning them scores. 287 AICLI_LOG(Repo, Verbose, << "No exact ARP match found. Trying to find one with heuristics"); 288 289 result = FindARPEntryForNewlyInstalledPackageWithHeuristics(manifest, m_postInstallSnapshot); 290 } 291 292 return result; 293 } 294 }