From 6ea13623e5e4b870b81efeea9142d15a98dd4208 Mon Sep 17 00:00:00 2001 From: yao-msft <50888816+yao-msft@users.noreply.github.com> Date: Mon, 30 Sep 2024 20:17:40 -0700 Subject: [PATCH] More messages for common Store package download scenarios (#4841) - Rename downloaded file to contain platform architecture info, and a message for the option to filter packages to be downloaded - Message for "package does not support download" - Make licensing errors as warning, added a specific message for "Entra Id account does not have enough access to licensing" --- .../package-manager/winget/returnCodes.md | 2 + src/AppInstallerCLICore/Resources.h | 5 +- .../Workflows/MSStoreInstallerHandler.cpp | 23 +++- .../Shared/Strings/en-us/winget.resw | 28 ++++- .../MSStoreDownloadFlow.cpp | 106 +++++++++++------- src/AppInstallerCLITests/MsixInfo.cpp | 11 +- .../MSStoreDownload.cpp | 97 +++++++++++++--- .../Manifest/ManifestCommon.cpp | 12 +- src/AppInstallerCommonCore/MsixInfo.cpp | 8 +- .../Public/AppInstallerMsixInfo.h | 10 +- .../Public/winget/ManifestCommon.h | 3 +- src/AppInstallerSharedLib/Errors.cpp | 2 + .../Public/AppInstallerErrors.h | 2 + 13 files changed, 228 insertions(+), 81 deletions(-) diff --git a/doc/windows/package-manager/winget/returnCodes.md b/doc/windows/package-manager/winget/returnCodes.md index 29486e308d..2c12ede792 100644 --- a/doc/windows/package-manager/winget/returnCodes.md +++ b/doc/windows/package-manager/winget/returnCodes.md @@ -143,6 +143,8 @@ ms.localizationpriority: medium | 0x8A150081 | -1978335103 | APPINSTALLER_CLI_ERROR_SFSCLIENT_API_FAILED | Failed to get Microsoft Store package download information. | | 0x8A150082 | -1978335102 | APPINSTALLER_CLI_ERROR_NO_APPLICABLE_SFSCLIENT_PACKAGE | No applicable Microsoft Store package download information found. | | 0x8A150083 | -1978335101 | APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED | Failed to retrieve Microsoft Store package license. | +| 0x8A150084 | -1978335100 | APPINSTALLER_CLI_ERROR_SFSCLIENT_PACKAGE_NOT_SUPPORTED | The Microsoft Store package does not support download command. | +| 0x8A150085 | -1978335099 | APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED_FORBIDDEN | Failed to retrieve Microsoft Store package license. The Microsoft Entra Id account does not have required privilege. | ## Install errors. diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 284c012577..04b932f07f 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -370,14 +370,17 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadGetDownloadInfoFailed); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadGetLicense); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadGetLicenseFailed); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadGetLicenseForbidden); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadGetLicenseSuccess); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadMainPackages); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadMultiplePackagesNotice); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadNoApplicablePackageFound); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadPackageDownloaded); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadPackageDownloadFailed); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadPackageDownloadNotSupported); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadPackageDownloadSuccess); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadPackageHashMismatch); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadPackageHashVerified); - WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadPackageNotFound); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadRenameNotSupported); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallOrUpdateFailed); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallTryGetEntitlement); diff --git a/src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.cpp index 4bb3b279ce..6a8147a086 100644 --- a/src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.cpp +++ b/src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.cpp @@ -271,6 +271,7 @@ namespace AppInstaller::CLI::Workflow // Authentication notice context.Reporter.Warn() << Resource::String::MSStoreDownloadAuthenticationNotice << std::endl; + context.Reporter.Warn() << Resource::String::MSStoreDownloadMultiplePackagesNotice << std::endl; const auto& installer = context.Get().value(); @@ -307,13 +308,16 @@ namespace AppInstaller::CLI::Workflow { case APPINSTALLER_CLI_ERROR_NO_APPLICABLE_DISPLAYCATALOG_PACKAGE: case APPINSTALLER_CLI_ERROR_NO_APPLICABLE_SFSCLIENT_PACKAGE: - context.Reporter.Error() << Resource::String::MSStoreDownloadPackageNotFound << std::endl; + context.Reporter.Error() << Resource::String::MSStoreDownloadNoApplicablePackageFound << std::endl; + break; + case APPINSTALLER_CLI_ERROR_SFSCLIENT_PACKAGE_NOT_SUPPORTED: + context.Reporter.Error() << Resource::String::MSStoreDownloadPackageDownloadNotSupported << std::endl; break; default: context.Reporter.Error() << Resource::String::MSStoreDownloadGetDownloadInfoFailed << std::endl; } - throw; + AICLI_TERMINATE_CONTEXT(re.GetErrorCode()); } bool skipDependencies = context.Args.Contains(Execution::Args::Type::SkipDependencies); @@ -368,9 +372,18 @@ namespace AppInstaller::CLI::Workflow } catch (const wil::ResultException& re) { - AICLI_LOG(CLI, Error, << "Getting MSStore package license failed. Error code: " << re.GetErrorCode()); - context.Reporter.Error() << Resource::String::MSStoreDownloadGetLicenseFailed << std::endl; - throw; + if (re.GetErrorCode() == APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED_FORBIDDEN) + { + AICLI_LOG(CLI, Warning, << "Getting MSStore package license failed. The Microsoft Entra Id account does not have privilege."); + context.Reporter.Warn() << Resource::String::MSStoreDownloadGetLicenseForbidden << std::endl; + } + else + { + AICLI_LOG(CLI, Warning, << "Getting MSStore package license failed. Error code: " << re.GetErrorCode()); + context.Reporter.Warn() << Resource::String::MSStoreDownloadGetLicenseFailed << std::endl; + } + + AICLI_TERMINATE_CONTEXT(re.GetErrorCode()); } std::filesystem::path licenseFilePath = downloadDirectory / Utility::ConvertToUTF16(installer.ProductId + "_License.xml"); diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index c1d8771680..9a6bff5c87 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -2896,13 +2896,13 @@ Please specify one of them using the --source option to proceed. Failed to get Microsoft Store package download information. - No applicable Microsoft Store package download information found. + No applicable Microsoft Store package found for download. Failed to retrieve Microsoft Store package license. - - The Microsoft Store package could not be found. + + No applicable Microsoft Store package found. Successfully verified Microsoft Store package hash @@ -2948,7 +2948,8 @@ Please specify one of them using the --source option to proceed. {Locked="--rename"} - Microsoft Store package download requires Microsoft Entra Id authentication. Authentication prompt may appear when necessary. Authenticated information will be shared with Microsoft services for access authorization. For Microsoft Store package licensing, the Microsoft Entra Id account needs to have administrator access to the Microsoft Entra Id tenant. + Microsoft Store package download requires Microsoft Entra Id authentication. Authentication prompt may appear when necessary. Authenticated information will be shared with Microsoft services for access authorization. For Microsoft Store package licensing, the Microsoft Entra Id account needs to be a member of Global Administrator, User Administrator, or License Administrator. + {Locked="Global Administrator,User Administrator,License Administrator"} Skips retrieving Microsoft Store package offline license @@ -3112,4 +3113,21 @@ Please specify one of them using the --source option to proceed. Suppress showing initial configuration details when possible - \ No newline at end of file + + Multiple Microsoft Store packages may be downloaded targeting different platforms and architectures. --platform, --architecture can be used to filter the packages. + {Locked="--platform--architecture"} + + + Failed to retrieve Microsoft Store package license. The Microsoft Entra Id account is not a member of Global Administrator, User Administrator, or License Administrator. Use --skip-license to skip retrieving Microsoft Store package license. + {Locked="Global Administrator,User Administrator,License Administrator,--skip-license"} + + + The Microsoft Store package does not support download. + + + The Microsoft Store package does not support download. + + + Failed to retrieve Microsoft Store package license. The Microsoft Entra Id account does not have required privilege. + + diff --git a/src/AppInstallerCLITests/MSStoreDownloadFlow.cpp b/src/AppInstallerCLITests/MSStoreDownloadFlow.cpp index 53fa34a8a9..793e64179d 100644 --- a/src/AppInstallerCLITests/MSStoreDownloadFlow.cpp +++ b/src/AppInstallerCLITests/MSStoreDownloadFlow.cpp @@ -212,7 +212,7 @@ std::vector GetSfsAppContentsOverrideFunction(std::string_view "https://NotUsed/" + wuCategoryIdStr + "/Xbox/arm", 100, { { SFS::HashType::Sha256, base64EncodedSha256 } }, - { SFS::Architecture::Amd64 }, + { SFS::Architecture::Arm }, { "Xbox=10.0.0.0" }, wuCategoryIdStr + ".Xbox_1.0.0.0_arm__8wekyb3d8bbwe", packageXbox); @@ -225,7 +225,7 @@ std::vector GetSfsAppContentsOverrideFunction(std::string_view "https://NotUsed/" + wuCategoryIdStr + "/cab", 100, { { SFS::HashType::Sha256, base64EncodedSha256 } }, - { SFS::Architecture::Amd64 }, + { SFS::Architecture::Arm }, { "Desktop=10.0.0.0" }, wuCategoryIdStr + ".Data_1.0.0.0_arm__8wekyb3d8bbwe", packageData); @@ -263,11 +263,11 @@ TEST_CASE("MSStoreDownloadFlow_Success", "[MSStoreDownloadFlow][workflow]") // Verify downloaded files REQUIRE(std::filesystem::exists(tempDirectory.GetPath())); REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_1.2.3.4_x64__8wekyb3d8bbwe.appx")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_1.2.3.4_arm__8wekyb3d8bbwe.appx")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_1.0.0.0_x64__8wekyb3d8bbwe.appx")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_1.0.0.0_arm__8wekyb3d8bbwe.appx")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish.IoT_2.0.0.0_arm__8wekyb3d8bbwe.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_1.2.3.4_Universal_X64.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_1.2.3.4_Universal_Arm.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_1.0.0.0_Desktop_X64.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_1.0.0.0_Desktop_Arm.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish.IoT_2.0.0.0_IoT_Arm.appx")); // Verify license REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"9WZDNCRFJ364_License.xml")); @@ -278,9 +278,9 @@ TEST_CASE("MSStoreDownloadFlow_Success", "[MSStoreDownloadFlow][workflow]") REQUIRE(licenseFileStr == LicenseContent); // Verify unsupported packages filtered out - REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish.IoT_1.0.0.0_arm__8wekyb3d8bbwe.appx")); - REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish.Xbox_1.0.0.0_arm__8wekyb3d8bbwe.appx")); - REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish.Data_1.0.0.0_arm__8wekyb3d8bbwe.cab")); + REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish.IoT_1.0.0.0_IoT_Arm.appx")); + REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish.Xbox_1.0.0.0_Xbox_Arm.appx")); + REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish.Data_1.0.0.0_Desktop_Arm.cab")); } TEST_CASE("MSStoreDownloadFlow_Success_SkipDependencies", "[MSStoreDownloadFlow][workflow]") @@ -307,11 +307,11 @@ TEST_CASE("MSStoreDownloadFlow_Success_SkipDependencies", "[MSStoreDownloadFlow] // Verify downloaded files REQUIRE(std::filesystem::exists(tempDirectory.GetPath())); REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies")); - REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_1.2.3.4_x64__8wekyb3d8bbwe.appx")); - REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_1.2.3.4_arm__8wekyb3d8bbwe.appx")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_1.0.0.0_x64__8wekyb3d8bbwe.appx")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_1.0.0.0_arm__8wekyb3d8bbwe.appx")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish.IoT_2.0.0.0_arm__8wekyb3d8bbwe.appx")); + REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_1.2.3.4_Universal_X64.appx")); + REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_1.2.3.4_Universal_Arm.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_1.0.0.0_Desktop_X64.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_1.0.0.0_Desktop_Arm.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish.IoT_2.0.0.0_IoT_Arm.appx")); // Verify license REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"9WZDNCRFJ364_License.xml")); @@ -346,11 +346,11 @@ TEST_CASE("MSStoreDownloadFlow_Success_SkipLicense", "[MSStoreDownloadFlow][work // Verify downloaded files REQUIRE(std::filesystem::exists(tempDirectory.GetPath())); REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_1.2.3.4_x64__8wekyb3d8bbwe.appx")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_1.2.3.4_arm__8wekyb3d8bbwe.appx")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_1.0.0.0_x64__8wekyb3d8bbwe.appx")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_1.0.0.0_arm__8wekyb3d8bbwe.appx")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish.IoT_2.0.0.0_arm__8wekyb3d8bbwe.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_1.2.3.4_Universal_X64.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_1.2.3.4_Universal_Arm.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_1.0.0.0_Desktop_X64.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_1.0.0.0_Desktop_Arm.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish.IoT_2.0.0.0_IoT_Arm.appx")); // Verify license REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"9WZDNCRFJ364_License.xml")); @@ -379,11 +379,11 @@ TEST_CASE("MSStoreDownloadFlow_Success_SpecificLocale", "[MSStoreDownloadFlow][w // Verify downloaded files REQUIRE(std::filesystem::exists(tempDirectory.GetPath())); REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdFrench.Dependency_1.2.3.4_x64__8wekyb3d8bbwe.appx")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdFrench.Dependency_1.2.3.4_arm__8wekyb3d8bbwe.appx")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdFrench_1.0.0.0_x64__8wekyb3d8bbwe.appx")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdFrench_1.0.0.0_arm__8wekyb3d8bbwe.appx")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdFrench.IoT_2.0.0.0_arm__8wekyb3d8bbwe.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdFrench.Dependency_1.2.3.4_Universal_X64.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdFrench.Dependency_1.2.3.4_Universal_Arm.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdFrench_1.0.0.0_Desktop_X64.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdFrench_1.0.0.0_Desktop_Arm.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdFrench.IoT_2.0.0.0_IoT_Arm.appx")); // Verify license REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"9WZDNCRFJ364_License.xml")); @@ -418,11 +418,11 @@ TEST_CASE("MSStoreDownloadFlow_Success_SpecificArchitecture", "[MSStoreDownloadF // Verify downloaded files REQUIRE(std::filesystem::exists(tempDirectory.GetPath())); REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_1.2.3.4_x64__8wekyb3d8bbwe.appx")); - REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_1.2.3.4_arm__8wekyb3d8bbwe.appx")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_1.0.0.0_x64__8wekyb3d8bbwe.appx")); - REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_1.0.0.0_arm__8wekyb3d8bbwe.appx")); - REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish.IoT_2.0.0.0_arm__8wekyb3d8bbwe.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_1.2.3.4_Universal_x64.appx")); + REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_1.2.3.4_Universal_Arm.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_1.0.0.0_Desktop_X64.appx")); + REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_1.0.0.0_Desktop_Arm.appx")); + REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish.IoT_2.0.0.0_IoT_Arm.appx")); // Verify license REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"9WZDNCRFJ364_License.xml")); @@ -457,11 +457,11 @@ TEST_CASE("MSStoreDownloadFlow_Success_SpecificPlatform", "[MSStoreDownloadFlow] // Verify downloaded files REQUIRE(std::filesystem::exists(tempDirectory.GetPath())); REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_1.2.3.4_x64__8wekyb3d8bbwe.appx")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_1.2.3.4_arm__8wekyb3d8bbwe.appx")); - REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_1.0.0.0_x64__8wekyb3d8bbwe.appx")); - REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_1.0.0.0_arm__8wekyb3d8bbwe.appx")); - REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish.IoT_2.0.0.0_arm__8wekyb3d8bbwe.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_1.2.3.4_Universal_X64.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_1.2.3.4_Universal_Arm.appx")); + REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_1.0.0.0_Desktop_X64.appx")); + REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_1.0.0.0_Desktop_Arm.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish.IoT_2.0.0.0_IoT_Arm.appx")); // Verify license REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"9WZDNCRFJ364_License.xml")); @@ -487,7 +487,8 @@ TEST_CASE("MSStoreDownloadFlow_Fail_TargetSkuNotFound", "[MSStoreDownloadFlow][w context.Args.AddArg(Execution::Args::Type::Locale, "en-US"sv); DownloadCommand download({}); - REQUIRE_THROWS_HR(download.Execute(context), APPINSTALLER_CLI_ERROR_NO_APPLICABLE_DISPLAYCATALOG_PACKAGE); + download.Execute(context); + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_NO_APPLICABLE_DISPLAYCATALOG_PACKAGE); INFO(downloadOutput.str()); } @@ -506,7 +507,8 @@ TEST_CASE("MSStoreDownloadFlow_Fail_LocaleNotApplicable", "[MSStoreDownloadFlow] context.Args.AddArg(Execution::Args::Type::Locale, "ja-JP"sv); DownloadCommand download({}); - REQUIRE_THROWS_HR(download.Execute(context), APPINSTALLER_CLI_ERROR_NO_APPLICABLE_DISPLAYCATALOG_PACKAGE); + download.Execute(context); + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_NO_APPLICABLE_DISPLAYCATALOG_PACKAGE); INFO(downloadOutput.str()); } @@ -526,7 +528,8 @@ TEST_CASE("MSStoreDownloadFlow_Fail_ArchitectureNotApplicable", "[MSStoreDownloa context.Args.AddArg(Execution::Args::Type::InstallArchitecture, "arm64"sv); DownloadCommand download({}); - REQUIRE_THROWS_HR(download.Execute(context), APPINSTALLER_CLI_ERROR_NO_APPLICABLE_DISPLAYCATALOG_PACKAGE); + download.Execute(context); + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_NO_APPLICABLE_DISPLAYCATALOG_PACKAGE); INFO(downloadOutput.str()); } @@ -546,7 +549,8 @@ TEST_CASE("MSStoreDownloadFlow_Fail_PlatformNotApplicable", "[MSStoreDownloadFlo context.Args.AddArg(Execution::Args::Type::Platform, "Windows.Holographic"sv); DownloadCommand download({}); - REQUIRE_THROWS_HR(download.Execute(context), APPINSTALLER_CLI_ERROR_NO_APPLICABLE_SFSCLIENT_PACKAGE); + download.Execute(context); + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_NO_APPLICABLE_SFSCLIENT_PACKAGE); INFO(downloadOutput.str()); } @@ -554,6 +558,27 @@ TEST_CASE("MSStoreDownloadFlow_Fail_Licensing", "[MSStoreDownloadFlow][workflow] { TestCommon::TempDirectory tempDirectory("TestDownloadDirectory", false); + std::ostringstream downloadOutput; + TestContext context{ downloadOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideDownloadInstallerFileForMSStoreDownload(context); + TestHook::SetDisplayCatalogHttpPipelineStage_Override displayCatalogOverride(GetTestRestRequestHandler(web::http::status_codes::OK, TestDisplayCatalogResponse)); + TestHook::SetSfsClientAppContents_Override sfsClientOverride({ &GetSfsAppContentsOverrideFunction }); + TestHook::SetLicensingHttpPipelineStage_Override licensingOverride(GetTestRestRequestHandler(web::http::status_codes::InternalError)); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("DownloadFlowTest_MSStore.yaml").GetPath().u8string()); + context.Args.AddArg(Execution::Args::Type::DownloadDirectory, tempDirectory); + context.Args.AddArg(Execution::Args::Type::Locale, "en-US"sv); + + DownloadCommand download({}); + download.Execute(context); + REQUIRE_TERMINATED_WITH(context, MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, web::http::status_codes::InternalError)); + INFO(downloadOutput.str()); +} + +TEST_CASE("MSStoreDownloadFlow_Fail_Licensing_Forbidden", "[MSStoreDownloadFlow][workflow]") +{ + TestCommon::TempDirectory tempDirectory("TestDownloadDirectory", false); + std::ostringstream downloadOutput; TestContext context{ downloadOutput, std::cin }; auto previousThreadGlobals = context.SetForCurrentThread(); @@ -566,6 +591,7 @@ TEST_CASE("MSStoreDownloadFlow_Fail_Licensing", "[MSStoreDownloadFlow][workflow] context.Args.AddArg(Execution::Args::Type::Locale, "en-US"sv); DownloadCommand download({}); - REQUIRE_THROWS_HR(download.Execute(context), MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, web::http::status_codes::Forbidden)); + download.Execute(context); + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED_FORBIDDEN); INFO(downloadOutput.str()); } diff --git a/src/AppInstallerCLITests/MsixInfo.cpp b/src/AppInstallerCLITests/MsixInfo.cpp index 5ae96900eb..c620672d14 100644 --- a/src/AppInstallerCLITests/MsixInfo.cpp +++ b/src/AppInstallerCLITests/MsixInfo.cpp @@ -95,8 +95,13 @@ TEST_CASE("MsixInfo_ValidateMsixTrustInfo", "[msixinfo]") } } -TEST_CASE("MsixInfo_GetPackageVersionFromFullName", "[msixinfo]") +TEST_CASE("MsixInfo_GetPackageIdInfoFromFullName", "[msixinfo]") { - REQUIRE(Msix::GetPackageVersionFromFullName("Microsoft.NET.Native.Framework.2.2_2.2.29512.0_arm64__8wekyb3d8bbwe") == Utility::UInt64Version{ "2.2.29512.0" }); - REQUIRE(Msix::GetPackageVersionFromFullName("Microsoft.DoesNotExist_1.2.3.4_neutral_~_8wekyb3d8bbwe") == Utility::UInt64Version{ "1.2.3.4" }); + auto testPackageIdInfo = Msix::GetPackageIdInfoFromFullName("Microsoft.NET.Native.Framework.2.2_2.2.29512.0_arm64__8wekyb3d8bbwe"); + REQUIRE(testPackageIdInfo.Name == "Microsoft.NET.Native.Framework.2.2"); + REQUIRE(testPackageIdInfo.Version == Utility::UInt64Version{ "2.2.29512.0" }); + + auto testPackageIdInfo2 = Msix::GetPackageIdInfoFromFullName("Microsoft.DoesNotExist_1.2.3.4_neutral_~_8wekyb3d8bbwe"); + REQUIRE(testPackageIdInfo2.Name == "Microsoft.DoesNotExist"); + REQUIRE(testPackageIdInfo2.Version == Utility::UInt64Version{ "1.2.3.4" }); } diff --git a/src/AppInstallerCommonCore/MSStoreDownload.cpp b/src/AppInstallerCommonCore/MSStoreDownload.cpp index 254987f3e1..4c7dc63253 100644 --- a/src/AppInstallerCommonCore/MSStoreDownload.cpp +++ b/src/AppInstallerCommonCore/MSStoreDownload.cpp @@ -669,9 +669,9 @@ namespace AppInstaller::MSStore return Utility::Architecture::Unknown; } - std::vector GetSfsPackageFileSupportedPlatforms(const SFS::AppFile& appFile, Manifest::PlatformEnum requiredPlatform) + std::vector GetSfsPackageFileSupportedPlatforms(const SFS::AppFile& appFile, Manifest::PlatformEnum requiredPlatform) { - std::vector supportedPlatforms; + std::vector supportedPlatforms; for (auto const& applicability : appFile.GetApplicabilityDetails().GetPlatformApplicabilityForPackage()) { @@ -679,7 +679,7 @@ namespace AppInstaller::MSStore if (platform != Manifest::PlatformEnum::Unknown && (platform == requiredPlatform || requiredPlatform == Manifest::PlatformEnum::Unknown)) { - supportedPlatforms.emplace_back(applicability); + supportedPlatforms.emplace_back(platform); } } @@ -709,7 +709,7 @@ namespace AppInstaller::MSStore } // This also checks if the file type is supported. If not supported, the return is empty string. - std::string GetSfsPackageFileName(const SFS::AppFile& appFile) + std::string GetSfsPackageFileExtension(const SFS::AppFile& appFile) { std::string fileExtension = std::filesystem::path{ appFile.GetFileId() }.extension().u8string(); @@ -728,7 +728,48 @@ namespace AppInstaller::MSStore return {}; } - return appFile.GetFileMoniker() + fileExtension; + return fileExtension; + } + + // The file name will be {Name}_{Version}_{Platform list}_{Arch list}.{File Extension} + // If the file name is longer than 256, file moniker will be used. + std::string GetSfsPackageFileNameForDownload( + const std::string& packageName, + const Utility::UInt64Version& packageVersion, + const std::vector& supportedPlatforms, + const std::vector& supportedArchitectures, + const std::string& fileExtension, + const std::string& fileMoniker) + { + std::string platformString; + for (auto platform : supportedPlatforms) + { + platformString += std::string{ Manifest::PlatformToString(platform, true) } + '.'; + } + platformString.resize(platformString.size() - 1); + + std::string architectureString; + for (auto architecture : supportedArchitectures) + { + architectureString += std::string{ Utility::ToString(architecture) } + '.'; + } + architectureString.resize(architectureString.size() - 1); + + std::string fileName = + packageName + '_' + + packageVersion.ToString() + '_' + + platformString + '_' + + architectureString + + fileExtension; + + if (fileName.length() < 256) + { + return fileName; + } + else + { + return fileMoniker + fileExtension; + } } void SfsClientLoggingCallback(const SFS::LogData& logData) @@ -784,7 +825,7 @@ namespace AppInstaller::MSStore Utility::Architecture requiredArchitecture = Utility::Architecture::Unknown, Manifest::PlatformEnum requiredPlatform = Manifest::PlatformEnum::Unknown) { - using PlatformAndArchitectureKey = std::pair; + using PlatformAndArchitectureKey = std::pair; // Since the server may return multiple versions of the same package, we'll use this map to record the one with latest version // for each Platform|Architecture pair. @@ -805,8 +846,8 @@ namespace AppInstaller::MSStore AICLI_LOG(Core, Info, << "Package skipped due to unsupported architecture. FileId:" << appFile.GetFileId()); continue; } - std::string fileName = GetSfsPackageFileName(appFile); - if (fileName.empty()) + std::string fileExtension = GetSfsPackageFileExtension(appFile); + if (fileExtension.empty()) { AICLI_LOG(Core, Info, << "Package skipped due to unsupported file type. FileId:" << appFile.GetFileId()); continue; @@ -814,10 +855,13 @@ namespace AppInstaller::MSStore MSStoreDownloadFile downloadFile; downloadFile.Url = appFile.GetUrl(); - downloadFile.FileName = fileName; // The sha256 hash was base64 encoded downloadFile.Sha256 = JSON::Base64Decode(appFile.GetHashes().at(SFS::HashType::Sha256)); - downloadFile.Version = Msix::GetPackageVersionFromFullName(appFile.GetFileMoniker()); + auto packageInfo = Msix::GetPackageIdInfoFromFullName(appFile.GetFileMoniker()); + downloadFile.Version = packageInfo.Version; + downloadFile.FileName = GetSfsPackageFileNameForDownload( + packageInfo.Name, packageInfo.Version, supportedPlatforms, + supportedArchitectures, fileExtension, appFile.GetFileMoniker()); // Update the platform architecture map with latest package if applicable for (auto supportedPlatform : supportedPlatforms) @@ -870,8 +914,16 @@ namespace AppInstaller::MSStore auto requestResult = GetSfsClientInstance()->GetLatestAppDownloadInfo(sfsClientRequest, appContents); if (!requestResult) { - AICLI_LOG(Core, Error, << "Failed to call SfsClient GetLatestAppDownloadInfo. Error code: " << requestResult.GetCode() << " Message: " << requestResult.GetMsg()); - THROW_HR_MSG(APPINSTALLER_CLI_ERROR_SFSCLIENT_API_FAILED, "Failed to call SfsClient GetLatestAppDownloadInfo. ErrorCode: %lu Message: %hs", requestResult.GetCode(), requestResult.GetMsg().c_str()); + if (requestResult.GetCode() == SFS::Result::Code::HttpNotFound) + { + AICLI_LOG(Core, Error, << "Failed to call SfsClient GetLatestAppDownloadInfo. Package not found."); + THROW_HR_MSG(APPINSTALLER_CLI_ERROR_SFSCLIENT_PACKAGE_NOT_SUPPORTED, "Failed to call SfsClient GetLatestAppDownloadInfo. Package download not supported."); + } + else + { + AICLI_LOG(Core, Error, << "Failed to call SfsClient GetLatestAppDownloadInfo. Error code: " << requestResult.GetCode() << " Message: " << requestResult.GetMsg()); + THROW_HR_MSG(APPINSTALLER_CLI_ERROR_SFSCLIENT_API_FAILED, "Failed to call SfsClient GetLatestAppDownloadInfo. ErrorCode: %lu Message: %hs", requestResult.GetCode(), requestResult.GetMsg().c_str()); + } } } @@ -943,8 +995,25 @@ namespace AppInstaller::MSStore Http::HttpClientHelper::HttpRequestHeaders requestHeaders; requestHeaders.insert_or_assign(JSON::GetUtilityString(From), L"winget-cli"); - std::optional licensingResponseObject = httpClientHelper.HandlePost( - JSON::GetUtilityString(LicensingRestEndpoint), requestBody, requestHeaders, authHeaders); + std::optional licensingResponseObject = std::nullopt; + try + { + licensingResponseObject = httpClientHelper.HandlePost( + JSON::GetUtilityString(LicensingRestEndpoint), requestBody, requestHeaders, authHeaders); + } + catch (const wil::ResultException& re) + { + if (re.GetErrorCode() == HTTP_E_STATUS_FORBIDDEN) + { + AICLI_LOG(CLI, Error, << "Getting MSStore package license failed. The Microsoft Entra Id account does not have privilege."); + THROW_HR(APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED_FORBIDDEN); + } + else + { + AICLI_LOG(CLI, Error, << "Getting MSStore package license failed. Error code: " << re.GetErrorCode()); + THROW_HR(re.GetErrorCode()); + } + } if (!licensingResponseObject || licensingResponseObject->is_null()) { diff --git a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp index e37202f06e..108097561f 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp @@ -658,20 +658,20 @@ namespace AppInstaller::Manifest return "unknown"sv; } - std::string_view PlatformToString(PlatformEnum platform) + std::string_view PlatformToString(PlatformEnum platform, bool shortString) { switch (platform) { case PlatformEnum::Desktop: - return "Windows.Desktop"sv; + return shortString ? "Desktop" : "Windows.Desktop"sv; case PlatformEnum::Universal: - return "Windows.Universal"sv; + return shortString ? "Universal" : "Windows.Universal"sv; case PlatformEnum::IoT: - return "Windows.IoT"sv; + return shortString ? "IoT" : "Windows.IoT"sv; case PlatformEnum::Holographic: - return "Windows.Holographic"sv; + return shortString ? "Holographic" : "Windows.Holographic"sv; case PlatformEnum::Team: - return "Windows.Team"sv; + return shortString ? "Team" : "Windows.Team"sv; } return "Unknown"sv; diff --git a/src/AppInstallerCommonCore/MsixInfo.cpp b/src/AppInstallerCommonCore/MsixInfo.cpp index 6c261b133a..f1bbb72eb8 100644 --- a/src/AppInstallerCommonCore/MsixInfo.cpp +++ b/src/AppInstallerCommonCore/MsixInfo.cpp @@ -465,7 +465,7 @@ namespace AppInstaller::Msix return { result }; } - Utility::UInt64Version GetPackageVersionFromFullName(std::string_view fullName) + Msix::PackageIdInfo GetPackageIdInfoFromFullName(std::string_view fullName) { std::wstring fullNameWide = Utility::ConvertToUTF16(fullName); @@ -474,7 +474,7 @@ namespace AppInstaller::Msix if (returnVal != ERROR_INSUFFICIENT_BUFFER) { LOG_WIN32(returnVal); - return 0; + return {}; } THROW_HR_IF(E_UNEXPECTED, length == 0); @@ -485,12 +485,12 @@ namespace AppInstaller::Msix if (returnVal != ERROR_SUCCESS) { LOG_WIN32(returnVal); - return 0; + return {}; } PACKAGE_ID* packageId = (PACKAGE_ID*)packageIdContent.get(); - return packageId->version.Version; + return { Utility::ConvertToUTF8(packageId->name), packageId->version.Version }; } GetCertContextResult GetCertContextFromMsix(const std::filesystem::path& msixPath) diff --git a/src/AppInstallerCommonCore/Public/AppInstallerMsixInfo.h b/src/AppInstallerCommonCore/Public/AppInstallerMsixInfo.h index 7efff7a2bc..156756e86c 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerMsixInfo.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerMsixInfo.h @@ -47,8 +47,14 @@ namespace AppInstaller::Msix // Gets the package location from the given full name. std::optional GetPackageLocationFromFullName(std::string_view fullName); - // Gets the package version from the given full name. - AppInstaller::Utility::UInt64Version GetPackageVersionFromFullName(std::string_view fullName); + struct PackageIdInfo + { + std::string Name; + AppInstaller::Utility::UInt64Version Version; + }; + + // Gets the package id info from the given full name. + PackageIdInfo GetPackageIdInfoFromFullName(std::string_view fullName); // MsixInfo class handles all appx/msix related query. struct MsixInfo diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h index a5e8f42b5d..a501191eb6 100644 --- a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h +++ b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h @@ -406,7 +406,8 @@ namespace AppInstaller::Manifest std::string_view RepairBehaviorToString(RepairBehaviorEnum repairBehavior); - std::string_view PlatformToString(PlatformEnum platform); + // Short string representation does not contain "Windows." + std::string_view PlatformToString(PlatformEnum platform, bool shortString = false); std::string_view ScopeToString(ScopeEnum scope); diff --git a/src/AppInstallerSharedLib/Errors.cpp b/src/AppInstallerSharedLib/Errors.cpp index fea0fcad7c..46267e0a65 100644 --- a/src/AppInstallerSharedLib/Errors.cpp +++ b/src/AppInstallerSharedLib/Errors.cpp @@ -219,6 +219,8 @@ namespace AppInstaller WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_SFSCLIENT_API_FAILED, "Failed to get Microsoft Store package download information."), WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_NO_APPLICABLE_SFSCLIENT_PACKAGE, "No applicable Microsoft Store package download information found."), WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED, "Failed to retrieve Microsoft Store package license."), + WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_SFSCLIENT_PACKAGE_NOT_SUPPORTED, "The Microsoft Store package does not support download."), + WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED_FORBIDDEN, "Failed to retrieve Microsoft Store package license. The Microsoft Entra Id account does not have the required privilege."), // Install errors. WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE, "Application is currently running. Exit the application then try again."), diff --git a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h index 33c03906cf..821a76b815 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h @@ -149,6 +149,8 @@ #define APPINSTALLER_CLI_ERROR_SFSCLIENT_API_FAILED ((HRESULT)0x8A150081) #define APPINSTALLER_CLI_ERROR_NO_APPLICABLE_SFSCLIENT_PACKAGE ((HRESULT)0x8A150082) #define APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED ((HRESULT)0x8A150083) +#define APPINSTALLER_CLI_ERROR_SFSCLIENT_PACKAGE_NOT_SUPPORTED ((HRESULT)0x8A150084) +#define APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED_FORBIDDEN ((HRESULT)0x8A150085) // Install errors. #define APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE ((HRESULT)0x8A150101)