ARPHelper.cpp (25473B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "ARPHelper.h" 5 #include "winget/PortableARPEntry.h" 6 7 namespace AppInstaller::Repository::Microsoft 8 { 9 using namespace AppInstaller::Registry::Portable; 10 11 namespace 12 { 13 // "Unpacks" a GUID in the format used by the UpgradesCode registry key into the usual format. 14 // Returns empty if it is not a valid GUID 15 std::optional<std::string> TryUnpackUpgradeCodeGuid(std::string_view packed) 16 { 17 // A GUID is made up of 4 parts: 18 // - Part 1 is made up of one 4 byte block 19 // - Parts 2 and 3 are made up of one 2 byte block 20 // - Part 4 is made up of eight 1 byte blocks 21 // 22 // The GUID strings we have in the manifests represent all of this in hex in order, 23 // with dashes between each part, and after the second byte of Part 4. 24 // The "packed" GUIDs in the registry place the blocks in the same order, 25 // without dashes and with opposite endian-ness. 26 // 27 // For example 28 // ARP: {FECAFEB5-8D0E-4AE4-8FA0-745BAA835C35} 29 // FECAFEB5 8D0E 4AE4 8F A0 74 5B AA 83 5C 35 30 // Part 1 P2 P3 <------ Part 4 -------> 31 // 5BEFACEF E0D8 4EA4 F8 0A 47 B5 AA 38 C5 53 32 // UpgradeCode: 5BEFACEFE0D84EA4F80A47B5AA38C553 33 // 34 // The conversion can be done by mapping each location in the packed string 35 // to the appropriate location in the unpacked string. 36 constexpr size_t PackedLength = 32; 37 if (packed.length() != PackedLength || !std::all_of(packed.begin(), packed.end(), isxdigit)) 38 { 39 return {}; 40 } 41 42 // PositionMapping[i] is the position to which the i-th char is mapped 43 // I.e., unpacked[ PositionMapping[i] ] = packed[i] 44 constexpr size_t PositionMapping[PackedLength] = 45 { 46 8,7,6,5,4,3,2,1, 47 13,12,11,10, 48 18,17,16,15, 49 21,20, 23,22, 50 26,25, 28,27, 30,29, 32,31, 34,33, 36,35, 51 }; 52 53 std::string unpacked("{00000000-0000-0000-0000-000000000000}"); 54 for (size_t i = 0; i < PackedLength; ++i) 55 { 56 unpacked[PositionMapping[i]] = packed[i]; 57 } 58 59 return unpacked; 60 } 61 62 // Gets a mapping from ProductCode to UpgradeCode for MSI packages. 63 std::map<std::string, std::string> GetUpgradeCodes() 64 { 65 // The UpgradeCode is not stored in the ARP registry keys, so we have to get it separately. 66 // We could use MsiGetProductProperty or MsiGetProperty from the MSI API to query it, 67 // but it is very slow. 68 // 69 // The UpgradeCode is also stored in the registry under 70 // HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UpgradeCodes 71 // (Note that this key is not documented, so it is possible that it will change but very unlikely...) 72 // 73 // Under 'UpgradeCodes' there is one key for each upgrade code, and each upgrade code key 74 // contains the product code as a value. All the upgrade codes and product codes are GUIDs, 75 // but represented in an unusual way - see TryUnpackUpgradeCodeGuid() 76 77 AICLI_LOG(Repo, Info, << "Reading MSI UpgradeCodes"); 78 std::map<std::string, std::string> upgradeCodes; 79 80 try 81 { 82 // There is no UpgradeCodes key on the x86 view of the registry 83 Registry::Key upgradeCodesKey = Registry::Key::OpenIfExists(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Installer\\UpgradeCodes", 0, KEY_READ | KEY_WOW64_64KEY); 84 85 if (upgradeCodesKey) 86 { 87 for (const auto& upgradeCodeKeyRef : upgradeCodesKey) 88 { 89 std::string keyName; 90 91 try 92 { 93 keyName = upgradeCodeKeyRef.Name(); 94 auto upgradeCode = TryUnpackUpgradeCodeGuid(keyName); 95 if (upgradeCode) 96 { 97 auto upgradeCodeKey = upgradeCodeKeyRef.Open(); 98 for (const auto& productCodeValue : upgradeCodeKey.Values()) 99 { 100 auto productCode = TryUnpackUpgradeCodeGuid(productCodeValue.Name()); 101 if (productCode) 102 { 103 upgradeCodes[*productCode] = *upgradeCode; 104 } 105 } 106 } 107 } 108 CATCH_LOG_MSG("Failed to read upgrade code: %hs", keyName.c_str()); 109 } 110 } 111 } 112 CATCH_LOG_MSG("Failed to read upgrade codes."); 113 114 return upgradeCodes; 115 } 116 } 117 118 #ifndef AICLI_DISABLE_TEST_HOOKS 119 using GetARPKeyFunc = std::function<Registry::Key(Manifest::ScopeEnum, Utility::Architecture)>; 120 static GetARPKeyFunc s_GetARPKey_Override; 121 122 void SetGetARPKeyOverride(GetARPKeyFunc value) 123 { 124 s_GetARPKey_Override = value; 125 } 126 #endif 127 128 Registry::Key ARPHelper::GetARPKey(Manifest::ScopeEnum scope, Utility::Architecture architecture) const 129 { 130 #ifndef AICLI_DISABLE_TEST_HOOKS 131 if (s_GetARPKey_Override) 132 { 133 return s_GetARPKey_Override(scope, architecture); 134 } 135 #endif 136 137 HKEY rootKey = NULL; 138 139 switch (scope) 140 { 141 case Manifest::ScopeEnum::User: 142 rootKey = HKEY_CURRENT_USER; 143 break; 144 case Manifest::ScopeEnum::Machine: 145 rootKey = HKEY_LOCAL_MACHINE; 146 break; 147 default: 148 THROW_HR(E_UNEXPECTED); 149 } 150 151 bool isValid = false; 152 REGSAM access = KEY_READ; 153 154 switch (Utility::GetSystemArchitecture()) 155 { 156 case Utility::Architecture::X86: 157 switch (architecture) 158 { 159 case Utility::Architecture::X86: 160 isValid = true; 161 break; 162 } 163 break; 164 case Utility::Architecture::X64: 165 switch (architecture) 166 { 167 case Utility::Architecture::X86: 168 if (scope == Manifest::ScopeEnum::Machine) 169 { 170 access |= KEY_WOW64_32KEY; 171 isValid = true; 172 } 173 break; 174 case Utility::Architecture::X64: 175 access |= KEY_WOW64_64KEY; 176 isValid = true; 177 break; 178 } 179 break; 180 case Utility::Architecture::Arm: 181 switch (architecture) 182 { 183 case Utility::Architecture::Arm: 184 isValid = true; 185 break; 186 } 187 break; 188 case Utility::Architecture::Arm64: 189 switch (architecture) 190 { 191 case Utility::Architecture::X86: 192 if (scope == Manifest::ScopeEnum::Machine) 193 { 194 #ifdef _ARM_ 195 // Not accessible if this is an ARM process 196 AICLI_LOG(Repo, Warning, << "Cannot enumerate x86 machine ARP entries when current process is ARM"); 197 #else 198 access |= KEY_WOW64_32KEY; 199 isValid = true; 200 #endif 201 } 202 break; 203 case Utility::Architecture::Arm64: 204 access |= KEY_WOW64_64KEY; 205 isValid = true; 206 break; 207 } 208 break; 209 } 210 211 if (isValid) 212 { 213 return Registry::Key::OpenIfExists(rootKey, SubKeyPath, 0, access); 214 } 215 else 216 { 217 return {}; 218 } 219 } 220 221 Registry::Key ARPHelper::FindARPEntry(const std::string& productCode, Manifest::ScopeEnum scope) const 222 { 223 if (productCode.empty()) 224 { 225 return {}; 226 } 227 228 std::vector<Manifest::ScopeEnum> scopesToSearch; 229 if (scope == Manifest::ScopeEnum::Unknown) 230 { 231 scopesToSearch = { Manifest::ScopeEnum::User, Manifest::ScopeEnum::Machine }; 232 } 233 else 234 { 235 scopesToSearch = { scope }; 236 } 237 238 for (auto scopeToSearch : scopesToSearch) 239 { 240 for (auto architecture : Utility::GetApplicableArchitectures()) 241 { 242 Registry::Key arpRootKey = GetARPKey(scopeToSearch, architecture); 243 if (arpRootKey) 244 { 245 for (const auto& entry : arpRootKey) 246 { 247 if (Utility::CaseInsensitiveEquals(productCode, entry.Name())) 248 { 249 return entry.Open(); 250 } 251 } 252 } 253 } 254 } 255 256 return {}; 257 } 258 259 bool ARPHelper::GetBoolValue(const Registry::Key& arpKey, const std::wstring& name) 260 { 261 auto value = arpKey[name]; 262 return (value && value->GetType() == Registry::Value::Type::DWord && value->GetValue<Registry::Value::Type::DWord>()); 263 } 264 265 std::string ARPHelper::GetStringValue(const Registry::Key& arpKey, const std::wstring& name) 266 { 267 auto value = arpKey[name]; 268 if (value && value->GetType() == Registry::Value::Type::String) 269 { 270 return value->GetValue<Registry::Value::Type::String>(); 271 } 272 273 return {}; 274 } 275 276 std::string ARPHelper::DetermineVersion(const Registry::Key& arpKey) const 277 { 278 // First check DisplayVersion for a complete version string 279 auto displayVersion = arpKey[DisplayVersion]; 280 if (displayVersion && displayVersion->GetType() == Registry::Value::Type::String) 281 { 282 std::string result = displayVersion->GetValue<Registry::Value::Type::String>(); 283 if (!result.empty()) 284 { 285 return result; 286 } 287 } 288 289 // Next attempt VersionMajor.VersionMinor, then MajorVersion.MinorVersion 290 for (const auto& names : { std::make_pair(std::ref(VersionMajor), std::ref(VersionMinor)), std::make_pair(std::ref(MajorVersion), std::ref(MinorVersion)) }) 291 { 292 auto majorVersion = arpKey[names.first]; 293 auto minorVersion = arpKey[names.second]; 294 if (majorVersion || minorVersion) 295 { 296 uint32_t majorVersionInt = 0; 297 uint32_t minorVersionInt = 0; 298 299 if (majorVersion && majorVersion->GetType() == Registry::Value::Type::DWord) 300 { 301 majorVersionInt = majorVersion->GetValue<Registry::Value::Type::DWord>(); 302 } 303 304 if (minorVersion && minorVersion->GetType() == Registry::Value::Type::DWord) 305 { 306 minorVersionInt = minorVersion->GetValue<Registry::Value::Type::DWord>(); 307 } 308 309 if (majorVersionInt || minorVersionInt) 310 { 311 std::ostringstream strstr; 312 strstr << majorVersionInt << '.' << minorVersionInt; 313 return strstr.str(); 314 } 315 } 316 } 317 318 // Finally attempt to turn the Version DWORD into a version string 319 auto version = arpKey[Version]; 320 if (version && version->GetType() == Registry::Value::Type::DWord) 321 { 322 uint32_t versionInt = version->GetValue<Registry::Value::Type::DWord>(); 323 if (versionInt) 324 { 325 std::ostringstream strstr; 326 strstr << ((versionInt & 0xFF000000) >> 24) << '.' << ((versionInt & 0x00FF0000) >> 16) << '.' << (versionInt & 0x0000FFFF); 327 return strstr.str(); 328 } 329 } 330 331 return Utility::Version::CreateUnknown().ToString(); 332 } 333 334 void ARPHelper::AddMetadataIfPresent(const Registry::Key& key, const std::wstring& name, SQLiteIndex& index, SQLiteIndex::IdType manifestId, PackageVersionMetadata metadata) const 335 { 336 auto value = key[name]; 337 if (value) 338 { 339 std::string valueString; 340 341 if (value->GetType() == Registry::Value::Type::String) 342 { 343 valueString = value->GetValue<Registry::Value::Type::String>(); 344 } 345 else if (value->GetType() == Registry::Value::Type::ExpandString) 346 { 347 valueString = value->GetValue<Registry::Value::Type::ExpandString>(); 348 } 349 else if (value->GetType() == Registry::Value::Type::DWord) 350 { 351 DWORD dwordValue = value->GetValue<Registry::Value::Type::DWord>(); 352 if (name == Language) 353 { 354 valueString = Locale::LocaleIdToBcp47Tag(dwordValue); 355 } 356 else 357 { 358 std::ostringstream strstr; 359 strstr << dwordValue; 360 valueString = strstr.str(); 361 } 362 } 363 364 if (!valueString.empty()) 365 { 366 index.SetMetadataByManifestId(manifestId, metadata, valueString); 367 } 368 } 369 } 370 371 void ARPHelper::PopulateIndexFromARP(SQLiteIndex& index, Manifest::ScopeEnum scope) const 372 { 373 auto upgradeCodes = GetUpgradeCodes(); 374 375 for (auto architecture : Utility::GetApplicableArchitectures()) 376 { 377 Registry::Key arpRootKey = GetARPKey(scope, architecture); 378 379 if (arpRootKey) 380 { 381 PopulateIndexFromKey(index, arpRootKey, Manifest::ScopeToString(scope), Utility::ToString(architecture), upgradeCodes); 382 } 383 } 384 } 385 386 void ARPHelper::PopulateIndexFromKey(SQLiteIndex& index, const Registry::Key& key, std::string_view scope, std::string_view architecture, const std::map<std::string, std::string>& upgradeCodes) const 387 { 388 AICLI_LOG(Repo, Verbose, << "Examining ARP entries for " << scope << " | " << architecture); 389 390 for (const auto& arpEntry : key) 391 { 392 std::string productCode; 393 394 try 395 { 396 productCode = arpEntry.Name(); 397 398 Manifest::Manifest manifest; 399 manifest.DefaultLocalization.Add<Manifest::Localization::Tags>({ "ARP" }); 400 401 // Construct a unique name for this entry 402 const char separator = '\\'; 403 404 std::ostringstream stream; 405 stream << "ARP" << separator << scope << separator << architecture << separator << productCode; 406 407 manifest.Id = stream.str(); 408 409 manifest.Installers.emplace_back(); 410 // TODO: This likely needs some cleanup applied, as it looks like INNO tends to append an "_is#" 411 // that might vary across machines/installs. There may be other things we want to clean up as well, 412 // like trimming spaces at the ends, or removing the version string from the product code 413 // if it is present. 414 manifest.Installers[0].ProductCode = productCode; 415 416 Registry::Key arpKey = arpEntry.Open(); 417 418 // Ignore entries that are listed as SystemComponent 419 if (GetBoolValue(arpKey, SystemComponent)) 420 { 421 AICLI_LOG(Repo, Verbose, << "Skipping " << productCode << " because it is a SystemComponent"); 422 continue; 423 } 424 425 // If no name is provided, ignore this entry 426 auto displayName = arpKey[DisplayName]; 427 if (!displayName || displayName->GetType() != Registry::Value::Type::String) 428 { 429 AICLI_LOG(Repo, Verbose, << "Skipping " << productCode << " because DisplayName is not a REG_SZ value"); 430 continue; 431 } 432 auto displayNameValue = displayName->GetValue<Registry::Value::Type::String>(); 433 if (displayNameValue.empty()) 434 { 435 AICLI_LOG(Repo, Verbose, << "Skipping " << productCode << " because DisplayName is empty"); 436 continue; 437 } 438 439 manifest.DefaultLocalization.Add<Manifest::Localization::PackageName>(displayNameValue); 440 // Add DisplayName to ARP entries too 441 // This is to help normalized publisher and name correlation where ARP DisplayName matching 442 // will be getting improved in future iterations. 443 manifest.Installers[0].AppsAndFeaturesEntries.emplace_back(); 444 manifest.Installers[0].AppsAndFeaturesEntries[0].DisplayName = displayNameValue; 445 446 // If no version can be determined, ignore this entry 447 manifest.Version = DetermineVersion(arpKey); 448 if (manifest.Version.empty()) 449 { 450 AICLI_LOG(Repo, Verbose, << "Skipping " << productCode << " because a version could not be determined"); 451 continue; 452 } 453 454 auto publisher = arpKey[Publisher]; 455 if (publisher && publisher->GetType() == Registry::Value::Type::String) 456 { 457 manifest.DefaultLocalization.Add<Manifest::Localization::Publisher>(publisher->GetValue<Registry::Value::Type::String>()); 458 459 // If Publisher is set, change the Id using name normalization 460 // TODO: Figure out how to actually make this work since there are often instances of the same 461 // data in x64 and x86 entries that will collide. 462 //auto normalizedName = index.NormalizeName( 463 // manifest.DefaultLocalization.Get<Manifest::Localization::PackageName>(), 464 // manifest.DefaultLocalization.Get<Manifest::Localization::Publisher>()); 465 //manifest.Id = normalizedName.Publisher() + '.' + normalizedName.Name(); 466 } 467 468 // Pick up WindowsInstaller to determine if this is an MSI install. 469 // TODO: Could also determine Inno (and maybe other types) through detecting other keys here. 470 auto installedType = Manifest::InstallerTypeEnum::Exe; 471 472 if (GetBoolValue(arpKey, WindowsInstaller)) 473 { 474 installedType = Manifest::InstallerTypeEnum::Msi; 475 476 // If this is an MSI, look up the UpgradeCode 477 auto upgradeCodeItr = upgradeCodes.find(productCode); 478 if (upgradeCodeItr != upgradeCodes.end()) 479 { 480 manifest.Installers[0].AppsAndFeaturesEntries[0].UpgradeCode = upgradeCodeItr->second; 481 } 482 } 483 484 // TODO: If we want to keep the constructed manifest around to allow for `show` type commands 485 // against installed packages, we should use URLInfoAbout/HelpLink for the Homepage. 486 487 // TODO: Determine the best way to handle duplicates; sometimes the same package will be listed under 488 // both x64 and x86 locations for ARP. 489 // For now, we will attempt to insert and catch. 490 std::optional<SQLiteIndex::IdType> manifestIdOpt; 491 492 try 493 { 494 // Use the ProductCode as a unique key for the path 495 manifestIdOpt = index.AddManifest(manifest); 496 } 497 catch (...) 498 { 499 // Ignore errors if they occur, they are most likely a duplicate value 500 } 501 502 if (!manifestIdOpt) 503 { 504 AICLI_LOG(Repo, Warning, 505 << "Ignoring duplicate ARP entry " << scope << '|' << architecture << '|' << productCode << " [" << manifest.DefaultLocalization.Get<Manifest::Localization::PackageName>() << "]"); 506 continue; 507 } 508 509 SQLiteIndex::IdType manifestId = manifestIdOpt.value(); 510 511 // Pass scope along to metadata. 512 index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::InstalledScope, scope); 513 514 // TODO: Pass along architecture, although there are cases where it is not clear what architecture the package 515 // is from it's ARP location, despite it very clearly being a specific architecture. And note that user 516 // scope does not have separate ARP locations, so every architecture would appear as native. 517 518 // Publisher is needed for certain scenarios but we don't store it from the manifest 519 if (manifest.DefaultLocalization.Contains(Manifest::Localization::Publisher)) 520 { 521 index.SetMetadataByManifestId( 522 manifestId, PackageVersionMetadata::Publisher, 523 manifest.DefaultLocalization.Get<Manifest::Localization::Publisher>()); 524 } 525 526 // Pick up InstallLocation when upgrade supports remove/install to enable this location 527 // to survive across the removal. 528 AddMetadataIfPresent(arpKey, InstallLocation, index, manifestId, PackageVersionMetadata::InstalledLocation); 529 530 // Pick up UninstallString and QuietUninstallString for uninstall. 531 AddMetadataIfPresent(arpKey, UninstallString, index, manifestId, PackageVersionMetadata::StandardUninstallCommand); 532 AddMetadataIfPresent(arpKey, QuietUninstallString, index, manifestId, PackageVersionMetadata::SilentUninstallCommand); 533 534 // Pick up ModifyPath for repair. 535 AddMetadataIfPresent(arpKey, ModifyPath, index, manifestId, PackageVersionMetadata::StandardModifyCommand); 536 AddMetadataIfPresent(arpKey, NoModify, index, manifestId, PackageVersionMetadata::NoModify); 537 AddMetadataIfPresent(arpKey, NoRepair, index, manifestId, PackageVersionMetadata::NoRepair); 538 539 // Pick up Language to enable proper selection of language for upgrade. 540 AddMetadataIfPresent(arpKey, Language, index, manifestId, PackageVersionMetadata::InstalledLocale); 541 542 if (Manifest::ConvertToInstallerTypeEnum(GetStringValue(arpKey, std::wstring{ ToString(PortableValueName::WinGetInstallerType) })) == Manifest::InstallerTypeEnum::Portable) 543 { 544 // Portable uninstall requires the installed architecture for locating the entry in the registry. 545 index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::InstalledArchitecture, architecture); 546 installedType = Manifest::InstallerTypeEnum::Portable; 547 } 548 549 index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::InstalledType, Manifest::InstallerTypeToString(installedType)); 550 } 551 catch (...) 552 { 553 AICLI_LOG(Repo, Warning, << "Failed to read ARP entry, ignoring it: " << scope << '|' << architecture << '|' << productCode); 554 LOG_CAUGHT_EXCEPTION(); 555 } 556 } 557 } 558 559 std::vector<wil::unique_registry_watcher> ARPHelper::CreateRegistryWatchers(Manifest::ScopeEnum scope, std::function<void(Manifest::ScopeEnum, Utility::Architecture, wil::RegistryChangeKind)> callback) 560 { 561 std::vector<wil::unique_registry_watcher> result; 562 563 auto addToResult = [&](Manifest::ScopeEnum scopeToUse) 564 { 565 for (auto architecture : Utility::GetApplicableArchitectures()) 566 { 567 Registry::Key arpRootKey = GetARPKey(scopeToUse, architecture); 568 569 if (arpRootKey) 570 { 571 result.emplace_back(wil::make_registry_watcher(arpRootKey, L"", true, [scopeToUse, architecture, callback](wil::RegistryChangeKind change) { callback(scopeToUse, architecture, change); })); 572 } 573 } 574 }; 575 576 if (scope == Manifest::ScopeEnum::Unknown) 577 { 578 addToResult(Manifest::ScopeEnum::User); 579 addToResult(Manifest::ScopeEnum::Machine); 580 } 581 else 582 { 583 addToResult(scope); 584 } 585 586 return result; 587 } 588 }