MSStoreDownload.cpp (53067B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include <AppInstallerStrings.h> 5 #include <AppInstallerErrors.h> 6 #include <AppinstallerLogging.h> 7 #include "AppInstallerMsixInfo.h" 8 #include "AppInstallerRuntime.h" 9 #include "winget/HttpClientHelper.h" 10 #include "winget/JsonUtil.h" 11 #include "winget/Locale.h" 12 #include "winget/MSStoreDownload.h" 13 #include "winget/NetworkSettings.h" 14 #include "winget/Rest.h" 15 #include "winget/UserSettings.h" 16 #ifndef WINGET_DISABLE_FOR_FUZZING 17 #include <sfsclient/SFSClient.h> 18 #endif 19 20 namespace AppInstaller::MSStore 21 { 22 using namespace std::string_view_literals; 23 24 #ifndef AICLI_DISABLE_TEST_HOOKS 25 namespace TestHooks 26 { 27 static std::shared_ptr<web::http::http_pipeline_stage> s_DisplayCatalog_HttpPipelineStage_Override = nullptr; 28 29 void SetDisplayCatalogHttpPipelineStage_Override(std::shared_ptr<web::http::http_pipeline_stage> value) 30 { 31 s_DisplayCatalog_HttpPipelineStage_Override = value; 32 } 33 34 static std::function<std::vector<SFS::AppContent>(std::string_view)>* s_SfsClient_AppContents_Override = nullptr; 35 36 void SetSfsClientAppContents_Override(std::function<std::vector<SFS::AppContent>(std::string_view)>* value) 37 { 38 s_SfsClient_AppContents_Override = value; 39 } 40 41 static std::shared_ptr<web::http::http_pipeline_stage> s_Licensing_HttpPipelineStage_Override = nullptr; 42 43 void SetLicensingHttpPipelineStage_Override(std::shared_ptr<web::http::http_pipeline_stage> value) 44 { 45 s_Licensing_HttpPipelineStage_Override = value; 46 } 47 } 48 #endif 49 50 namespace DisplayCatalogDetails 51 { 52 // Default preferred sku to use 53 constexpr std::string_view TargetSkuIdValue = "0015"sv; 54 55 // Json response fields 56 constexpr std::string_view Product = "Product"sv; 57 constexpr std::string_view DisplaySkuAvailabilities = "DisplaySkuAvailabilities"sv; 58 constexpr std::string_view Sku = "Sku"sv; 59 constexpr std::string_view SkuId = "SkuId"sv; 60 constexpr std::string_view Properties = "Properties"sv; 61 constexpr std::string_view Packages = "Packages"sv; 62 constexpr std::string_view Languages = "Languages"sv; 63 constexpr std::string_view PackageFormat = "PackageFormat"sv; 64 constexpr std::string_view PackageId = "PackageId"sv; 65 constexpr std::string_view Architectures = "Architectures"sv; 66 constexpr std::string_view ContentId = "ContentId"sv; 67 constexpr std::string_view FulfillmentData = "FulfillmentData"sv; 68 constexpr std::string_view WuCategoryId = "WuCategoryId"sv; 69 70 // Display catalog rest endpoint 71 constexpr std::string_view DisplayCatalogRestApi = R"(https://displaycatalog.mp.microsoft.com/v7.0/products/{0}?fieldsTemplate={1}&market={2}&languages={3}&catalogIds={4})"; 72 constexpr std::string_view Details = "Details"sv; 73 constexpr std::string_view Neutral = "Neutral"sv; 74 constexpr std::string_view TargetCatalogId = "4"sv; 75 76 enum class DisplayCatalogPackageFormatEnum 77 { 78 Unknown, 79 AppxBundle, 80 MsixBundle, 81 Appx, 82 Msix, 83 }; 84 85 DisplayCatalogPackageFormatEnum ConvertToPackageFormatEnum(std::string_view packageFormatStr) 86 { 87 std::string packageFormat = Utility::ToLower(packageFormatStr); 88 if (packageFormat == "appxbundle") 89 { 90 return DisplayCatalogPackageFormatEnum::AppxBundle; 91 } 92 else if (packageFormat == "msixbundle") 93 { 94 return DisplayCatalogPackageFormatEnum::MsixBundle; 95 } 96 else if (packageFormat == "appx") 97 { 98 return DisplayCatalogPackageFormatEnum::Appx; 99 } 100 else if (packageFormat == "msix") 101 { 102 return DisplayCatalogPackageFormatEnum::Msix; 103 } 104 105 AICLI_LOG(Core, Info, << "ConvertToPackageFormatEnum: Unknown package format: " << packageFormatStr); 106 return DisplayCatalogPackageFormatEnum::Unknown; 107 } 108 109 struct DisplayCatalogPackage 110 { 111 std::string PackageId; 112 113 std::vector<AppInstaller::Utility::Architecture> Architectures; 114 115 std::vector<std::string> Languages; 116 117 DisplayCatalogPackageFormatEnum PackageFormat = DisplayCatalogPackageFormatEnum::Unknown; 118 119 // To be used later in sfs-client 120 std::string WuCategoryId; 121 122 // To be used later in licensing 123 std::string ContentId; 124 }; 125 126 // Display catalog package comparison logic. 127 // The comparator follows similar logic as ManifestComparator. 128 namespace DisplayCatalogPackageComparison 129 { 130 struct DisplayCatalogPackageComparisonField 131 { 132 DisplayCatalogPackageComparisonField(std::string_view name) : m_name(name) {} 133 134 virtual ~DisplayCatalogPackageComparisonField() = default; 135 136 std::string_view Name() const { return m_name; } 137 138 virtual bool IsApplicable(const DisplayCatalogPackage& package) = 0; 139 140 virtual bool IsFirstBetter(const DisplayCatalogPackage& first, const DisplayCatalogPackage& second) = 0; 141 142 private: 143 std::string_view m_name; 144 }; 145 146 struct PackageFormatComparator : public DisplayCatalogPackageComparisonField 147 { 148 PackageFormatComparator() : DisplayCatalogPackageComparisonField("Package Format") {} 149 150 bool IsApplicable(const DisplayCatalogPackage& package) override 151 { 152 return package.PackageFormat != DisplayCatalogPackageFormatEnum::Unknown; 153 } 154 155 bool IsFirstBetter(const DisplayCatalogPackage& first, const DisplayCatalogPackage& second) override 156 { 157 return IsPackageFormatBundle(first) && !IsPackageFormatBundle(second); 158 } 159 160 private: 161 bool IsPackageFormatBundle(const DisplayCatalogPackage& package) 162 { 163 return 164 package.PackageFormat == DisplayCatalogPackageFormatEnum::AppxBundle || 165 package.PackageFormat == DisplayCatalogPackageFormatEnum::MsixBundle; 166 } 167 }; 168 169 struct LocaleComparator : public DisplayCatalogPackageComparisonField 170 { 171 LocaleComparator(std::string locale) : DisplayCatalogPackageComparisonField("Locale") 172 { 173 if (!locale.empty()) 174 { 175 m_locales.emplace_back(std::move(locale)); 176 m_isRequirement = true; 177 } 178 else 179 { 180 m_locales = Locale::GetUserPreferredLanguages(); 181 } 182 183 AICLI_LOG(Core, Verbose, 184 << "Locale Comparator created with locales: " << Utility::ConvertContainerToString(m_locales) 185 << " , Is requirement: " << m_isRequirement); 186 } 187 188 bool IsApplicable(const DisplayCatalogPackage& package) override 189 { 190 if (m_isRequirement) 191 { 192 for (auto const& locale : m_locales) 193 { 194 double distanceScore = GetBestDistanceScoreFromList(locale, package.Languages); 195 if (distanceScore >= Locale::MinimumDistanceScoreAsCompatibleMatch) 196 { 197 return true; 198 } 199 } 200 201 return false; 202 } 203 else 204 { 205 return true; 206 } 207 } 208 209 bool IsFirstBetter(const DisplayCatalogPackage& first, const DisplayCatalogPackage& second) 210 { 211 for (auto const& locale : m_locales) 212 { 213 double firstScore = GetBestDistanceScoreFromList(locale, first.Languages); 214 double secondScore = GetBestDistanceScoreFromList(locale, second.Languages); 215 216 if (firstScore >= Locale::MinimumDistanceScoreAsCompatibleMatch || secondScore >= Locale::MinimumDistanceScoreAsCompatibleMatch) 217 { 218 return firstScore > secondScore; 219 } 220 } 221 222 return false; 223 } 224 225 private: 226 double GetBestDistanceScoreFromList(std::string_view targetLocale, const std::vector<std::string>& locales) 227 { 228 double finalScore = 0; 229 for (auto const& locale : locales) 230 { 231 double currentScore = Locale::GetDistanceOfLanguage(targetLocale, locale); 232 if (currentScore > finalScore) 233 { 234 finalScore = currentScore; 235 } 236 } 237 238 return finalScore; 239 } 240 241 std::vector<std::string> m_locales; 242 bool m_isRequirement = false; 243 }; 244 245 struct ArchitectureComparator : public DisplayCatalogPackageComparisonField 246 { 247 ArchitectureComparator(Utility::Architecture architecture) : DisplayCatalogPackageComparisonField("Architecture") 248 { 249 if (architecture != Utility::Architecture::Unknown) 250 { 251 m_architectures.emplace_back(architecture); 252 m_isRequirement = true; 253 } 254 else 255 { 256 m_architectures = Utility::GetApplicableArchitectures(); 257 } 258 259 AICLI_LOG(Core, Verbose, 260 << "Architecture Comparator created with archs: " << Utility::ConvertContainerToString(m_architectures, Utility::ToString) 261 << " , Is requirement: " << m_isRequirement); 262 } 263 264 bool IsApplicable(const DisplayCatalogPackage& package) override 265 { 266 if (m_isRequirement) 267 { 268 for (auto arch : package.Architectures) 269 { 270 if (Utility::IsApplicableArchitecture(arch, m_architectures) > Utility::InapplicableArchitecture) 271 { 272 return true; 273 } 274 } 275 276 return false; 277 } 278 else 279 { 280 return true; 281 } 282 } 283 284 bool IsFirstBetter(const DisplayCatalogPackage& first, const DisplayCatalogPackage& second) override 285 { 286 for (auto arch : m_architectures) 287 { 288 auto firstItr = std::find(first.Architectures.begin(), first.Architectures.end(), arch); 289 auto secondItr = std::find(second.Architectures.begin(), second.Architectures.end(), arch); 290 291 if (firstItr != first.Architectures.end() && secondItr == second.Architectures.end()) 292 { 293 true; 294 } 295 else if (secondItr != second.Architectures.end()) 296 { 297 return false; 298 } 299 } 300 301 return false; 302 } 303 304 private: 305 std::vector<Utility::Architecture> m_architectures; 306 bool m_isRequirement = false; 307 }; 308 309 struct DisplayCatalogPackageComparator 310 { 311 DisplayCatalogPackageComparator(std::string requiredLocale, Utility::Architecture requiredArch) 312 { 313 // Order of comparators matters. 314 AddComparator(std::make_unique<LocaleComparator>(requiredLocale)); 315 AddComparator(std::make_unique<ArchitectureComparator>(requiredArch)); 316 AddComparator(std::make_unique<PackageFormatComparator>()); 317 } 318 319 // Gets the best installer from the manifest, if at least one is applicable. 320 std::optional<DisplayCatalogPackage> GetPreferredPackage(const std::vector<DisplayCatalogPackage>& packages) 321 { 322 AICLI_LOG(Core, Verbose, << "Starting display catalog package selection."); 323 324 const DisplayCatalogPackage* result = nullptr; 325 for (const auto& package : packages) 326 { 327 if (IsApplicable(package) && (!result || IsFirstBetter(package, *result))) 328 { 329 result = &package; 330 } 331 } 332 333 if (result) 334 { 335 return *result; 336 } 337 else 338 { 339 return {}; 340 } 341 } 342 343 // Determines if the package is applicable. 344 bool IsApplicable(const DisplayCatalogPackage& package) 345 { 346 for (const auto& comparator : m_comparators) 347 { 348 if (!comparator->IsApplicable(package)) 349 { 350 return false; 351 } 352 } 353 354 return true; 355 } 356 357 // Determines if the first package is a better choice. 358 bool IsFirstBetter(const DisplayCatalogPackage& first, const DisplayCatalogPackage& second) 359 { 360 for (const auto& comparator : m_comparators) 361 { 362 bool forwardCompare = comparator->IsFirstBetter(first, second); 363 bool reverseCompare = comparator->IsFirstBetter(second, first); 364 365 if (forwardCompare && reverseCompare) 366 { 367 AICLI_LOG(Core, Error, << "Packages are both better than each other?"); 368 THROW_HR(E_UNEXPECTED); 369 } 370 371 if (forwardCompare && !reverseCompare) 372 { 373 AICLI_LOG(Core, Verbose, << "Package " << first.PackageId << " is better than " << second.PackageId); 374 return true; 375 } 376 } 377 378 AICLI_LOG(Core, Verbose, << "Package " << first.PackageId << " is equivalent in priority to " << second.PackageId); 379 return false; 380 } 381 382 private: 383 void AddComparator(std::unique_ptr<DisplayCatalogPackageComparisonField>&& comparator) 384 { 385 if (comparator) 386 { 387 m_comparators.emplace_back(std::move(comparator)); 388 } 389 } 390 391 std::vector<std::unique_ptr<DisplayCatalogPackageComparisonField>> m_comparators; 392 }; 393 } 394 395 // Display catalog API invocation and handling 396 397 utility::string_t GetDisplayCatalogRestApi(std::string_view productId, std::string_view locale) 398 { 399 std::vector<Utility::LocIndString> locales; 400 if (!locale.empty()) 401 { 402 locales.emplace_back(locale); 403 } 404 else 405 { 406 for (auto const& localeEntry : Locale::GetUserPreferredLanguages()) 407 { 408 locales.emplace_back(localeEntry); 409 } 410 } 411 412 // Neutral is always added 413 locales.emplace_back(Neutral); 414 415 auto restEndpoint = AppInstaller::Utility::Format(std::string{ DisplayCatalogRestApi }, 416 productId, Details, AppInstaller::Runtime::GetOSRegion(), Utility::Join(Utility::LocIndView(","), locales), TargetCatalogId); 417 418 return JSON::GetUtilityString(restEndpoint); 419 } 420 421 // Response format: 422 // { 423 // "Product": { 424 // "DisplaySkuAvailabilities": [ 425 // { 426 // "Sku": { 427 // "SkuId": "0015", 428 // ... Sku Contents ... 429 // } 430 // } 431 // ] 432 // } 433 // } 434 std::reference_wrapper<const web::json::value> GetSkuNodeFromDisplayCatalogResponse(const web::json::value& responseObject) 435 { 436 AICLI_LOG(Core, Info, << "Started parsing display catalog response. Try to find target sku: " << TargetSkuIdValue); 437 438 if (responseObject.is_null()) 439 { 440 AICLI_LOG(Core, Error, << "Missing DisplayCatalog Response json object."); 441 THROW_HR(APPINSTALLER_CLI_ERROR_DISPLAYCATALOG_API_FAILED); 442 } 443 444 std::optional<std::reference_wrapper<const web::json::value>> product = JSON::GetJsonValueFromNode(responseObject, JSON::GetUtilityString(Product)); 445 if (!product) 446 { 447 AICLI_LOG(Core, Error, << "Missing Product node"); 448 THROW_HR(APPINSTALLER_CLI_ERROR_DISPLAYCATALOG_API_FAILED); 449 } 450 451 auto skuEntries = JSON::GetRawJsonArrayFromJsonNode(product.value().get(), JSON::GetUtilityString(DisplaySkuAvailabilities)); 452 if (!skuEntries) 453 { 454 AICLI_LOG(Core, Error, << "Missing DisplaySkuAvailabilities"); 455 THROW_HR(APPINSTALLER_CLI_ERROR_DISPLAYCATALOG_API_FAILED); 456 } 457 458 for (const auto& skuEntry : skuEntries.value().get()) 459 { 460 std::optional<std::reference_wrapper<const web::json::value>> sku = JSON::GetJsonValueFromNode(skuEntry, JSON::GetUtilityString(Sku)); 461 if (!sku) 462 { 463 AICLI_LOG(Core, Error, << "Missing Sku"); 464 THROW_HR(APPINSTALLER_CLI_ERROR_DISPLAYCATALOG_API_FAILED); 465 } 466 467 const auto& skuValue = sku.value().get(); 468 auto skuId = JSON::GetRawStringValueFromJsonNode(skuValue, JSON::GetUtilityString(SkuId)).value_or(""); 469 if (TargetSkuIdValue == skuId) 470 { 471 AICLI_LOG(Core, Info, << "Target Sku (" << TargetSkuIdValue << ") found"); 472 return skuValue; 473 } 474 } 475 476 AICLI_LOG(Core, Error, << "Target Sku (" << TargetSkuIdValue << ") not found"); 477 THROW_HR(APPINSTALLER_CLI_ERROR_NO_APPLICABLE_DISPLAYCATALOG_PACKAGE); 478 } 479 480 // Response format: 481 // { 482 // "Sku": { 483 // "Properties": { 484 // "Packages": [ 485 // { 486 // "PackageId": "package id", 487 // "Architectures": [ "x86", "x64" ], 488 // "Languages": [ "en", "fr" ], 489 // "PackageFormat": "AppxBundle", 490 // "ContentId": "guid", 491 // "FulfillmentData": { 492 // "WuCategoryId": "guid", 493 // } 494 // } 495 // ] 496 // } 497 // } 498 // } 499 std::vector<DisplayCatalogPackage> GetDisplayCatalogPackagesFromSkuNode(const web::json::value& jsonObject) 500 { 501 AICLI_LOG(Core, Info, << "Started extracting display catalog packages from sku."); 502 503 std::optional<std::reference_wrapper<const web::json::value>> properties = JSON::GetJsonValueFromNode(jsonObject, JSON::GetUtilityString(Properties)); 504 if (!properties) 505 { 506 AICLI_LOG(Core, Error, << "Missing Properties"); 507 THROW_HR(APPINSTALLER_CLI_ERROR_DISPLAYCATALOG_API_FAILED); 508 } 509 510 const auto& propertiesValue = properties.value().get(); 511 auto packages = JSON::GetRawJsonArrayFromJsonNode(propertiesValue, JSON::GetUtilityString(Packages)); 512 if (!packages) 513 { 514 AICLI_LOG(Core, Error, << "Missing Packages"); 515 THROW_HR(APPINSTALLER_CLI_ERROR_DISPLAYCATALOG_API_FAILED); 516 } 517 518 std::vector<DisplayCatalogPackage> displayCatalogPackages; 519 520 for (const auto& packageEntry : packages.value().get()) 521 { 522 DisplayCatalogPackage catalogPackage; 523 524 // Package Id 525 catalogPackage.PackageId = JSON::GetRawStringValueFromJsonNode(packageEntry, JSON::GetUtilityString(PackageId)).value_or(""); 526 // Architectures 527 auto architectures = JSON::GetRawStringArrayFromJsonNode(packageEntry, JSON::GetUtilityString(Architectures)); 528 for (const auto& arch : architectures) 529 { 530 auto archEnum = Utility::ConvertToArchitectureEnum(arch); 531 if (archEnum != Utility::Architecture::Unknown) 532 { 533 catalogPackage.Architectures.emplace_back(archEnum); 534 } 535 } 536 // Languages 537 auto languages = JSON::GetRawStringArrayFromJsonNode(packageEntry, JSON::GetUtilityString(Languages)); 538 for (const auto& language : languages) 539 { 540 catalogPackage.Languages.emplace_back(language); 541 } 542 // Package Format 543 auto packageFormat = JSON::GetRawStringValueFromJsonNode(packageEntry, JSON::GetUtilityString(PackageFormat)).value_or(""); 544 catalogPackage.PackageFormat = ConvertToPackageFormatEnum(packageFormat); 545 // Content Id 546 catalogPackage.ContentId = JSON::GetRawStringValueFromJsonNode(packageEntry, JSON::GetUtilityString(ContentId)).value_or(""); 547 if (catalogPackage.ContentId.empty()) 548 { 549 AICLI_LOG(Core, Warning, << "Missing ContentId"); 550 // ContentId is required for licensing. Skip this package if missing. 551 continue; 552 } 553 // WuCategoryId 554 std::optional<std::reference_wrapper<const web::json::value>> fulfillmentData = JSON::GetJsonValueFromNode(packageEntry, JSON::GetUtilityString(FulfillmentData)); 555 if (!fulfillmentData) 556 { 557 AICLI_LOG(Core, Warning, << "Missing FulfillmentData"); 558 // WuCategoryId is required for sfs-client. Skip this package if missing. 559 continue; 560 } 561 catalogPackage.WuCategoryId = JSON::GetRawStringValueFromJsonNode(fulfillmentData.value().get(), JSON::GetUtilityString(WuCategoryId)).value_or(""); 562 if (catalogPackage.WuCategoryId.empty()) 563 { 564 AICLI_LOG(Core, Warning, << "Missing WuCategoryId"); 565 // WuCategoryId is required for sfs-client. Skip this package if missing. 566 continue; 567 } 568 569 displayCatalogPackages.emplace_back(std::move(catalogPackage)); 570 } 571 572 return displayCatalogPackages; 573 } 574 575 DisplayCatalogPackage CallDisplayCatalogAndGetPreferredPackage(std::string_view productId, std::string_view locale, Utility::Architecture architecture, const Http::HttpClientHelper::HttpRequestHeaders& authHeaders) 576 { 577 AICLI_LOG(Core, Info, << "CallDisplayCatalogAndGetPreferredPackage with ProductId: " << productId << " Locale: " << locale << " Architecture: " << Utility::ToString(architecture)); 578 579 auto displayCatalogApi = GetDisplayCatalogRestApi(productId, locale); 580 581 AppInstaller::Http::HttpClientHelper httpClientHelper; 582 583 #ifndef AICLI_DISABLE_TEST_HOOKS 584 if (TestHooks::s_DisplayCatalog_HttpPipelineStage_Override) 585 { 586 httpClientHelper = AppInstaller::Http::HttpClientHelper{ TestHooks::s_DisplayCatalog_HttpPipelineStage_Override }; 587 } 588 #endif 589 590 std::optional<web::json::value> displayCatalogResponseObject = httpClientHelper.HandleGet(displayCatalogApi, {}, authHeaders); 591 592 if (!displayCatalogResponseObject) 593 { 594 AICLI_LOG(Core, Error, << "No display catalog json object found"); 595 THROW_HR(APPINSTALLER_CLI_ERROR_DISPLAYCATALOG_API_FAILED); 596 } 597 598 const auto& sku = GetSkuNodeFromDisplayCatalogResponse(displayCatalogResponseObject.value()); 599 auto displayCatalogPackages = GetDisplayCatalogPackagesFromSkuNode(sku.get()); 600 601 DisplayCatalogPackageComparison::DisplayCatalogPackageComparator packageComparator{ std::string{ locale }, architecture }; 602 auto preferredPackageResult = packageComparator.GetPreferredPackage(displayCatalogPackages); 603 604 if (!preferredPackageResult) 605 { 606 AICLI_LOG(Core, Error, 607 << "No applicable display catalog package found for ProductId: " << productId 608 << " , Locale: " << locale << " , Architecture: " << Utility::ToString(architecture)); 609 610 THROW_HR(APPINSTALLER_CLI_ERROR_NO_APPLICABLE_DISPLAYCATALOG_PACKAGE); 611 } 612 613 auto preferredPackage = preferredPackageResult.value(); 614 615 AICLI_LOG(Core, Info, 616 << "DisplayCatalog package selected. WuCategoryId: " << preferredPackage.WuCategoryId 617 << " , ContentId: " << preferredPackage.ContentId); 618 619 return preferredPackage; 620 } 621 } 622 623 #ifndef WINGET_DISABLE_FOR_FUZZING 624 namespace SfsClientDetails 625 { 626 const std::string SupportedFileTypes[] = { ".msix", ".msixbundle", ".appx", ".appxbundle" }; 627 628 Manifest::PlatformEnum ConvertFromSfsPlatform(std::string_view applicability) 629 { 630 if (Utility::CaseInsensitiveStartsWith(applicability, "universal")) 631 { 632 return Manifest::PlatformEnum::Universal; 633 } 634 else if (Utility::CaseInsensitiveStartsWith(applicability, "desktop")) 635 { 636 return Manifest::PlatformEnum::Desktop; 637 } 638 else if (Utility::CaseInsensitiveStartsWith(applicability, "iot")) 639 { 640 return Manifest::PlatformEnum::IoT; 641 } 642 else if (Utility::CaseInsensitiveStartsWith(applicability, "analog")) 643 { 644 return Manifest::PlatformEnum::Holographic; 645 } 646 else if (Utility::CaseInsensitiveStartsWith(applicability, "ppi")) 647 { 648 return Manifest::PlatformEnum::Team; 649 } 650 651 return Manifest::PlatformEnum::Unknown; 652 } 653 654 // Parses a string of the form `<PLATFORM>=<MINIMUM REQUIRED VERSION>{,}?`. 655 struct PlatformApplicability 656 { 657 explicit PlatformApplicability(std::string_view input, bool extractVersion = true) : 658 Platform(ConvertFromSfsPlatform(input)) 659 { 660 if (extractVersion) 661 { 662 THROW_HR_IF(E_INVALIDARG, input.empty()); 663 664 size_t position = input.find('='); 665 THROW_HR_IF(E_INVALIDARG, std::string_view::npos == position); 666 667 position += 1; 668 size_t length = input.size() - position; 669 if (length > 0 && input.back() == ',') 670 { 671 length -= 1; 672 } 673 674 MinimumVersion = Utility::UInt64Version{ std::string{ input.substr(position, length) } }; 675 } 676 } 677 678 Manifest::PlatformEnum Platform; 679 std::optional<Utility::UInt64Version> MinimumVersion; 680 }; 681 682 Utility::Architecture ConvertFromSfsArchitecture(SFS::Architecture sfsArchitecture) 683 { 684 switch (sfsArchitecture) 685 { 686 case SFS::Architecture::Amd64: 687 return Utility::Architecture::X64; 688 case SFS::Architecture::x86: 689 return Utility::Architecture::X86; 690 case SFS::Architecture::Arm64: 691 return Utility::Architecture::Arm64; 692 case SFS::Architecture::Arm: 693 return Utility::Architecture::Arm; 694 case SFS::Architecture::None: 695 return Utility::Architecture::Neutral; 696 } 697 698 return Utility::Architecture::Unknown; 699 } 700 701 std::vector<Manifest::PlatformEnum> GetSfsPackageFileSupportedPlatforms( 702 const SFS::AppFile& appFile, 703 Manifest::PlatformEnum requiredPlatform, 704 const std::optional<Utility::UInt64Version>& targetOSVersion) 705 { 706 std::vector<Manifest::PlatformEnum> supportedPlatforms; 707 708 for (auto const& applicability : appFile.GetApplicabilityDetails().GetPlatformApplicabilityForPackage()) 709 { 710 AICLI_LOG(Core, Verbose, << " examining platform [" << applicability << "] for applicability..."); 711 PlatformApplicability platform(applicability, targetOSVersion.has_value()); 712 713 if (platform.Platform == Manifest::PlatformEnum::Unknown) 714 { 715 AICLI_LOG(Core, Verbose, << " not applicable due to unknown platform"); 716 continue; 717 } 718 719 if (platform.MinimumVersion && targetOSVersion) 720 { 721 if (targetOSVersion.value() < platform.MinimumVersion.value()) 722 { 723 AICLI_LOG(Core, Verbose, << " not applicable due to OS version; target [" 724 << targetOSVersion.value().ToString() << "] is lower than minimum [" 725 << platform.MinimumVersion.value().ToString() << "]"); 726 continue; 727 } 728 } 729 730 if (platform.Platform == requiredPlatform || requiredPlatform == Manifest::PlatformEnum::Unknown) 731 { 732 AICLI_LOG(Core, Verbose, << " applicable"); 733 supportedPlatforms.emplace_back(platform.Platform); 734 } 735 else 736 { 737 AICLI_LOG(Core, Verbose, << " not applicable due to platform requirement"); 738 } 739 } 740 741 return supportedPlatforms; 742 } 743 744 std::vector<Utility::Architecture> GetSfsPackageFileSupportedArchitectures(const SFS::AppFile& appFile, Utility::Architecture requiredArchitecture) 745 { 746 std::vector<Utility::Architecture> supportedArchitectures; 747 748 for (auto const& sfsArchitecture : appFile.GetApplicabilityDetails().GetArchitectures()) 749 { 750 auto convertedArchitecture = ConvertFromSfsArchitecture(sfsArchitecture); 751 if (convertedArchitecture == Utility::Architecture::Unknown) 752 { 753 continue; 754 } 755 756 if (requiredArchitecture == Utility::Architecture::Unknown || // No required architecture 757 convertedArchitecture == requiredArchitecture) 758 { 759 supportedArchitectures.emplace_back(convertedArchitecture); 760 } 761 } 762 763 return supportedArchitectures; 764 } 765 766 std::string GetSfsPackageFileExtension(const SFS::AppFile& appFile) 767 { 768 return std::filesystem::path{ appFile.GetFileId() }.extension().u8string(); 769 } 770 771 bool IsFileExtensionSupported(std::string_view fileExtension) 772 { 773 for (auto const& supportedFileType : SupportedFileTypes) 774 { 775 if (Utility::CaseInsensitiveEquals(supportedFileType, fileExtension)) 776 { 777 return true; 778 } 779 } 780 781 return false; 782 } 783 784 // The file name will be {Name}_{Version}_{Platform list}_{Arch list}.{File Extension} 785 // If the file name is longer than 256, file moniker will be used. 786 std::string GetSfsPackageFileNameForDownload( 787 const std::string& packageName, 788 const Utility::UInt64Version& packageVersion, 789 const std::vector<Manifest::PlatformEnum>& supportedPlatforms, 790 const std::vector<Utility::Architecture>& supportedArchitectures, 791 const std::string& fileExtension, 792 const std::string& fileMoniker) 793 { 794 std::string platformString; 795 for (auto platform : supportedPlatforms) 796 { 797 platformString += std::string{ Manifest::PlatformToString(platform, true) } + '.'; 798 } 799 platformString.resize(platformString.size() - 1); 800 801 std::string architectureString; 802 for (auto architecture : supportedArchitectures) 803 { 804 architectureString += std::string{ Utility::ToString(architecture) } + '.'; 805 } 806 architectureString.resize(architectureString.size() - 1); 807 808 std::string fileName = 809 packageName + '_' + 810 packageVersion.ToString() + '_' + 811 platformString + '_' + 812 architectureString + 813 fileExtension; 814 815 if (fileName.length() < 256) 816 { 817 return fileName; 818 } 819 else 820 { 821 return fileMoniker + fileExtension; 822 } 823 } 824 825 void SfsClientLoggingCallback(const SFS::LogData& logData) 826 { 827 std::string message = "Message: " + std::string{ logData.message }; 828 message += " File: " + std::string{ logData.file }; 829 message += " Line: " + std::to_string(logData.line); 830 message += " Function: " + std::string{ logData.function }; 831 832 switch (logData.severity) 833 { 834 case SFS::LogSeverity::Verbose: 835 AICLI_LOG(Core, Verbose, << message); 836 break; 837 case SFS::LogSeverity::Info: 838 AICLI_LOG(Core, Info, << message); 839 break; 840 case SFS::LogSeverity::Warning: 841 AICLI_LOG(Core, Warning, << message); 842 break; 843 case SFS::LogSeverity::Error: 844 AICLI_LOG(Core, Error, << message); 845 break; 846 } 847 } 848 849 const std::unique_ptr<SFS::SFSClient>& GetSfsClientInstance() 850 { 851 static std::unique_ptr<SFS::SFSClient> s_sfsClient; 852 static std::once_flag s_sfsClientInitializeOnce; 853 854 std::call_once(s_sfsClientInitializeOnce, 855 [&]() 856 { 857 SFS::ClientConfig config; 858 config.accountId = "storeapps"; 859 config.instanceId = "storeapps"; 860 config.logCallbackFn = SfsClientLoggingCallback; 861 862 auto result = SFS::SFSClient::Make(config, s_sfsClient); 863 if (!result) 864 { 865 AICLI_LOG(Core, Error, << "Failed to initialize SfsClient. Error code: " << result.GetCode() << " Message: " << result.GetMsg()); 866 THROW_HR_MSG(APPINSTALLER_CLI_ERROR_SFSCLIENT_API_FAILED, "Failed to initialize SfsClient. ErrorCode: %lu Message: %hs", result.GetCode(), result.GetMsg().c_str()); 867 } 868 }); 869 870 return s_sfsClient; 871 } 872 873 std::vector<MSStoreDownloadFile> PopulateSfsAppFileToMSStoreDownloadFileVector( 874 const std::vector<SFS::AppFile>& appFiles, 875 Utility::Architecture requiredArchitecture = Utility::Architecture::Unknown, 876 Manifest::PlatformEnum requiredPlatform = Manifest::PlatformEnum::Unknown, 877 const std::optional<Utility::UInt64Version>& targetOSVersion = std::nullopt) 878 { 879 using PlatformAndArchitectureKey = std::pair<Manifest::PlatformEnum, Utility::Architecture>; 880 881 // Since the server may return multiple versions of the same package, we'll use this map to record the one with latest version 882 // for each Platform|Architecture pair. 883 std::map<PlatformAndArchitectureKey, MSStoreDownloadFile> downloadFilesMap; 884 885 for (auto const& appFile : appFiles) 886 { 887 AICLI_LOG(Core, Info, << "Examining package [" << appFile.GetFileMoniker() << " (" << appFile.GetFileId() << ")] for download..."); 888 889 // Filter out unsupported packages 890 auto supportedPlatforms = GetSfsPackageFileSupportedPlatforms(appFile, requiredPlatform, targetOSVersion); 891 if (supportedPlatforms.empty()) 892 { 893 AICLI_LOG(Core, Verbose, << " package has no applicable platform."); 894 continue; 895 } 896 auto supportedArchitectures = GetSfsPackageFileSupportedArchitectures(appFile, requiredArchitecture); 897 if (supportedArchitectures.empty()) 898 { 899 AICLI_LOG(Core, Verbose, << " package has no applicable architecture."); 900 continue; 901 } 902 std::string fileExtension = GetSfsPackageFileExtension(appFile); 903 if (!IsFileExtensionSupported(fileExtension)) 904 { 905 AICLI_LOG(Core, Verbose, << " package has unsupported file type [" << fileExtension << "]."); 906 continue; 907 } 908 909 MSStoreDownloadFile downloadFile; 910 downloadFile.Url = appFile.GetUrl(); 911 // The sha256 hash was base64 encoded 912 downloadFile.Sha256 = JSON::Base64Decode(appFile.GetHashes().at(SFS::HashType::Sha256)); 913 auto packageInfo = Msix::GetPackageIdInfoFromFullName(appFile.GetFileMoniker()); 914 downloadFile.Version = packageInfo.Version; 915 downloadFile.FileName = GetSfsPackageFileNameForDownload( 916 packageInfo.Name, packageInfo.Version, supportedPlatforms, 917 supportedArchitectures, fileExtension, appFile.GetFileMoniker()); 918 919 // Update the platform architecture map with latest package if applicable 920 for (auto supportedPlatform : supportedPlatforms) 921 { 922 for (auto supportedArchitecture : supportedArchitectures) 923 { 924 PlatformAndArchitectureKey downloadFileKey{ supportedPlatform, supportedArchitecture }; 925 if (downloadFile.Version > downloadFilesMap[downloadFileKey].Version) 926 { 927 downloadFilesMap[downloadFileKey] = downloadFile; 928 } 929 } 930 } 931 } 932 933 // Generate MSStoreDownloadFile vector and remove duplication. 934 std::vector<MSStoreDownloadFile> result; 935 for (auto& downloadFileEntry : downloadFilesMap) 936 { 937 if (std::find_if(result.begin(), result.end(), 938 [&](const MSStoreDownloadFile& downloadFile) 939 { 940 return Utility::CaseInsensitiveEquals(downloadFile.FileName, downloadFileEntry.second.FileName); 941 }) == result.end()) 942 { 943 result.emplace_back(std::move(downloadFileEntry.second)); 944 } 945 } 946 947 return result; 948 } 949 950 MSStoreDownloadInfo CallSfsClientAndGetMSStoreDownloadInfo( 951 std::string_view wuCategoryId, 952 Utility::Architecture requiredArchitecture, 953 Manifest::PlatformEnum requiredPlatform, 954 const std::optional<Utility::UInt64Version>& targetOSVersion) 955 { 956 AICLI_LOG(Core, Info, << "CallSfsClientAndGetMSStoreDownloadInfo with WuCategoryId: " << wuCategoryId 957 << " Architecture: " << Utility::ToString(requiredArchitecture) << " Platform: " << Manifest::PlatformToString(requiredPlatform) 958 << " Target OS Version: " << (targetOSVersion ? targetOSVersion.value().ToString() : "any")); 959 960 std::vector<SFS::AppContent> appContents; 961 962 #ifndef AICLI_DISABLE_TEST_HOOKS 963 if (TestHooks::s_SfsClient_AppContents_Override) 964 { 965 appContents = (*TestHooks::s_SfsClient_AppContents_Override)(wuCategoryId); 966 } 967 else 968 #endif 969 { 970 SFS::RequestParams sfsClientRequest; 971 sfsClientRequest.productRequests = { {std::string{ wuCategoryId }, {}} }; 972 const auto& proxyUri = AppInstaller::Settings::Network().GetProxyUri(); 973 if (proxyUri) 974 { 975 AICLI_LOG(Core, Info, << "Passing proxy to SFS client " << *proxyUri); 976 sfsClientRequest.proxy = *proxyUri; 977 } 978 979 auto requestResult = GetSfsClientInstance()->GetLatestAppDownloadInfo(sfsClientRequest, appContents); 980 if (!requestResult) 981 { 982 if (requestResult.GetCode() == SFS::Result::Code::HttpNotFound) 983 { 984 AICLI_LOG(Core, Error, << "Failed to call SfsClient GetLatestAppDownloadInfo. Package not found."); 985 THROW_HR_MSG(APPINSTALLER_CLI_ERROR_SFSCLIENT_PACKAGE_NOT_SUPPORTED, "Failed to call SfsClient GetLatestAppDownloadInfo. Package download not supported."); 986 } 987 else 988 { 989 AICLI_LOG(Core, Error, << "Failed to call SfsClient GetLatestAppDownloadInfo. Error code: " << requestResult.GetCode() << " Message: " << requestResult.GetMsg()); 990 THROW_HR_MSG(APPINSTALLER_CLI_ERROR_SFSCLIENT_API_FAILED, "Failed to call SfsClient GetLatestAppDownloadInfo. ErrorCode: %lu Message: %hs", requestResult.GetCode(), requestResult.GetMsg().c_str()); 991 } 992 } 993 } 994 995 THROW_HR_IF(E_UNEXPECTED, appContents.empty()); 996 997 MSStoreDownloadInfo result; 998 // Currently for app downloads, the result vector is always size 1. 999 const auto& appContent = appContents.at(0); 1000 1001 // Populate main packages 1002 result.MainPackages = PopulateSfsAppFileToMSStoreDownloadFileVector(appContent.GetFiles(), requiredArchitecture, requiredPlatform, targetOSVersion); 1003 1004 // Populate dependency packages 1005 for (auto const& dependencyEntry : appContent.GetPrerequisites()) 1006 { 1007 // Not passing in required platform for dependencies. Dependencies are mostly Windows.Universal. 1008 auto dependencyPackages = PopulateSfsAppFileToMSStoreDownloadFileVector(dependencyEntry.GetFiles(), requiredArchitecture, Manifest::PlatformEnum::Unknown, targetOSVersion); 1009 std::move(dependencyPackages.begin(), dependencyPackages.end(), std::inserter(result.DependencyPackages, result.DependencyPackages.end())); 1010 } 1011 1012 if (result.MainPackages.empty()) 1013 { 1014 AICLI_LOG(Core, Error, << "No applicable SFS main package."); 1015 THROW_HR(APPINSTALLER_CLI_ERROR_NO_APPLICABLE_SFSCLIENT_PACKAGE); 1016 } 1017 1018 return result; 1019 } 1020 } 1021 #endif 1022 1023 namespace LicensingDetails 1024 { 1025 // Json response fields 1026 constexpr std::string_view License = "license"sv; 1027 constexpr std::string_view Keys = "keys"sv; 1028 constexpr std::string_view Value = "value"sv; 1029 1030 // Licensing rest endpoint 1031 constexpr std::string_view LicensingRestEndpoint = "https://licensing.md.mp.microsoft.com/v9.0/licenses/offlineContent"; 1032 constexpr std::string_view ContentId = "contentId"sv; 1033 constexpr std::string_view From = "From"sv; 1034 1035 // Response: 1036 // { 1037 // "license": { 1038 // "keys": [ // returned as array for future, for now only 1 key 1039 // { 1040 // "value": "base64 encoded string" 1041 // } 1042 // ] 1043 // } 1044 // } 1045 std::vector<BYTE> GetLicensing(std::string_view contentId, const Http::HttpClientHelper::HttpRequestHeaders& authHeaders) 1046 { 1047 AICLI_LOG(Core, Error, << "GetLicensing with ContentId: " << contentId); 1048 1049 AppInstaller::Http::HttpClientHelper httpClientHelper; 1050 1051 #ifndef AICLI_DISABLE_TEST_HOOKS 1052 if (TestHooks::s_Licensing_HttpPipelineStage_Override) 1053 { 1054 httpClientHelper = AppInstaller::Http::HttpClientHelper{ TestHooks::s_Licensing_HttpPipelineStage_Override }; 1055 } 1056 #endif 1057 1058 web::json::value requestBody; 1059 requestBody[JSON::GetUtilityString(ContentId)] = web::json::value::string(JSON::GetUtilityString(contentId)); 1060 Http::HttpClientHelper::HttpRequestHeaders requestHeaders; 1061 requestHeaders.insert_or_assign(JSON::GetUtilityString(From), L"winget-cli"); 1062 1063 std::optional<web::json::value> licensingResponseObject = std::nullopt; 1064 try 1065 { 1066 licensingResponseObject = httpClientHelper.HandlePost( 1067 JSON::GetUtilityString(LicensingRestEndpoint), requestBody, requestHeaders, authHeaders); 1068 } 1069 catch (const wil::ResultException& re) 1070 { 1071 if (re.GetErrorCode() == HTTP_E_STATUS_FORBIDDEN) 1072 { 1073 AICLI_LOG(CLI, Error, << "Getting MSStore package license failed. The Microsoft Entra Id account does not have privilege."); 1074 THROW_HR(APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED_FORBIDDEN); 1075 } 1076 else 1077 { 1078 AICLI_LOG(CLI, Error, << "Getting MSStore package license failed. Error code: " << re.GetErrorCode()); 1079 THROW_HR(re.GetErrorCode()); 1080 } 1081 } 1082 1083 if (!licensingResponseObject || licensingResponseObject->is_null()) 1084 { 1085 AICLI_LOG(Core, Error, << "Empty licensing response"); 1086 THROW_HR(APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED); 1087 } 1088 1089 std::optional<std::reference_wrapper<const web::json::value>> license = JSON::GetJsonValueFromNode(licensingResponseObject.value(), JSON::GetUtilityString(License)); 1090 if (!license) 1091 { 1092 AICLI_LOG(Core, Error, << "Missing license node"); 1093 THROW_HR(APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED); 1094 } 1095 1096 auto keys = JSON::GetRawJsonArrayFromJsonNode(license.value().get(), JSON::GetUtilityString(Keys)); 1097 if (!keys || keys->get().size() == 0) 1098 { 1099 AICLI_LOG(Core, Error, << "Missing keys or empty keys"); 1100 THROW_HR(APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED); 1101 } 1102 1103 std::string base64LicenseContent = JSON::GetRawStringValueFromJsonNode(keys->get().at(0), JSON::GetUtilityString(Value)).value_or(""); 1104 if (base64LicenseContent.empty()) 1105 { 1106 AICLI_LOG(Core, Error, << "Missing license content"); 1107 THROW_HR(APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED); 1108 } 1109 1110 return JSON::Base64Decode(base64LicenseContent); 1111 } 1112 } 1113 1114 namespace 1115 { 1116 Http::HttpClientHelper::HttpRequestHeaders GetAuthHeaders(std::unique_ptr<Authentication::Authenticator>& authenticator) 1117 { 1118 if (!authenticator) 1119 { 1120 return {}; 1121 } 1122 1123 Http::HttpClientHelper::HttpRequestHeaders result; 1124 1125 auto authResult = authenticator->AuthenticateForToken(); 1126 if (FAILED(authResult.Status)) 1127 { 1128 AICLI_LOG(Repo, Error, << "Authentication failed. Result: " << authResult.Status); 1129 THROW_HR_MSG(authResult.Status, "Failed to authenticate for MicrosoftEntraId"); 1130 } 1131 result.insert_or_assign(web::http::header_names::authorization, JSON::GetUtilityString(Authentication::CreateBearerToken(authResult.Token))); 1132 1133 return result; 1134 } 1135 } 1136 1137 MSStoreDownloadContext::MSStoreDownloadContext( 1138 std::string productId, 1139 AppInstaller::Utility::Architecture architecture, 1140 Manifest::PlatformEnum platform, 1141 std::string locale, 1142 AppInstaller::Authentication::AuthenticationArguments authArgs) : 1143 m_productId(std::move(productId)), m_architecture(architecture), m_platform(platform), m_locale(std::move(locale)) 1144 { 1145 #ifndef AICLI_DISABLE_TEST_HOOKS 1146 if (!TestHooks::s_DisplayCatalog_HttpPipelineStage_Override) 1147 #endif 1148 { 1149 Authentication::MicrosoftEntraIdAuthenticationInfo displayCatalogMicrosoftEntraIdAuthInfo; 1150 displayCatalogMicrosoftEntraIdAuthInfo.Resource = "https://bigcatalog.commerce.microsoft.com"; 1151 Authentication::AuthenticationInfo displayCatalogAuthInfo; 1152 displayCatalogAuthInfo.Type = Authentication::AuthenticationType::MicrosoftEntraId; 1153 displayCatalogAuthInfo.MicrosoftEntraIdInfo = std::move(displayCatalogMicrosoftEntraIdAuthInfo); 1154 1155 m_displayCatalogAuthenticator = std::make_unique<Authentication::Authenticator>(std::move(displayCatalogAuthInfo), authArgs); 1156 } 1157 1158 #ifndef AICLI_DISABLE_TEST_HOOKS 1159 if (!TestHooks::s_Licensing_HttpPipelineStage_Override) 1160 #endif 1161 { 1162 Authentication::MicrosoftEntraIdAuthenticationInfo licensingMicrosoftEntraIdAuthInfo; 1163 licensingMicrosoftEntraIdAuthInfo.Resource = "c5e1cb0d-5d24-4b1a-b291-ec684152b2ba"; 1164 Authentication::AuthenticationInfo licensingAuthInfo; 1165 licensingAuthInfo.Type = Authentication::AuthenticationType::MicrosoftEntraId; 1166 licensingAuthInfo.MicrosoftEntraIdInfo = std::move(licensingMicrosoftEntraIdAuthInfo); 1167 1168 m_licensingAuthenticator = std::make_unique<Authentication::Authenticator>(std::move(licensingAuthInfo), authArgs); 1169 } 1170 } 1171 1172 void MSStoreDownloadContext::TargetOSVersion(std::optional<Utility::UInt64Version> targetOSVersion) 1173 { 1174 m_targetOSVersion = std::move(targetOSVersion); 1175 } 1176 1177 MSStoreDownloadInfo MSStoreDownloadContext::GetDownloadInfo() 1178 { 1179 #ifndef WINGET_DISABLE_FOR_FUZZING 1180 auto displayCatalogPackage = DisplayCatalogDetails::CallDisplayCatalogAndGetPreferredPackage(m_productId, m_locale, m_architecture, GetAuthHeaders(m_displayCatalogAuthenticator)); 1181 auto downloadInfo = SfsClientDetails::CallSfsClientAndGetMSStoreDownloadInfo(displayCatalogPackage.WuCategoryId, m_architecture, m_platform, m_targetOSVersion); 1182 downloadInfo.ContentId = displayCatalogPackage.ContentId; 1183 return downloadInfo; 1184 #else 1185 return {}; 1186 #endif 1187 } 1188 1189 std::vector<BYTE> MSStoreDownloadContext::GetLicense(std::string_view contentId) 1190 { 1191 return LicensingDetails::GetLicensing(contentId, GetAuthHeaders(m_licensingAuthenticator)); 1192 } 1193 }