winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit f31f411c780b3e95893cae3cb0ec3ed58d6d4989
parent 4073c4304e6a87db3cbf0826bb694d9296072cd3
Author: yao-msft <50888816+yao-msft@users.noreply.github.com>
Date:   Thu,  9 May 2024 15:53:50 -0700

Complete MSStore download and licensing (#4464)

The MSStore download feature is mainly targeted for enterprise offline
distribution. So a Microsoft Entra Id account that is at least
Administrator role of the directory is required for licensing. A follow
up pr will make the DisplayCatalog calls (for acquiring MSStore package
info) require Microsoft Entra Id account as well, when the backend
configuration is done.

Added tests. Manually validated and applied feedback with stakeholders
too.

Diffstat:
M.github/actions/spelling/allow.txt | 3+++
M.github/actions/spelling/expect.txt | 9+++++----
M.github/actions/spelling/patterns.txt | 4++--
Mdoc/windows/package-manager/winget/returnCodes.md | 13+++++++++----
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj | 29++++++++++++++---------------
Msrc/AppInstallerCLICore/Argument.cpp | 8++++++++
Msrc/AppInstallerCLICore/Commands/DownloadCommand.cpp | 21++++++++++++++++++---
Msrc/AppInstallerCLICore/ExecutionArgs.h | 2++
Msrc/AppInstallerCLICore/Resources.h | 17+++++++++++++++++
Msrc/AppInstallerCLICore/Workflows/DependenciesFlow.cpp | 2+-
Msrc/AppInstallerCLICore/Workflows/DownloadFlow.cpp | 26+++++++++++++-------------
Msrc/AppInstallerCLICore/Workflows/DownloadFlow.h | 1+
Msrc/AppInstallerCLICore/Workflows/InstallFlow.cpp | 2+-
Msrc/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.cpp | 209++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.h | 4+---
Msrc/AppInstallerCLICore/Workflows/ShowFlow.cpp | 4++--
Msrc/AppInstallerCLICore/pch.h | 6------
Msrc/AppInstallerCLIE2ETests/Constants.cs | 6+++---
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 90++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 12++++++++----
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 6++++++
Msrc/AppInstallerCLITests/CustomHeader.cpp | 2+-
Msrc/AppInstallerCLITests/HttpClientHelper.cpp | 4++--
Asrc/AppInstallerCLITests/MSStoreDownloadFlow.cpp | 593+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/MsixInfo.cpp | 9+++++++--
Msrc/AppInstallerCLITests/RestInterface_1_0.cpp | 6+++---
Msrc/AppInstallerCLITests/TestCommon.cpp | 3+++
Asrc/AppInstallerCLITests/TestData/DownloadFlowTest_MSStore.yaml | 38++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/TestHooks.h | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/AppInstallerCLITests/WorkflowCommon.cpp | 16++++++++++++++--
Msrc/AppInstallerCLITests/WorkflowCommon.h | 5+++--
Msrc/AppInstallerCommonCore/HttpClientHelper.cpp | 8++++----
Msrc/AppInstallerCommonCore/MSStoreDownload.cpp | 1160++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/AppInstallerCommonCore/Manifest/ManifestCommon.cpp | 48+++++++++++++++++++++++++++++++++++++++++-------
Msrc/AppInstallerCommonCore/MsixInfo.cpp | 28++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Public/AppInstallerMsixInfo.h | 7+++++--
Msrc/AppInstallerCommonCore/Public/winget/MSStoreDownload.h | 88++++++++++++++++++++++++++-----------------------------------------------------
Msrc/AppInstallerCommonCore/Public/winget/ManifestCommon.h | 10+++++++---
Msrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchRequestSerializer_1_0.cpp | 2+-
Msrc/AppInstallerSharedLib/Errors.cpp | 12++++++++----
Msrc/AppInstallerSharedLib/Public/AppInstallerErrors.h | 12++++++++----
Msrc/Microsoft.Management.Deployment/Converters.cpp | 5+++--
42 files changed, 2074 insertions(+), 512 deletions(-)

diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt @@ -141,6 +141,7 @@ INSTALLPATH INSTALLUILEVEL INVALIDARG INVALIDSID +iot IPinning IPortable ISAPPROVEDFOROUTPUT @@ -255,6 +256,7 @@ resetpins resheader resmimetype RESOLVESOURCE +RESTAPI RESTARTAPPS resw resx @@ -398,6 +400,7 @@ wrl WTD wtypesbase Xamarin +Xbox XElement Xlang XResource diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -108,8 +108,6 @@ dsc DUPLICATEALIAS dustojnikhummer dvinns -EAppx -eappxbundle ecfr ecfrbrowse ECustom @@ -412,7 +410,9 @@ servercert servercertificate setmetadatabymanifestid SETTINGCHANGE -SETTINGMAPPING +SETTINGMAPPING +sfs +sfsclient SHCONTF SHGDN SHOWNORMAL @@ -433,7 +433,8 @@ Srinivasan srs startswith STARTUPINFOW -STDMETHODCALLTYPE +STDMETHODCALLTYPE +storeapps storeorigin STRRET stylecop diff --git a/.github/actions/spelling/patterns.txt b/.github/actions/spelling/patterns.txt @@ -16,8 +16,8 @@ data:[a-zA-Z=;,/0-9+-]+ # sha-1 \b[0-9a-f]{40}\b El proyecto .* diferentes -# Package family names -\b[-.A-Za-z0-9]+_[a-z0-9]{13}\b +# Package family names and package full names +\b[-_~.A-Za-z0-9]+_[a-z0-9]{13}\b # Locales for name normalization \b\p{Lu}{2,3}(?:-(?:CANS|CYRL|LATN|MONG))?-\p{Lu}{2}(?![A-Z])(?:-VALENCIA)?\b # Azure pipeline tasks diff --git a/doc/windows/package-manager/winget/returnCodes.md b/doc/windows/package-manager/winget/returnCodes.md @@ -70,16 +70,16 @@ ms.localizationpriority: medium | 0x8A150038 | -1978335176 | APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE | The configured rest source is not supported | | 0x8A150039 | -1978335175 | APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA | Invalid data returned by rest source | | 0x8A15003A | -1978335174 | APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY | Operation is blocked by Group Policy | -| 0x8A15003B | -1978335173 | APPINSTALLER_CLI_ERROR_RESTSOURCE_INTERNAL_ERROR | Rest source internal error | +| 0x8A15003B | -1978335173 | APPINSTALLER_CLI_ERROR_RESTAPI_INTERNAL_ERROR | Rest API internal error | | 0x8A15003C | -1978335172 | APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_URL | Invalid rest source url | -| 0x8A15003D | -1978335171 | APPINSTALLER_CLI_ERROR_RESTSOURCE_UNSUPPORTED_MIME_TYPE | Unsupported MIME type returned by rest source | +| 0x8A15003D | -1978335171 | APPINSTALLER_CLI_ERROR_RESTAPI_UNSUPPORTED_MIME_TYPE | Unsupported MIME type returned by rest API | | 0x8A15003E | -1978335170 | APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_VERSION | Invalid rest source contract version | | 0x8A15003F | -1978335169 | APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE | The source data is corrupted or tampered | | 0x8A150040 | -1978335168 | APPINSTALLER_CLI_ERROR_STREAM_READ_FAILURE | Error reading from the stream | | 0x8A150041 | -1978335167 | APPINSTALLER_CLI_ERROR_PACKAGE_AGREEMENTS_NOT_ACCEPTED | Package agreements were not agreed to | | 0x8A150042 | -1978335166 | APPINSTALLER_CLI_ERROR_PROMPT_INPUT_ERROR | Error reading input in prompt | | 0x8A150043 | -1978335165 | APPINSTALLER_CLI_ERROR_UNSUPPORTED_SOURCE_REQUEST | The search request is not supported by one or more sources | -| 0x8A150044 | -1978335164 | APPINSTALLER_CLI_ERROR_RESTSOURCE_ENDPOINT_NOT_FOUND | The rest source endpoint is not found. | +| 0x8A150044 | -1978335164 | APPINSTALLER_CLI_ERROR_RESTAPI_ENDPOINT_NOT_FOUND | The rest API endpoint is not found. | | 0x8A150045 | -1978335163 | APPINSTALLER_CLI_ERROR_SOURCE_OPEN_FAILED | Failed to open the source. | | 0x8A150046 | -1978335162 | APPINSTALLER_CLI_ERROR_SOURCE_AGREEMENTS_NOT_ACCEPTED | Source agreements were not agreed to | | 0x8A150047 | -1978335161 | APPINSTALLER_CLI_ERROR_CUSTOMHEADER_EXCEEDS_MAXLENGTH | Header size exceeds the allowable limit of 1024 characters. Please reduce the size and try again. | @@ -137,7 +137,12 @@ ms.localizationpriority: medium | 0x8A15007B | -1978335109 | APPINSTALLER_CLI_ERROR_EXEC_REPAIR_FAILED | Repair operation failed. | | 0x8A15007C | -1978335108 | APPINSTALLER_CLI_ERROR_REPAIR_NOT_SUPPORTED | The installer technology in use doesn't support repair. | | 0x8A15007D | -1978335107 | APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED | Repair operations involving administrator privileges are not permitted on packages installed within the user scope. | - +| 0x8A15007E | -1978335106 | APPINSTALLER_CLI_ERROR_SQLITE_CONNECTION_TERMINATED | The SQLite connection was terminated to prevent corruption. | +| 0x8A15007F | -1978335105 | APPINSTALLER_CLI_ERROR_DISPLAYCATALOG_API_FAILED | Failed to get Microsoft Store package catalog. | +| 0x8A150080 | -1978335104 | APPINSTALLER_CLI_ERROR_NO_APPLICABLE_DISPLAYCATALOG_PACKAGE | No applicable Microsoft Store package found from Microsoft Store package catalog. | +| 0x8A150081 | -1978335103 | APPINSTALLER_CLI_ERROR_SFSCLIENT_API_FAILED | Failed to get Microsoft Store package download infomation. | +| 0x8A150082 | -1978335102 | APPINSTALLER_CLI_ERROR_NO_APPLICABLE_SFSCLIENT_PACKAGE | No applicable Microsoft Store package download infomation found. | +| 0x8A150083 | -1978335101 | APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED | Failed to retrieve Microsoft Store package license. | ## Install errors. diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -222,10 +222,10 @@ <ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'"> <ClCompile> <Optimization>Disabled</Optimization> - <PreprocessorDefinitions>_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING;_DEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD</PreprocessorDefinitions> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">true</TreatWarningAsError> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">true</TreatWarningAsError> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</TreatWarningAsError> @@ -249,7 +249,7 @@ <ItemDefinitionGroup Condition="'$(Platform)'=='Win32'"> <ClCompile> <PreprocessorDefinitions>WIN32;%(PreprocessorDefinitions);CLICOREDLLBUILD</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</TreatWarningAsError> <SDLCheck Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</SDLCheck> <EnablePREfast Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</EnablePREfast> @@ -265,10 +265,10 @@ <FunctionLevelLinking>true</FunctionLevelLinking> <IntrinsicFunctions>true</IntrinsicFunctions> <PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">true</TreatWarningAsError> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">true</TreatWarningAsError> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</TreatWarningAsError> @@ -302,10 +302,10 @@ <FunctionLevelLinking>true</FunctionLevelLinking> <IntrinsicFunctions>true</IntrinsicFunctions> <PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|ARM'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|ARM64'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|Win32'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|x64'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|ARM'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|ARM64'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|Win32'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|x64'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|ARM'">true</TreatWarningAsError> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|ARM64'">true</TreatWarningAsError> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|Win32'">true</TreatWarningAsError> @@ -551,4 +551,4 @@ </Reference> </ItemGroup> </Target> -</Project>- \ No newline at end of file +</Project> diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -219,6 +219,10 @@ namespace AppInstaller::CLI // Download command case Execution::Args::Type::DownloadDirectory: return { type, "download-directory"_liv, 'd', ArgTypeCategory::None }; + case Execution::Args::Type::Platform: + return { type, "platform"_liv, ArgTypeCategory::None }; + case Execution::Args::Type::SkipMicrosoftStorePackageLicense: + return { type, "skip-microsoft-store-package-license"_liv, "skip-license"_liv, ArgTypeCategory::None }; // Common arguments case Execution::Args::Type::NoVT: @@ -398,6 +402,10 @@ namespace AppInstaller::CLI return Argument{ type, Resource::String::ForceArgumentDescription, ArgumentType::Flag, false }; case Args::Type::DownloadDirectory: return Argument{ type, Resource::String::DownloadDirectoryArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help, false }; + case Args::Type::SkipMicrosoftStorePackageLicense: + return Argument{ type, Resource::String::SkipMicrosoftStorePackageLicenseArgumentDescription, ArgumentType::Flag, Argument::Visibility::Help, false }; + case Args::Type::Platform: + return Argument{ type, Resource::String::PlatformArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help, false }; case Args::Type::InstallerType: return Argument{ type, Resource::String::InstallerTypeArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help, false }; case Args::Type::ResumeId: diff --git a/src/AppInstallerCLICore/Commands/DownloadCommand.cpp b/src/AppInstallerCLICore/Commands/DownloadCommand.cpp @@ -6,7 +6,8 @@ #include "Workflows/InstallFlow.h" #include "Workflows/PromptFlow.h" #include "Resources.h" -#include <AppInstallerRuntime.h> +#include <AppInstallerRuntime.h> +#include <winget/ManifestCommon.h> namespace AppInstaller::CLI { @@ -37,7 +38,9 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::AuthenticationMode), Argument::ForType(Args::Type::AuthenticationAccount), Argument::ForType(Args::Type::AcceptPackageAgreements), - Argument::ForType(Args::Type::AcceptSourceAgreements), + Argument::ForType(Args::Type::AcceptSourceAgreements), + Argument::ForType(Args::Type::SkipMicrosoftStorePackageLicense), + Argument::ForType(Args::Type::Platform), }; } @@ -58,7 +61,19 @@ namespace AppInstaller::CLI void DownloadCommand::ValidateArgumentsInternal(Args& execArgs) const { - Argument::ValidateCommonArguments(execArgs); + Argument::ValidateCommonArguments(execArgs); + + if (execArgs.Contains(Execution::Args::Type::Platform)) + { + Manifest::PlatformEnum selectedPlatform = Manifest::ConvertToPlatformEnumForMSStoreDownload(execArgs.GetArg(Execution::Args::Type::Platform)); + if (selectedPlatform == Manifest::PlatformEnum::Unknown) + { + auto validOptions = Utility::Join(", "_liv, std::vector<Utility::LocIndString>{ + "Windows.Universal"_lis, "Windows.Desktop"_lis, "Windows.IoT"_lis, "Windows.Team"_lis, "Windows.Holographic"_lis + }); + throw CommandException(Resource::String::InvalidArgumentValueError(Argument::ForType(Execution::Args::Type::Platform).Name(), validOptions)); + } + } } void DownloadCommand::ExecuteInternal(Context& context) const diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -88,6 +88,8 @@ namespace AppInstaller::CLI::Execution // Download Command DownloadDirectory, + SkipMicrosoftStorePackageLicense, + Platform, // Setting Command AdminSettingEnable, diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -336,7 +336,21 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(MsixArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(MsixSignatureHashFailed); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreAppBlocked); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadAuthenticationNotice); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadDependencyPackages); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadGetDownloadInfo); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadGetDownloadInfoFailed); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadGetLicense); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadGetLicenseFailed); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadGetLicenseSuccess); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadMainPackages); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadPackageDownloaded); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreDownloadPackageDownloadFailed); + 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); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreRepairFailed); @@ -409,6 +423,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PinResetUseForceArg); WINGET_DEFINE_RESOURCE_STRINGID(PinType); WINGET_DEFINE_RESOURCE_STRINGID(PinVersion); + WINGET_DEFINE_RESOURCE_STRINGID(PlatformArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(PoliciesPolicy); WINGET_DEFINE_RESOURCE_STRINGID(PortableAliasAdded); WINGET_DEFINE_RESOURCE_STRINGID(PortableHashMismatchOverridden); @@ -506,6 +521,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelInstallationNotes); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelInstaller); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelInstallerLocale); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelInstallerOfflineDistributionSupported); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelInstallerProductId); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelInstallerReleaseDate); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelInstallerSha256); @@ -531,6 +547,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SilentArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SingleCharAfterDashError); WINGET_DEFINE_RESOURCE_STRINGID(SkipDependenciesArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(SkipMicrosoftStorePackageLicenseArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceAddAlreadyExistsDifferentArg); WINGET_DEFINE_RESOURCE_STRINGID(SourceAddAlreadyExistsDifferentName); WINGET_DEFINE_RESOURCE_STRINGID(SourceAddAlreadyExistsMatch); diff --git a/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp b/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp @@ -337,7 +337,7 @@ namespace AppInstaller::CLI::Workflow if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerDownloadOnly)) { - dependencyContext.Add<Execution::Data::DownloadDirectory>(context.Get<Execution::Data::DownloadDirectory>() / "Dependencies"); + dependencyContext.Add<Execution::Data::DownloadDirectory>(context.Get<Execution::Data::DownloadDirectory>() / L"Dependencies"); } dependencyPackageContexts.emplace_back(std::move(dependencyContextPtr)); diff --git a/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp b/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp @@ -11,9 +11,6 @@ #include <winget/GroupPolicy.h> #include <winget/ManifestYamlWriter.h> #include <winget/NetworkSettings.h> -#include <winget/HttpClientHelper.h> -#include <winget/Rest.h> - namespace AppInstaller::CLI::Workflow { @@ -247,14 +244,13 @@ namespace AppInstaller::CLI::Workflow case InstallerTypeEnum::MSStore: if (installerDownloadOnly) { - context << MSStoreDownload; - break; - } - else - { - // Nothing to do here - return; + context << + EnsureFeatureEnabled(Settings::ExperimentalFeature::Feature::StoreDownload) << + MSStoreDownload << + ExportManifest; } + + return; default: THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } @@ -619,7 +615,7 @@ namespace AppInstaller::CLI::Workflow if (context.Args.Contains(Execution::Args::Type::DownloadDirectory)) { - context.Add<Execution::Data::DownloadDirectory>(std::filesystem::path{ context.Args.GetArg(Execution::Args::Type::DownloadDirectory) }); + context.Add<Execution::Data::DownloadDirectory>(std::filesystem::path{ Utility::ConvertToUTF16(context.Args.GetArg(Execution::Args::Type::DownloadDirectory)) }); } else { @@ -631,8 +627,12 @@ namespace AppInstaller::CLI::Workflow } const auto& manifest = context.Get<Execution::Data::Manifest>(); - std::string packageDownloadFolderName = manifest.Id + '_' + manifest.Version; - context.Add<Execution::Data::DownloadDirectory>(downloadsDirectory / packageDownloadFolderName); + std::string packageDownloadFolderName = manifest.Id; + if (!Utility::Version{ manifest.Version }.IsUnknown()) + { + packageDownloadFolderName += '_' + manifest.Version; + } + context.Add<Execution::Data::DownloadDirectory>(downloadsDirectory / Utility::ConvertToUTF16(packageDownloadFolderName)); } } diff --git a/src/AppInstallerCLICore/Workflows/DownloadFlow.h b/src/AppInstallerCLICore/Workflows/DownloadFlow.h @@ -24,6 +24,7 @@ namespace AppInstaller::CLI::Workflow void GetInstallerDownloadPath(Execution::Context& context); // Downloads the file referenced by the Installer. + // This workflow task is also used by MSStoreDownload task. // Required Args: None // Inputs: Installer, Manifest // Outputs: HashPair, InstallerPath diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -808,7 +808,7 @@ namespace AppInstaller::CLI::Workflow } CATCH_LOG() - void ReportARPChanges(Execution::Context& context) try + void ReportARPChanges(Execution::Context& context) try { if (!context.Contains(Execution::Data::ARPCorrelationData)) { diff --git a/src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.cpp @@ -2,13 +2,22 @@ // Licensed under the MIT License. #include "pch.h" #include "MSStoreInstallerHandler.h" -#include <winget/HttpClientHelper.h> +#include "WorkflowBase.h" +#include <AppInstallerSHA256.h> +#include <AppInstallerDownloader.h> +#include <AppInstallerRuntime.h> +#include <winget/Filesystem.h> #include <winget/MSStore.h> #include <winget/MSStoreDownload.h> #include <winget/SelfManagement.h> namespace AppInstaller::CLI::Workflow { + void DownloadInstallerFile(Execution::Context& context); +} + +namespace AppInstaller::CLI::Workflow +{ using namespace AppInstaller::MSStore; using namespace AppInstaller::SelfManagement; using namespace winrt::Windows::Foundation; @@ -64,6 +73,70 @@ namespace AppInstaller::CLI::Workflow THROW_IF_FAILED(hr); } + + HRESULT DownloadMSStorePackageFile(const MSStore::MSStoreDownloadFile& downloadFile, const std::filesystem::path& downloadDirectory, Execution::Context& context) + { + try + { + // Create a sub context to execute the package download + auto subContextPtr = context.CreateSubContext(); + Execution::Context& subContext = *subContextPtr; + auto previousThreadGlobals = subContext.SetForCurrentThread(); + + // Populate Installer and temp download path for sub context + Manifest::ManifestInstaller installer; + installer.Url = downloadFile.Url; + installer.Sha256 = downloadFile.Sha256; + subContext.Add<Execution::Data::Installer>(std::move(installer)); + + auto tempInstallerPath = Runtime::GetPathTo(Runtime::PathName::Temp); + tempInstallerPath /= Utility::SHA256::ConvertToString(downloadFile.Sha256); + AICLI_LOG(CLI, Info, << "Generated temp download path: " << tempInstallerPath); + subContext.Add<Execution::Data::InstallerPath>(tempInstallerPath); + + subContext << Workflow::DownloadInstallerFile; + if (subContext.IsTerminated()) + { + RETURN_HR(subContext.GetTerminationHR()); + } + + // Verify hash + const auto& hashPair = subContext.Get<Execution::Data::HashPair>(); + if (std::equal(hashPair.first.begin(), hashPair.first.end(), hashPair.second.begin())) + { + AICLI_LOG(CLI, Info, << "Microsoft Store package hash verified"); + subContext.Reporter.Info() << Resource::String::MSStoreDownloadPackageHashVerified << std::endl; + // Trust direct download from Store if hash matched + Utility::ApplyMotwIfApplicable(tempInstallerPath, URLZONE_TRUSTED); + } + else + { + if (!subContext.Args.Contains(Execution::Args::Type::HashOverride)) + { + AICLI_LOG(CLI, Error, << "Microsoft Store package hash mismatch"); + subContext.Reporter.Error() << Resource::String::MSStoreDownloadPackageHashMismatch << std::endl; + RETURN_HR(APPINSTALLER_CLI_ERROR_INSTALLER_HASH_MISMATCH); + } + else + { + AICLI_LOG(CLI, Warning, << "Microsoft Store package hash mismatch, but overridden."); + subContext.Reporter.Warn() << Resource::String::MSStoreDownloadPackageHashMismatch << std::endl; + } + } + + auto renamedDownloadedPackage = downloadDirectory / Utility::ConvertToUTF16(downloadFile.FileName); + Filesystem::RenameFile(tempInstallerPath, renamedDownloadedPackage); + subContext.Reporter.Info() << Resource::String::MSStoreDownloadPackageDownloaded(Utility::LocIndView{ renamedDownloadedPackage.u8string() }) << std::endl; + + return S_OK; + } + catch (...) + { + AICLI_LOG(CLI, Error, << "Microsoft Store package download failed. File: " << downloadFile.FileName); + context.Reporter.Error() << Resource::String::MSStoreDownloadPackageDownloadFailed(Utility::LocIndView{ downloadFile.FileName }) << std::endl; + RETURN_HR(APPINSTALLER_CLI_ERROR_DOWNLOAD_FAILED); + } + } } void MSStoreInstall(Execution::Context& context) @@ -191,56 +264,124 @@ namespace AppInstaller::CLI::Workflow void MSStoreDownload(Execution::Context& context) { - if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::StoreDownload)) + if (context.Args.Contains(Execution::Args::Type::Rename)) { - const auto& installer = context.Get<Execution::Data::Installer>().value(); - std::string storeRestEndpoint = MSStore::GetMSStoreCatalogRestApi(installer.ProductId, installer.Locale); + context.Reporter.Warn() << Resource::String::MSStoreDownloadRenameNotSupported << std::endl; + } - AppInstaller::Http::HttpClientHelper httpClientHelper; - std::optional<web::json::value> jsonObject = httpClientHelper.HandleGet(JSON::GetUtilityString(storeRestEndpoint)); + // Authentication notice + context.Reporter.Warn() << Resource::String::MSStoreDownloadAuthenticationNotice << std::endl; - if (!jsonObject) - { - AICLI_LOG(Core, Error, << "No json object found"); - } + const auto& installer = context.Get<Execution::Data::Installer>().value(); + + Utility::Architecture requiredArchitecture = Utility::Architecture::Unknown; + Manifest::PlatformEnum requiredPlatform = Manifest::PlatformEnum::Unknown; + std::string requiredLocale; + if (context.Args.Contains(Execution::Args::Type::InstallArchitecture)) + { + requiredArchitecture = Utility::ConvertToArchitectureEnum(context.Args.GetArg(Execution::Args::Type::InstallArchitecture)); + } + if (context.Args.Contains(Execution::Args::Type::Platform)) + { + requiredPlatform = Manifest::ConvertToPlatformEnumForMSStoreDownload(context.Args.GetArg(Execution::Args::Type::Platform)); + } + if (context.Args.Contains(Execution::Args::Type::Locale)) + { + requiredLocale = context.Args.GetArg(Execution::Args::Type::Locale); + } - const auto& packages = MSStore::DeserializeMSStoreCatalogPackages(jsonObject.value()); + MSStoreDownloadContext downloadContext{ installer.ProductId, requiredArchitecture, requiredPlatform, requiredLocale, GetAuthenticationArguments(context) }; - // Language - std::vector<std::string> requiredLocale; - if (context.Args.Contains(Execution::Args::Type::Locale)) - { - requiredLocale.emplace_back(context.Args.GetArg(Execution::Args::Type::Locale)); - } + MSStoreDownloadInfo downloadInfo; + try + { + context.Reporter.Info() << Resource::String::MSStoreDownloadGetDownloadInfo << std::endl; - // Architectures - std::vector<Utility::Architecture> allowedArchitectures; - if (context.Contains(Execution::Data::AllowedArchitectures)) + downloadInfo = downloadContext.GetDownloadInfo(); + } + catch (const wil::ResultException& re) + { + AICLI_LOG(CLI, Error, << "Getting MSStore package download info failed. Error code: " << re.GetErrorCode()); + + switch (re.GetErrorCode()) { - // Com caller can directly set allowed architectures - allowedArchitectures = context.Get<Execution::Data::AllowedArchitectures>(); + case APPINSTALLER_CLI_ERROR_NO_APPLICABLE_DISPLAYCATALOG_PACKAGE: + case APPINSTALLER_CLI_ERROR_NO_APPLICABLE_SFSCLIENT_PACKAGE: + context.Reporter.Error() << Resource::String::MSStoreDownloadPackageNotFound << std::endl; + break; + default: + context.Reporter.Error() << Resource::String::MSStoreDownloadGetDownloadInfoFailed << std::endl; } - else if (context.Args.Contains(Execution::Args::Type::InstallArchitecture)) + + throw; + } + + bool skipDependencies = context.Args.Contains(Execution::Args::Type::SkipDependencies); + + // Prepare directories + std::filesystem::path downloadDirectory = context.Get<Execution::Data::DownloadDirectory>(); + std::filesystem::path dependenciesDirectory = downloadDirectory / L"Dependencies"; + + // Create directories if needed. + auto directoryToCreate = (skipDependencies || downloadInfo.DependencyPackages.empty()) ? downloadDirectory : dependenciesDirectory; + if (!std::filesystem::exists(directoryToCreate)) + { + std::filesystem::create_directories(directoryToCreate); + } + else + { + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_CANNOT_MAKE), !std::filesystem::is_directory(directoryToCreate)); + } + + // Download dependency packages + if (!skipDependencies) + { + AICLI_LOG(CLI, Info, << "Downloading MSStore dependency packages"); + context.Reporter.Info() << Resource::String::MSStoreDownloadDependencyPackages << std::endl; + + for (auto const& dependencyPackage : downloadInfo.DependencyPackages) { - allowedArchitectures.emplace_back(Utility::ConvertToArchitectureEnum(context.Args.GetArg(Execution::Args::Type::InstallArchitecture))); + THROW_IF_FAILED(DownloadMSStorePackageFile(dependencyPackage, dependenciesDirectory, context)); } + } + + // Download main packages + AICLI_LOG(CLI, Info, << "Downloading MSStore main packages"); + context.Reporter.Info() << Resource::String::MSStoreDownloadMainPackages << std::endl; + for (auto const& mainPackage : downloadInfo.MainPackages) + { + THROW_IF_FAILED(DownloadMSStorePackageFile(mainPackage, downloadDirectory, context)); + } - DisplayCatalogPackageComparator packageComparator(requiredLocale, allowedArchitectures); - auto result = packageComparator.GetPreferredPackage(packages); + context.Reporter.Info() << Resource::String::MSStoreDownloadPackageDownloadSuccess << std::endl; - if (!result) + // Get license + if (!context.Args.Contains(Execution::Args::Type::SkipMicrosoftStorePackageLicense)) + { + AICLI_LOG(CLI, Info, << "Getting MSStore package license"); + context.Reporter.Info() << Resource::String::MSStoreDownloadGetLicense << std::endl; + + std::vector<BYTE> licenseContent; + try { - context.Reporter.Error() << Resource::String::MSStoreDownloadPackageNotFound << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_MSSTORE_NO_APPLICABLE_PACKAGE); + licenseContent = downloadContext.GetLicense(downloadInfo.ContentId); + } + 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; } - auto preferredPackage = result.value(); + std::filesystem::path licenseFilePath = downloadDirectory / Utility::ConvertToUTF16(installer.ProductId + "_License.xml"); + std::ofstream licenseFile(licenseFilePath, std::ofstream::out | std::ofstream::trunc | std::ofstream::binary); + licenseFile.write((const char *)&licenseContent[0], licenseContent.size()); + licenseFile.flush(); + licenseFile.close(); - AICLI_LOG(Core, Info, << "WuCategoryId: " << preferredPackage.WuCategoryId); - AICLI_LOG(Core, Info, << "ContentId: " << preferredPackage.ContentId); + AICLI_LOG(CLI, Info, << "Getting MSStore package license success"); + context.Reporter.Info() << Resource::String::MSStoreDownloadGetLicenseSuccess(Utility::LocIndView{ licenseFilePath.u8string() }) << std::endl; } - - THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } void EnsureStorePolicySatisfied(Execution::Context& context) diff --git a/src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.h b/src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.h @@ -3,9 +3,7 @@ #pragma once #include "ExecutionContext.h" - -// ShellExecuteInstallerHandler handles installers run through ShellExecute. -// Exe, Wix, Nullsoft, Msi and Inno should be handled by this installer handler. +// MSStoreInstallerHandler handles msstore installers. namespace AppInstaller::CLI::Workflow { // Deploys the Store app. diff --git a/src/AppInstallerCLICore/Workflows/ShowFlow.cpp b/src/AppInstallerCLICore/Workflows/ShowFlow.cpp @@ -192,6 +192,7 @@ namespace AppInstaller::CLI::Workflow ShowSingleLineField(info, Resource::String::ShowLabelInstallerSha256, (installer->Sha256.empty()) ? "" : Utility::SHA256::ConvertToString(installer->Sha256), true); ShowSingleLineField(info, Resource::String::ShowLabelInstallerProductId, installer->ProductId, true); ShowSingleLineField(info, Resource::String::ShowLabelInstallerReleaseDate, installer->ReleaseDate, true); + ShowSingleLineField(info, Resource::String::ShowLabelInstallerOfflineDistributionSupported, Utility::ConvertBoolToString(!installer->DownloadCommandProhibited), true); const auto& dependencies = installer->Dependencies; @@ -263,4 +264,4 @@ namespace AppInstaller::CLI::Workflow GetManifestFromPackage(m_considerPins); } } -}- \ No newline at end of file +} diff --git a/src/AppInstallerCLICore/pch.h b/src/AppInstallerCLICore/pch.h @@ -32,12 +32,6 @@ #include <string_view> #include <vector> -#pragma warning( push ) -#pragma warning ( disable : 26495 26439 ) -#include <cpprest/http_client.h> -#include <cpprest/json.h> -#pragma warning( pop ) - #include <winrt/Windows.Foundation.h> #include <winrt/Windows.Foundation.Collections.h> #include <winrt/Windows.Management.Deployment.h> diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs @@ -206,16 +206,16 @@ namespace AppInstallerCLIE2ETests public const int ERROR_UNSUPPORTED_RESTSOURCE = unchecked((int)0x8A150038); public const int ERROR_RESTSOURCE_INVALID_DATA = unchecked((int)0x8A150039); public const int ERROR_BLOCKED_BY_POLICY = unchecked((int)0x8A15003A); - public const int ERROR_RESTSOURCE_INTERNAL_ERROR = unchecked((int)0x8A15003B); + public const int ERROR_RESTAPI_INTERNAL_ERROR = unchecked((int)0x8A15003B); public const int ERROR_RESTSOURCE_INVALID_URL = unchecked((int)0x8A15003C); - public const int ERROR_RESTSOURCE_UNSUPPORTED_MIME_TYPE = unchecked((int)0x8A15003D); + public const int ERROR_RESTAPI_UNSUPPORTED_MIME_TYPE = unchecked((int)0x8A15003D); public const int ERROR_RESTSOURCE_INVALID_VERSION = unchecked((int)0x8A15003E); public const int ERROR_SOURCE_DATA_INTEGRITY_FAILURE = unchecked((int)0x8A15003F); public const int ERROR_STREAM_READ_FAILURE = unchecked((int)0x8A150040); public const int ERROR_PACKAGE_AGREEMENTS_NOT_ACCEPTED = unchecked((int)0x8A150041); public const int ERROR_PROMPT_INPUT_ERROR = unchecked((int)0x8A150042); public const int ERROR_UNSUPPORTED_SOURCE_REQUEST = unchecked((int)0x8A150043); - public const int ERROR_RESTSOURCE_ENDPOINT_NOT_FOUND = unchecked((int)0x8A150044); + public const int ERROR_RESTAPI_ENDPOINT_NOT_FOUND = unchecked((int)0x8A150044); public const int ERROR_SOURCE_OPEN_FAILED = unchecked((int)0x8A150045); public const int ERROR_SOURCE_AGREEMENTS_NOT_ACCEPTED = unchecked((int)0x8A150046); public const int ERROR_CUSTOMHEADER_EXCEEDS_MAXLENGTH = unchecked((int)0x8A150047); diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="utf-8"?> <root> <!-- Microsoft ResX Schema @@ -1173,6 +1173,9 @@ Do you agree to the terms?</value> <data name="ShowLabelInstallerReleaseDate" xml:space="preserve"> <value>Release Date:</value> </data> + <data name="ShowLabelInstallerOfflineDistributionSupported" xml:space="preserve"> + <value>Offline Distribution Supported:</value> + </data> <data name="ShowLabelPublisherUrl" xml:space="preserve"> <value>Publisher Url:</value> </data> @@ -2272,14 +2275,14 @@ Please specify one of them using the --source option to proceed.</value> <data name="APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY" xml:space="preserve"> <value>Operation is blocked by Group Policy</value> </data> - <data name="APPINSTALLER_CLI_ERROR_RESTSOURCE_INTERNAL_ERROR" xml:space="preserve"> - <value>Rest source internal error</value> + <data name="APPINSTALLER_CLI_ERROR_RESTAPI_INTERNAL_ERROR" xml:space="preserve"> + <value>Rest API internal error</value> </data> <data name="APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_URL" xml:space="preserve"> <value>Invalid rest source url</value> </data> - <data name="APPINSTALLER_CLI_ERROR_RESTSOURCE_UNSUPPORTED_MIME_TYPE" xml:space="preserve"> - <value>Unsupported MIME type returned by rest source</value> + <data name="APPINSTALLER_CLI_ERROR_RESTAPI_UNSUPPORTED_MIME_TYPE" xml:space="preserve"> + <value>Unsupported MIME type returned by rest API</value> </data> <data name="APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_VERSION" xml:space="preserve"> <value>Invalid rest source contract version</value> @@ -2299,8 +2302,8 @@ Please specify one of them using the --source option to proceed.</value> <data name="APPINSTALLER_CLI_ERROR_UNSUPPORTED_SOURCE_REQUEST" xml:space="preserve"> <value>The search request is not supported by one or more sources</value> </data> - <data name="APPINSTALLER_CLI_ERROR_RESTSOURCE_ENDPOINT_NOT_FOUND" xml:space="preserve"> - <value>The rest source endpoint is not found.</value> + <data name="APPINSTALLER_CLI_ERROR_RESTAPI_ENDPOINT_NOT_FOUND" xml:space="preserve"> + <value>The rest API endpoint is not found.</value> </data> <data name="APPINSTALLER_CLI_ERROR_SOURCE_OPEN_FAILED" xml:space="preserve"> <value>Failed to open the source.</value> @@ -2883,11 +2886,75 @@ Please specify one of them using the --source option to proceed.</value> <data name="SourceListTrustLevel" xml:space="preserve"> <value>Trust Level</value> </data> - <data name="APPINSTALLER_CLI_ERROR_MSSTORE_NO_APPLICABLE_PACKAGE" xml:space="preserve"> - <value>No applicable MSStore package found.</value> + <data name="APPINSTALLER_CLI_ERROR_DISPLAYCATALOG_API_FAILED" xml:space="preserve"> + <value>Failed to get Microsoft Store package catalog.</value> + </data> + <data name="APPINSTALLER_CLI_ERROR_NO_APPLICABLE_DISPLAYCATALOG_PACKAGE" xml:space="preserve"> + <value>No applicable Microsoft Store package found from Microsoft Store package catalog.</value> + </data> + <data name="APPINSTALLER_CLI_ERROR_SFSCLIENT_API_FAILED" xml:space="preserve"> + <value>Failed to get Microsoft Store package download information.</value> + </data> + <data name="APPINSTALLER_CLI_ERROR_NO_APPLICABLE_SFSCLIENT_PACKAGE" xml:space="preserve"> + <value>No applicable Microsoft Store package download information found.</value> + </data> + <data name="APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED" xml:space="preserve"> + <value>Failed to retrieve Microsoft Store package license.</value> </data> <data name="MSStoreDownloadPackageNotFound" xml:space="preserve"> - <value>The MSStore package could not be found.</value> + <value>The Microsoft Store package could not be found.</value> + </data> + <data name="MSStoreDownloadPackageHashVerified" xml:space="preserve"> + <value>Successfully verified Microsoft Store package hash</value> + </data> + <data name="MSStoreDownloadPackageHashMismatch" xml:space="preserve"> + <value>Microsoft Store package hash mismatch</value> + </data> + <data name="MSStoreDownloadPackageDownloaded" xml:space="preserve"> + <value>Microsoft Store package downloaded: {0}</value> + <comment>{Locked="{0}"} Full path of the downloaded package.</comment> + </data> + <data name="MSStoreDownloadPackageDownloadFailed" xml:space="preserve"> + <value>Microsoft Store package download failed: {0}</value> + <comment>{Locked="{0}"} Package name.</comment> + </data> + <data name="MSStoreDownloadPackageDownloadSuccess" xml:space="preserve"> + <value>Microsoft Store package download completed</value> + </data> + <data name="MSStoreDownloadMainPackages" xml:space="preserve"> + <value>Downloading main packages from Microsoft Store...</value> + </data> + <data name="MSStoreDownloadDependencyPackages" xml:space="preserve"> + <value>Downloading dependency packages from Microsoft Store...</value> + </data> + <data name="MSStoreDownloadGetDownloadInfo" xml:space="preserve"> + <value>Retrieving Microsoft Store package download information</value> + </data> + <data name="MSStoreDownloadGetDownloadInfoFailed" xml:space="preserve"> + <value>Failed to retrieve Microsoft Store package download information</value> + </data> + <data name="MSStoreDownloadGetLicense" xml:space="preserve"> + <value>Retrieving Microsoft Store package license</value> + </data> + <data name="MSStoreDownloadGetLicenseSuccess" xml:space="preserve"> + <value>Microsoft Store package license saved: {0}</value> + <comment>{Locked="{0}"} License file full path.</comment> + </data> + <data name="MSStoreDownloadGetLicenseFailed" xml:space="preserve"> + <value>Failed to retrieve Microsoft Store package license</value> + </data> + <data name="MSStoreDownloadRenameNotSupported" xml:space="preserve"> + <value>Microsoft Store package download does not support --rename argument. Microsoft Store package will use names provided by Microsoft Store catalog.</value> + <comment>{Locked="--rename"}</comment> + </data> + <data name="MSStoreDownloadAuthenticationNotice" xml:space="preserve"> + <value>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.</value> + </data> + <data name="SkipMicrosoftStorePackageLicenseArgumentDescription" xml:space="preserve"> + <value>Skips retrieving Microsoft Store package offline license</value> + </data> + <data name="PlatformArgumentDescription" xml:space="preserve"> + <value>Select the target platform</value> </data> <data name="ConfigurationExportAddingToFile" xml:space="preserve"> <value>Adding configuration file: {0}</value> @@ -2940,4 +3007,4 @@ Please specify one of them using the --source option to proceed.</value> <value>&lt;this value has been truncated; inspect the file contents for the complete text&gt;</value> <comment>Keep some form of separator like the "&lt;&gt;" around the text so that it stands out from the preceding text.</comment> </data> -</root>- \ No newline at end of file +</root> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -150,7 +150,7 @@ <ClCompile> <Optimization>Disabled</Optimization> <PreprocessorDefinitions>_NO_ASYNCRTIMP;_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING;_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerSharedLib\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerSharedLib\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\SfsClient\sfs-client\client\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</TreatWarningAsError> <RuntimeTypeInfo Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">false</RuntimeTypeInfo> </ClCompile> @@ -169,7 +169,7 @@ <ItemDefinitionGroup Condition="'$(Platform)'=='Win32'"> <ClCompile> <PreprocessorDefinitions>_NO_ASYNCRTIMP;_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING;WIN32;%(PreprocessorDefinitions)</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerSharedLib\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerSharedLib\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\SfsClient\sfs-client\client\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</TreatWarningAsError> <RuntimeTypeInfo Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">false</RuntimeTypeInfo> </ClCompile> @@ -189,8 +189,8 @@ <FunctionLevelLinking>true</FunctionLevelLinking> <IntrinsicFunctions>true</IntrinsicFunctions> <PreprocessorDefinitions>_NO_ASYNCRTIMP;_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING;NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerSharedLib\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerSharedLib\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerSharedLib\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\SfsClient\sfs-client\client\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerSharedLib\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\SfsClient\sfs-client\client\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</TreatWarningAsError> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</TreatWarningAsError> <RuntimeTypeInfo Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">false</RuntimeTypeInfo> @@ -266,6 +266,7 @@ <ClCompile Include="MsiExecArguments.cpp" /> <ClCompile Include="MsixInfo.cpp" /> <ClCompile Include="MsixManifest.cpp" /> + <ClCompile Include="MSStoreDownloadFlow.cpp" /> <ClCompile Include="NameNormalization.cpp" /> <ClCompile Include="PackageCollection.cpp" /> <ClCompile Include="PackageDependenciesValidationUtil.cpp" /> @@ -335,6 +336,9 @@ <CopyFileToFolders Include="TestData\DownloadFlowTest_DownloadCommandProhibited.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\DownloadFlowTest_MSStore.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallFlowTest_EncodedUrl.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -146,6 +146,9 @@ <ClCompile Include="ManifestComparator.cpp"> <Filter>Source Files\CLI</Filter> </ClCompile> + <ClCompile Include="MSStoreDownloadFlow.cpp"> + <Filter>Source Files\CLI</Filter> + </ClCompile> <ClCompile Include="MsiExecArguments.cpp"> <Filter>Source Files\Common</Filter> </ClCompile> @@ -618,6 +621,9 @@ <CopyFileToFolders Include="TestData\DownloadFlowTest_DownloadCommandProhibited.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\DownloadFlowTest_MSStore.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallFlowTest_NoApplicableArchitecture.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> diff --git a/src/AppInstallerCLITests/CustomHeader.cpp b/src/AppInstallerCLITests/CustomHeader.cpp @@ -95,7 +95,7 @@ TEST_CASE("RestSourceSearch_NoCustomHeader", "[RestSource][CustomHeader]") headers.emplace(CustomHeaderName, customHeader); V1_1::Interface v1_1{ "https://restsource.com/api", std::move(helper), {}, {} }; - REQUIRE_THROWS_HR(v1_1.Search({}), APPINSTALLER_CLI_ERROR_RESTSOURCE_INTERNAL_ERROR); + REQUIRE_THROWS_HR(v1_1.Search({}), APPINSTALLER_CLI_ERROR_RESTAPI_INTERNAL_ERROR); } TEST_CASE("RestSourceSearch_CustomHeaderExceedingSize", "[RestSource][CustomHeader]") diff --git a/src/AppInstallerCLITests/HttpClientHelper.cpp b/src/AppInstallerCLITests/HttpClientHelper.cpp @@ -18,7 +18,7 @@ using namespace AppInstaller::Certificates; TEST_CASE("ExtractJsonResponse_UnsupportedMimeType", "[RestSource][RestSearch]") { HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, L"", web::http::details::mime_types::text_plain) }; - REQUIRE_THROWS_HR(helper.HandleGet(L"https://testUri"), APPINSTALLER_CLI_ERROR_RESTSOURCE_UNSUPPORTED_MIME_TYPE); + REQUIRE_THROWS_HR(helper.HandleGet(L"https://testUri"), APPINSTALLER_CLI_ERROR_RESTAPI_UNSUPPORTED_MIME_TYPE); } TEST_CASE("ValidateAndExtractResponse_ServiceUnavailable", "[RestSource]") @@ -30,7 +30,7 @@ TEST_CASE("ValidateAndExtractResponse_ServiceUnavailable", "[RestSource]") TEST_CASE("ValidateAndExtractResponse_NotFound", "[RestSource]") { HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::NotFound) }; - REQUIRE_THROWS_HR(helper.HandleGet(L"https://testUri"), APPINSTALLER_CLI_ERROR_RESTSOURCE_ENDPOINT_NOT_FOUND); + REQUIRE_THROWS_HR(helper.HandleGet(L"https://testUri"), APPINSTALLER_CLI_ERROR_RESTAPI_ENDPOINT_NOT_FOUND); } TEST_CASE("EnsureDefaultUserAgent", "[RestSource]") diff --git a/src/AppInstallerCLITests/MSStoreDownloadFlow.cpp b/src/AppInstallerCLITests/MSStoreDownloadFlow.cpp @@ -0,0 +1,593 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestHooks.h" +#include "TestRestRequestHandler.h" +#include "WorkflowCommon.h" +#include <AppInstallerStrings.h> +#include <AppInstallerSHA256.h> +#include <winget/JsonUtil.h> +#include <Commands/DownloadCommand.h> + +using namespace TestCommon; +using namespace AppInstaller::CLI; +using namespace AppInstaller::CLI::Execution; +using namespace AppInstaller::Settings; +using namespace AppInstaller::Utility::literals; + +utility::string_t TestDisplayCatalogResponse = _XPLATSTR( + R"delimiter( + { + "Product": { + "DisplaySkuAvailabilities": [ + { + "Sku": { + "SkuId": "0010", + "Properties": { + "Packages": [ + { + "PackageId": "PackageEnglish", + "Architectures": [ "x64", "arm" ], + "Languages": [ "en-US", "en-GB" ], + "PackageFormat": "Appx", + "ContentId": "LicenseContentId", + "FulfillmentData": { + "WuCategoryId": "TestCategoryIdEnglish" + } + }, + { + "PackageId": "PackageFrench", + "Architectures": [ "x64", "arm" ], + "Languages": [ "fr-FR" ], + "PackageFormat": "Appx", + "ContentId": "LicenseContentId", + "FulfillmentData": { + "WuCategoryId": "TestCategoryIdFrench" + } + } + ] + } + } + } + ] + } + })delimiter"); + +utility::string_t TestLicensingResponseRaw = _XPLATSTR( + R"delimiter( + { + "license": { + "keys": [ + { + "value": "<LicenseContent>" + } + ] + } + })delimiter"); + +std::string LicenseContent = "TestLicense"; + +utility::string_t TestLicensingResponse = AppInstaller::Utility::ReplaceWhileCopying( + TestLicensingResponseRaw, L"<LicenseContent>", + AppInstaller::Utility::ConvertToUTF16(AppInstaller::JSON::Base64Encode(std::vector<BYTE>{ LicenseContent.begin(), LicenseContent.end() }))); + +utility::string_t TestDisplayCatalogResponse_TargetSkuNotFound = _XPLATSTR( + R"delimiter( + { + "Product": { + "DisplaySkuAvailabilities": [ + { + "Sku": { + "SkuId": "0011", + "Properties": { + "Packages": [ + { + "PackageId": "PackageEnglish", + "Architectures": [ "x64", "arm" ], + "Languages": [ "en-US", "en-GB" ], + "PackageFormat": "Appx", + "ContentId": "LicenseContentId", + "FulfillmentData": { + "WuCategoryId": "TestCategoryIdEnglish" + } + } + ] + } + } + } + ] + } + })delimiter"); + +std::vector<SFS::AppContent> GetSfsAppContentsOverrideFunction(std::string_view wuCategoryId) +{ + std::string wuCategoryIdStr{ wuCategoryId }; + + std::vector<SFS::AppContent> result; + + std::unique_ptr<SFS::ContentId> contentId; + std::vector<SFS::AppPrerequisiteContent> dependencies; + std::vector<SFS::AppFile> packages; + std::unique_ptr<SFS::AppContent> appContent; + + std::vector<BYTE> sha256Bytes = AppInstaller::Utility::SHA256::ConvertToBytes("69D84CA8899800A5575CE31798223CD4FEBAB1D734A07C2E51E56A28E0DF8123"); + std::string base64EncodedSha256 = AppInstaller::JSON::Base64Encode(sha256Bytes); + + { + // Create dependencies content + std::unique_ptr<SFS::ContentId> dependencyContentId; + std::vector<SFS::AppFile> dependencyPackages; + std::unique_ptr<SFS::AppPrerequisiteContent> dependencyContent; + + std::ignore = SFS::ContentId::Make("testDependency", "testDependency", "1.0.0.0", dependencyContentId); + + std::unique_ptr<SFS::AppFile> dependencyX64; + std::ignore = SFS::AppFile::Make( + wuCategoryIdStr + ".appx", + "https://NotUsed/" + wuCategoryIdStr + "/dependency/x64", + 100, + { { SFS::HashType::Sha256, base64EncodedSha256 } }, + { SFS::Architecture::Amd64 }, + { "Universal=10.0.0.0" }, + wuCategoryIdStr + ".Dependency_1.2.3.4_x64__8wekyb3d8bbwe", + dependencyX64); + dependencyPackages.emplace_back(std::move(*dependencyX64)); + + std::unique_ptr<SFS::AppFile> dependencyArm; + std::ignore = SFS::AppFile::Make( + wuCategoryIdStr + ".appx", + "https://NotUsed/" + wuCategoryIdStr + "/dependency/arm", + 100, + { { SFS::HashType::Sha256, base64EncodedSha256 } }, + { SFS::Architecture::Arm }, + { "Universal=10.0.0.0" }, + wuCategoryIdStr + ".Dependency_1.2.3.4_arm__8wekyb3d8bbwe", + dependencyArm); + dependencyPackages.emplace_back(std::move(*dependencyArm)); + + std::ignore = SFS::AppPrerequisiteContent::Make(std::move(dependencyContentId), std::move(dependencyPackages), dependencyContent); + + dependencies.emplace_back(std::move(*dependencyContent)); + } + + { + // Create main packages + + // Good candidate x64 + std::unique_ptr<SFS::AppFile> packageX64; + std::ignore = SFS::AppFile::Make( + wuCategoryIdStr + ".appx", + "https://NotUsed/" + wuCategoryIdStr + "/x64", + 100, + { { SFS::HashType::Sha256, base64EncodedSha256 } }, + { SFS::Architecture::Amd64 }, + { "Desktop=10.0.0.0" }, + wuCategoryIdStr + "_1.0.0.0_x64__8wekyb3d8bbwe", + packageX64); + packages.emplace_back(std::move(*packageX64)); + + // Good candidate arm + std::unique_ptr<SFS::AppFile> packageArm; + std::ignore = SFS::AppFile::Make( + wuCategoryIdStr + ".appx", + "https://NotUsed/" + wuCategoryIdStr + "/arm", + 100, + { { SFS::HashType::Sha256, base64EncodedSha256 } }, + { SFS::Architecture::Arm }, + { "Desktop=10.0.0.0" }, + wuCategoryIdStr + "_1.0.0.0_arm__8wekyb3d8bbwe", + packageArm); + packages.emplace_back(std::move(*packageArm)); + + // Good candidate IoT + std::unique_ptr<SFS::AppFile> packageIoT; + std::ignore = SFS::AppFile::Make( + wuCategoryIdStr + ".appx", + "https://NotUsed/" + wuCategoryIdStr + "/IoT/arm", + 100, + { { SFS::HashType::Sha256, base64EncodedSha256 } }, + { SFS::Architecture::Arm }, + { "IoT=10.0.0.0" }, + wuCategoryIdStr + ".IoT_1.0.0.0_arm__8wekyb3d8bbwe", + packageIoT); + packages.emplace_back(std::move(*packageIoT)); + + // Good candidate IoT has newer version + std::unique_ptr<SFS::AppFile> packageIoT2; + std::ignore = SFS::AppFile::Make( + wuCategoryIdStr + ".appx", + "https://NotUsed/" + wuCategoryIdStr + "/IoT/arm/2.0", + 100, + { { SFS::HashType::Sha256, base64EncodedSha256 } }, + { SFS::Architecture::Arm }, + { "IoT=10.0.0.0" }, + wuCategoryIdStr + ".IoT_2.0.0.0_arm__8wekyb3d8bbwe", + packageIoT2); + packages.emplace_back(std::move(*packageIoT2)); + + // Candidate unsupported platform + std::unique_ptr<SFS::AppFile> packageXbox; + std::ignore = SFS::AppFile::Make( + wuCategoryIdStr + ".appx", + "https://NotUsed/" + wuCategoryIdStr + "/Xbox/arm", + 100, + { { SFS::HashType::Sha256, base64EncodedSha256 } }, + { SFS::Architecture::Amd64 }, + { "Xbox=10.0.0.0" }, + wuCategoryIdStr + ".Xbox_1.0.0.0_arm__8wekyb3d8bbwe", + packageXbox); + packages.emplace_back(std::move(*packageXbox)); + + // Candidate unsupported filetype + std::unique_ptr<SFS::AppFile> packageData; + std::ignore = SFS::AppFile::Make( + wuCategoryIdStr + ".cab", + "https://NotUsed/" + wuCategoryIdStr + "/cab", + 100, + { { SFS::HashType::Sha256, base64EncodedSha256 } }, + { SFS::Architecture::Amd64 }, + { "Desktop=10.0.0.0" }, + wuCategoryIdStr + ".Data_1.0.0.0_arm__8wekyb3d8bbwe", + packageData); + packages.emplace_back(std::move(*packageData)); + } + + std::ignore = SFS::ContentId::Make("test", "test", "1.0.0.0", contentId); + std::ignore = SFS::AppContent::Make(std::move(contentId), "updateId", std::move(dependencies), std::move(packages), appContent); + + result.emplace_back(std::move(*appContent)); + + return result; +} + +TEST_CASE("MSStoreDownloadFlow_Success", "[MSStoreDownloadFlow][workflow]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(AppInstaller::Settings::ExperimentalFeature::Feature::StoreDownload); + + 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::OK, TestLicensingResponse)); + 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(context.GetTerminationHR() == S_OK); + INFO(downloadOutput.str()); + + // 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")); + + // Verify license + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"9WZDNCRFJ364_License.xml")); + std::ifstream licenseFile(tempDirectory.GetPath() / L"9WZDNCRFJ364_License.xml"); + REQUIRE(licenseFile.is_open()); + std::string licenseFileStr; + std::getline(licenseFile, licenseFileStr); + 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")); +} + +TEST_CASE("MSStoreDownloadFlow_Success_SkipDependencies", "[MSStoreDownloadFlow][workflow]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(AppInstaller::Settings::ExperimentalFeature::Feature::StoreDownload); + + 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::OK, TestLicensingResponse)); + 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); + context.Args.AddArg(Execution::Args::Type::SkipDependencies); + + DownloadCommand download({}); + download.Execute(context); + REQUIRE(context.GetTerminationHR() == S_OK); + INFO(downloadOutput.str()); + + // 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")); + + // Verify license + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"9WZDNCRFJ364_License.xml")); + std::ifstream licenseFile(tempDirectory.GetPath() / L"9WZDNCRFJ364_License.xml"); + REQUIRE(licenseFile.is_open()); + std::string licenseFileStr; + std::getline(licenseFile, licenseFileStr); + REQUIRE(licenseFileStr == LicenseContent); +} + +TEST_CASE("MSStoreDownloadFlow_Success_SkipLicense", "[MSStoreDownloadFlow][workflow]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(AppInstaller::Settings::ExperimentalFeature::Feature::StoreDownload); + + 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::OK, TestLicensingResponse)); + 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); + context.Args.AddArg(Execution::Args::Type::SkipMicrosoftStorePackageLicense); + + DownloadCommand download({}); + download.Execute(context); + REQUIRE(context.GetTerminationHR() == S_OK); + INFO(downloadOutput.str()); + + // 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")); + + // Verify license + REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"9WZDNCRFJ364_License.xml")); +} + +TEST_CASE("MSStoreDownloadFlow_Success_SpecificLocale", "[MSStoreDownloadFlow][workflow]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(AppInstaller::Settings::ExperimentalFeature::Feature::StoreDownload); + + 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::OK, TestLicensingResponse)); + 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, "fr-FR"sv); + + DownloadCommand download({}); + download.Execute(context); + REQUIRE(context.GetTerminationHR() == S_OK); + INFO(downloadOutput.str()); + + // 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")); + + // Verify license + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"9WZDNCRFJ364_License.xml")); + std::ifstream licenseFile(tempDirectory.GetPath() / L"9WZDNCRFJ364_License.xml"); + REQUIRE(licenseFile.is_open()); + std::string licenseFileStr; + std::getline(licenseFile, licenseFileStr); + REQUIRE(licenseFileStr == LicenseContent); +} + +TEST_CASE("MSStoreDownloadFlow_Success_SpecificArchitecture", "[MSStoreDownloadFlow][workflow]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(AppInstaller::Settings::ExperimentalFeature::Feature::StoreDownload); + + 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::OK, TestLicensingResponse)); + 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); + context.Args.AddArg(Execution::Args::Type::InstallArchitecture, "x64"sv); + + DownloadCommand download({}); + download.Execute(context); + REQUIRE(context.GetTerminationHR() == S_OK); + INFO(downloadOutput.str()); + + // 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")); + + // Verify license + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"9WZDNCRFJ364_License.xml")); + std::ifstream licenseFile(tempDirectory.GetPath() / L"9WZDNCRFJ364_License.xml"); + REQUIRE(licenseFile.is_open()); + std::string licenseFileStr; + std::getline(licenseFile, licenseFileStr); + REQUIRE(licenseFileStr == LicenseContent); +} + +TEST_CASE("MSStoreDownloadFlow_Success_SpecificPlatform", "[MSStoreDownloadFlow][workflow]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(AppInstaller::Settings::ExperimentalFeature::Feature::StoreDownload); + + 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::OK, TestLicensingResponse)); + 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); + context.Args.AddArg(Execution::Args::Type::Platform, "Windows.IoT"sv); + + DownloadCommand download({}); + download.Execute(context); + REQUIRE(context.GetTerminationHR() == S_OK); + INFO(downloadOutput.str()); + + // 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")); + + // Verify license + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"9WZDNCRFJ364_License.xml")); + std::ifstream licenseFile(tempDirectory.GetPath() / L"9WZDNCRFJ364_License.xml"); + REQUIRE(licenseFile.is_open()); + std::string licenseFileStr; + std::getline(licenseFile, licenseFileStr); + REQUIRE(licenseFileStr == LicenseContent); +} + +TEST_CASE("MSStoreDownloadFlow_Fail_TargetSkuNotFound", "[MSStoreDownloadFlow][workflow]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(AppInstaller::Settings::ExperimentalFeature::Feature::StoreDownload); + + TestCommon::TempDirectory tempDirectory("TestDownloadDirectory", false); + + std::ostringstream downloadOutput; + TestContext context{ downloadOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + TestHook::SetDisplayCatalogHttpPipelineStage_Override displayCatalogOverride(GetTestRestRequestHandler(web::http::status_codes::OK, TestDisplayCatalogResponse_TargetSkuNotFound)); + TestHook::SetSfsClientAppContents_Override sfsClientOverride({ &GetSfsAppContentsOverrideFunction }); + TestHook::SetLicensingHttpPipelineStage_Override licensingOverride(GetTestRestRequestHandler(web::http::status_codes::OK, TestLicensingResponse)); + 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({}); + REQUIRE_THROWS_HR(download.Execute(context), APPINSTALLER_CLI_ERROR_NO_APPLICABLE_DISPLAYCATALOG_PACKAGE); + INFO(downloadOutput.str()); +} + +TEST_CASE("MSStoreDownloadFlow_Fail_LocaleNotApplicable", "[MSStoreDownloadFlow][workflow]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(AppInstaller::Settings::ExperimentalFeature::Feature::StoreDownload); + + TestCommon::TempDirectory tempDirectory("TestDownloadDirectory", false); + + std::ostringstream downloadOutput; + TestContext context{ downloadOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + 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::OK, TestLicensingResponse)); + 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, "ja-JP"sv); + + DownloadCommand download({}); + REQUIRE_THROWS_HR(download.Execute(context), APPINSTALLER_CLI_ERROR_NO_APPLICABLE_DISPLAYCATALOG_PACKAGE); + INFO(downloadOutput.str()); +} + +TEST_CASE("MSStoreDownloadFlow_Fail_ArchitectureNotApplicable", "[MSStoreDownloadFlow][workflow]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(AppInstaller::Settings::ExperimentalFeature::Feature::StoreDownload); + + TestCommon::TempDirectory tempDirectory("TestDownloadDirectory", false); + + std::ostringstream downloadOutput; + TestContext context{ downloadOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + 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::OK, TestLicensingResponse)); + 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); + context.Args.AddArg(Execution::Args::Type::InstallArchitecture, "arm64"sv); + + DownloadCommand download({}); + REQUIRE_THROWS_HR(download.Execute(context), APPINSTALLER_CLI_ERROR_NO_APPLICABLE_DISPLAYCATALOG_PACKAGE); + INFO(downloadOutput.str()); +} + +TEST_CASE("MSStoreDownloadFlow_Fail_PlatformNotApplicable", "[MSStoreDownloadFlow][workflow]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(AppInstaller::Settings::ExperimentalFeature::Feature::StoreDownload); + + TestCommon::TempDirectory tempDirectory("TestDownloadDirectory", false); + + std::ostringstream downloadOutput; + TestContext context{ downloadOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + 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::OK, TestLicensingResponse)); + 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); + 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); + INFO(downloadOutput.str()); +} + +TEST_CASE("MSStoreDownloadFlow_Fail_Licensing", "[MSStoreDownloadFlow][workflow]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(AppInstaller::Settings::ExperimentalFeature::Feature::StoreDownload); + + 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::Forbidden)); + 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({}); + REQUIRE_THROWS_HR(download.Execute(context), MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, web::http::status_codes::Forbidden)); + INFO(downloadOutput.str()); +} diff --git a/src/AppInstallerCLITests/MsixInfo.cpp b/src/AppInstallerCLITests/MsixInfo.cpp @@ -93,4 +93,10 @@ TEST_CASE("MsixInfo_ValidateMsixTrustInfo", "[msixinfo]") { UninstallCertFromSignedPackage(testSigned); } -}- \ No newline at end of file +} + +TEST_CASE("MsixInfo_GetPackageVersionFromFullName", "[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" }); +} diff --git a/src/AppInstallerCLITests/RestInterface_1_0.cpp b/src/AppInstallerCLITests/RestInterface_1_0.cpp @@ -395,7 +395,7 @@ TEST_CASE("Search_BadResponse_NotFoundCode", "[RestSource][Interface_1_0]") { HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::NotFound) }; Interface v1{ TestRestUriString, std::move(helper) }; - REQUIRE_THROWS_HR(v1.Search({}), APPINSTALLER_CLI_ERROR_RESTSOURCE_ENDPOINT_NOT_FOUND); + REQUIRE_THROWS_HR(v1.Search({}), APPINSTALLER_CLI_ERROR_RESTAPI_ENDPOINT_NOT_FOUND); } TEST_CASE("Search_Optimized_ManifestResponse", "[RestSource][Interface_1_0]") @@ -436,7 +436,7 @@ TEST_CASE("Search_Optimized_NoResponse_NotFoundCode", "[RestSource][Interface_1_ PackageMatchFilter filter{ PackageMatchField::Id, MatchType::Exact, "Foo" }; request.Filters.emplace_back(std::move(filter)); Interface v1{ TestRestUriString, std::move(helper) }; - REQUIRE_THROWS_HR(v1.Search(request), APPINSTALLER_CLI_ERROR_RESTSOURCE_ENDPOINT_NOT_FOUND); + REQUIRE_THROWS_HR(v1.Search(request), APPINSTALLER_CLI_ERROR_RESTAPI_ENDPOINT_NOT_FOUND); } TEST_CASE("GetManifests_GoodResponse", "[RestSource][Interface_1_0]") @@ -482,7 +482,7 @@ TEST_CASE("GetManifests_NotFoundCode", "[RestSource][Interface_1_0]") { HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::NotFound) }; Interface v1{ TestRestUriString, std::move(helper) }; - REQUIRE_THROWS_HR(v1.GetManifests("Foo.Bar"), APPINSTALLER_CLI_ERROR_RESTSOURCE_ENDPOINT_NOT_FOUND); + REQUIRE_THROWS_HR(v1.GetManifests("Foo.Bar"), APPINSTALLER_CLI_ERROR_RESTAPI_ENDPOINT_NOT_FOUND); } TEST_CASE("GetManifests_GoodResponse_UnknownInstaller", "[RestSource][Interface_1_0]") diff --git a/src/AppInstallerCLITests/TestCommon.cpp b/src/AppInstallerCLITests/TestCommon.cpp @@ -259,6 +259,9 @@ namespace TestCommon case Settings::ExperimentalFeature::Feature::SideBySide: result->Set<Settings::Setting::EFSideBySide>(true); break; + case Settings::ExperimentalFeature::Feature::StoreDownload: + result->Set<Settings::Setting::EFStoreDownload>(true); + break; default: THROW_HR(E_NOTIMPL); } diff --git a/src/AppInstallerCLITests/TestData/DownloadFlowTest_MSStore.yaml b/src/AppInstallerCLITests/TestData/DownloadFlowTest_MSStore.yaml @@ -0,0 +1,38 @@ +Id: AppInstallerCliTest.TestMSStoreDownload +Version: Latest +Name: AppInstaller Test MSStore Download +Publisher: Microsoft Corporation +AppMoniker: AICLITestMSStore +License: Test +Installers: + - Arch: x64 + Url: https://ThisIsNotUsed + InstallerType: msstore + ProductId: 9WZDNCRFJ364 + Language: en-US + - Arch: x64 + Url: https://ThisIsNotUsed + InstallerType: msstore + ProductId: 9WZDNCRFJ364 + Language: fr-FR + - Arch: arm + Url: https://ThisIsNotUsed + InstallerType: msstore + ProductId: 9WZDNCRFJ364 + Language: en-US + - Arch: arm + Url: https://ThisIsNotUsed + InstallerType: msstore + ProductId: 9WZDNCRFJ364 + Language: fr-FR + - Arch: x64 + Url: https://ThisIsNotUsed + InstallerType: msstore + ProductId: 9WZDNCRFJ364 + Language: ja-JP + - Arch: arm64 + Url: https://ThisIsNotUsed + InstallerType: msstore + ProductId: 9WZDNCRFJ364 + Language: en-US +ManifestVersion: 0.2.0-msstore diff --git a/src/AppInstallerCLITests/TestHooks.h b/src/AppInstallerCLITests/TestHooks.h @@ -13,6 +13,8 @@ #include <winget/Filesystem.h> #include <winget/IconExtraction.h> #include <winget/Authentication.h> +#include <winget/HttpClientHelper.h> +#include <sfsclient/SFSClient.h> #ifdef AICLI_DISABLE_TEST_HOOKS static_assert(false, "Test hooks have been disabled"); @@ -81,6 +83,15 @@ namespace AppInstaller { void TestHook_SetAuthenticationResult_Override(Authentication::AuthenticationResult* authResult); } + + namespace MSStore::TestHooks + { + void SetDisplayCatalogHttpPipelineStage_Override(std::shared_ptr<web::http::http_pipeline_stage> value); + + void SetSfsClientAppContents_Override(std::function<std::vector<SFS::AppContent>(std::string_view)>* value); + + void SetLicensingHttpPipelineStage_Override(std::shared_ptr<web::http::http_pipeline_stage> value); + } } namespace TestHook @@ -234,4 +245,46 @@ namespace TestHook private: AppInstaller::Authentication::AuthenticationResult m_authResult; }; -}- \ No newline at end of file + + struct SetDisplayCatalogHttpPipelineStage_Override + { + SetDisplayCatalogHttpPipelineStage_Override(std::shared_ptr<web::http::http_pipeline_stage> value) + { + AppInstaller::MSStore::TestHooks::SetDisplayCatalogHttpPipelineStage_Override(value); + } + + ~SetDisplayCatalogHttpPipelineStage_Override() + { + AppInstaller::MSStore::TestHooks::SetDisplayCatalogHttpPipelineStage_Override(nullptr); + } + }; + + struct SetSfsClientAppContents_Override + { + SetSfsClientAppContents_Override(std::function<std::vector<SFS::AppContent>(std::string_view)> value) : m_appContentsFunction(std::move(value)) + { + AppInstaller::MSStore::TestHooks::SetSfsClientAppContents_Override(&m_appContentsFunction); + } + + ~SetSfsClientAppContents_Override() + { + AppInstaller::MSStore::TestHooks::SetSfsClientAppContents_Override(nullptr); + } + + private: + std::function<std::vector<SFS::AppContent>(std::string_view)> m_appContentsFunction; + }; + + struct SetLicensingHttpPipelineStage_Override + { + SetLicensingHttpPipelineStage_Override(std::shared_ptr<web::http::http_pipeline_stage> value) + { + AppInstaller::MSStore::TestHooks::SetLicensingHttpPipelineStage_Override(value); + } + + ~SetLicensingHttpPipelineStage_Override() + { + AppInstaller::MSStore::TestHooks::SetLicensingHttpPipelineStage_Override(nullptr); + } + }; +} diff --git a/src/AppInstallerCLITests/WorkflowCommon.cpp b/src/AppInstallerCLITests/WorkflowCommon.cpp @@ -722,4 +722,17 @@ namespace TestCommon { } }); } -}- \ No newline at end of file + + void OverrideDownloadInstallerFileForMSStoreDownload(TestContext& context) + { + context.Override({ DownloadInstallerFile, [](TestContext& context) + { + const auto& installer = context.Get<Data::Installer>().value(); + const auto& installerPath = context.Get<Data::InstallerPath>(); + std::ofstream file(installerPath, std::ofstream::out | std::ofstream::trunc); + file << installer.Url; + file.close(); + context.Add<Data::HashPair>({ {}, {} }); + } }); + } +} diff --git a/src/AppInstallerCLITests/WorkflowCommon.h b/src/AppInstallerCLITests/WorkflowCommon.h @@ -139,4 +139,6 @@ namespace TestCommon void OverrideEnableWindowsFeaturesDependencies(TestContext& context); void OverrideRegisterStartupAfterReboot(TestContext& context); -}- \ No newline at end of file + + void OverrideDownloadInstallerFileForMSStoreDownload(TestContext& context); +} diff --git a/src/AppInstallerCommonCore/HttpClientHelper.cpp b/src/AppInstallerCommonCore/HttpClientHelper.cpp @@ -98,7 +98,7 @@ namespace AppInstaller::Http const HttpClientHelper::HttpRequestHeaders& authHeaders) const { web::http::http_response httpResponse; - HttpClientHelper::Post(uri, body, headers, authHeaders).then([&httpResponse](const web::http::http_response& response) + Post(uri, body, headers, authHeaders).then([&httpResponse](const web::http::http_response& response) { httpResponse = response; }).wait(); @@ -184,14 +184,14 @@ namespace AppInstaller::Http break; case web::http::status_codes::NotFound: - THROW_HR(APPINSTALLER_CLI_ERROR_RESTSOURCE_ENDPOINT_NOT_FOUND); + THROW_HR(APPINSTALLER_CLI_ERROR_RESTAPI_ENDPOINT_NOT_FOUND); case web::http::status_codes::NoContent: result = {}; break; case web::http::status_codes::BadRequest: - THROW_HR(APPINSTALLER_CLI_ERROR_RESTSOURCE_INTERNAL_ERROR); + THROW_HR(APPINSTALLER_CLI_ERROR_RESTAPI_INTERNAL_ERROR); case web::http::status_codes::TooManyRequests: case web::http::status_codes::ServiceUnavailable: @@ -208,7 +208,7 @@ namespace AppInstaller::Http { utility::string_t contentType = response.headers().content_type(); - THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_UNSUPPORTED_MIME_TYPE, + THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTAPI_UNSUPPORTED_MIME_TYPE, !contentType._Starts_with(web::http::details::mime_types::application_json)); return response.extract_json().get(); diff --git a/src/AppInstallerCommonCore/MSStoreDownload.cpp b/src/AppInstallerCommonCore/MSStoreDownload.cpp @@ -4,488 +4,1038 @@ #include <AppInstallerStrings.h> #include <AppInstallerErrors.h> #include <AppinstallerLogging.h> -#include <AppInstallerRuntime.h> -#include <winget/Locale.h> -#include <winget/MSStoreDownload.h> -#include <winget/Rest.h> -#include <winget/UserSettings.h> +#include "AppInstallerMsixInfo.h" +#include "AppInstallerRuntime.h" +#include "winget/Locale.h" +#include "winget/JsonUtil.h" +#include "winget/MSStoreDownload.h" +#include "winget/Rest.h" +#include "winget/HttpClientHelper.h" +#include "winget/UserSettings.h" +#ifndef WINGET_DISABLE_FOR_FUZZING +#include <sfsclient/SFSClient.h> +#endif namespace AppInstaller::MSStore { using namespace std::string_view_literals; - namespace +#ifndef AICLI_DISABLE_TEST_HOOKS + namespace TestHooks { - constexpr std::string_view Architectures = "Architectures"sv; - constexpr std::string_view ContentId = "ContentId"sv; - constexpr std::string_view Neutral = "Neutral"sv; - constexpr std::string_view DefaultSkuIdValue = "0010"sv; - constexpr std::string_view Details = "Details"sv; + static std::shared_ptr<web::http::http_pipeline_stage> s_DisplayCatalog_HttpPipelineStage_Override = nullptr; + + void SetDisplayCatalogHttpPipelineStage_Override(std::shared_ptr<web::http::http_pipeline_stage> value) + { + s_DisplayCatalog_HttpPipelineStage_Override = value; + } + + static std::function<std::vector<SFS::AppContent>(std::string_view)>* s_SfsClient_AppContents_Override = nullptr; + + void SetSfsClientAppContents_Override(std::function<std::vector<SFS::AppContent>(std::string_view)>* value) + { + s_SfsClient_AppContents_Override = value; + } + + static std::shared_ptr<web::http::http_pipeline_stage> s_Licensing_HttpPipelineStage_Override = nullptr; + + void SetLicensingHttpPipelineStage_Override(std::shared_ptr<web::http::http_pipeline_stage> value) + { + s_Licensing_HttpPipelineStage_Override = value; + } + } +#endif + + namespace DisplayCatalogDetails + { + // Default preferred sku to use + constexpr std::string_view TargetSkuIdValue = "0010"sv; + + // Json response fields + constexpr std::string_view Product = "Product"sv; constexpr std::string_view DisplaySkuAvailabilities = "DisplaySkuAvailabilities"sv; - constexpr std::string_view FulfillmentData = "FulfillmentData"sv; + constexpr std::string_view Sku = "Sku"sv; + constexpr std::string_view SkuId = "SkuId"sv; + constexpr std::string_view Properties = "Properties"sv; + constexpr std::string_view Packages = "Packages"sv; constexpr std::string_view Languages = "Languages"sv; constexpr std::string_view PackageFormat = "PackageFormat"sv; constexpr std::string_view PackageId = "PackageId"sv; - constexpr std::string_view Packages = "Packages"sv; - constexpr std::string_view PreferredSkuId = "PreferredSkuId"sv; - constexpr std::string_view Product = "Product"sv; - constexpr std::string_view Properties = "Properties"sv; - constexpr std::string_view Sku = "Sku"sv; - constexpr std::string_view SkuId = "SkuId"sv; + constexpr std::string_view Architectures = "Architectures"sv; + constexpr std::string_view ContentId = "ContentId"sv; + constexpr std::string_view FulfillmentData = "FulfillmentData"sv; constexpr std::string_view WuCategoryId = "WuCategoryId"sv; - constexpr std::string_view MSStoreCatalogRestApi = R"(https://displaycatalog.mp.microsoft.com/v7.0/products/{0}?fieldsTemplate={1}&market={2}&languages={3})"; - bool IsPackageFormatBundle(PackageFormatEnum packageFormatEnum) - { - return - packageFormatEnum == PackageFormatEnum::AppxBundle || - packageFormatEnum == PackageFormatEnum::MsixBundle; - } + // Display catalog rest endpoint + constexpr std::string_view DisplayCatalogRestApi = R"(https://displaycatalog.mp.microsoft.com/v7.0/products/{0}?fieldsTemplate={1}&market={2}&languages={3})"; + constexpr std::string_view Details = "Details"sv; + constexpr std::string_view Neutral = "Neutral"sv; - struct PackageFormatComparator : public details::MSStoreCatalogPackageComparisonField + enum class DisplayCatalogPackageFormatEnum { - PackageFormatComparator() : details::MSStoreCatalogPackageComparisonField("Package Format") {} + Unknown, + AppxBundle, + MsixBundle, + Appx, + Msix, + }; - bool IsApplicable(const MSStoreCatalogPackage& package) override + DisplayCatalogPackageFormatEnum ConvertToPackageFormatEnum(std::string_view packageFormatStr) + { + std::string packageFormat = Utility::ToLower(packageFormatStr); + if (packageFormat == "appxbundle") { - if (package.PackageFormat == PackageFormatEnum::EAppxBundle) - { - return false; - } - - return true; + return DisplayCatalogPackageFormatEnum::AppxBundle; } - - bool IsFirstBetter(const MSStoreCatalogPackage& first, const MSStoreCatalogPackage& second) override + else if (packageFormat == "msixbundle") { - return IsPackageFormatBundle(first.PackageFormat) && !IsPackageFormatBundle(second.PackageFormat); + return DisplayCatalogPackageFormatEnum::MsixBundle; } + else if (packageFormat == "appx") + { + return DisplayCatalogPackageFormatEnum::Appx; + } + else if (packageFormat == "msix") + { + return DisplayCatalogPackageFormatEnum::Msix; + } + + AICLI_LOG(Core, Info, << "ConvertToPackageFormatEnum: Unknown package format: " << packageFormatStr); + return DisplayCatalogPackageFormatEnum::Unknown; + } + + struct DisplayCatalogPackage + { + std::string PackageId; + + std::vector<AppInstaller::Utility::Architecture> Architectures; + + std::vector<std::string> Languages; + + DisplayCatalogPackageFormatEnum PackageFormat = DisplayCatalogPackageFormatEnum::Unknown; + + // To be used later in sfs-client + std::string WuCategoryId; + + // To be used later in licensing + std::string ContentId; }; - struct LanguageComparator : public details::MSStoreCatalogPackageComparisonField + // Display catalog package comparison logic. + // The comparator follows similar logic as ManifestComparator. + namespace DisplayCatalogPackageComparison { - LanguageComparator(std::vector<std::string> preference, std::vector<std::string> requirement) : - details::MSStoreCatalogPackageComparisonField("Language"), m_preference(std::move(preference)), m_requirement(std::move(requirement)) + struct DisplayCatalogPackageComparisonField { - AICLI_LOG(Core, Verbose, - << "Language Comparator created with Required Languages: " << Utility::ConvertContainerToString(m_requirement) - << " , Preferred Languages: " << Utility::ConvertContainerToString(m_preference)); - } + DisplayCatalogPackageComparisonField(std::string_view name) : m_name(name) {} - static std::unique_ptr<LanguageComparator> Create(const std::vector<std::string>& requiredLanguages) + virtual ~DisplayCatalogPackageComparisonField() = default; + + std::string_view Name() const { return m_name; } + + virtual bool IsApplicable(const DisplayCatalogPackage& package) = 0; + + virtual bool IsFirstBetter(const DisplayCatalogPackage& first, const DisplayCatalogPackage& second) = 0; + + private: + std::string_view m_name; + }; + + struct PackageFormatComparator : public DisplayCatalogPackageComparisonField { - std::vector<std::string> requirement = requiredLanguages; - if (requirement.empty()) + PackageFormatComparator() : DisplayCatalogPackageComparisonField("Package Format") {} + + bool IsApplicable(const DisplayCatalogPackage& package) override { - requirement = Settings::User().Get<Settings::Setting::InstallLocaleRequirement>(); + return package.PackageFormat != DisplayCatalogPackageFormatEnum::Unknown; } - std::vector<std::string> preference = Settings::User().Get<Settings::Setting::InstallLocalePreference>(); - if (preference.empty()) + bool IsFirstBetter(const DisplayCatalogPackage& first, const DisplayCatalogPackage& second) override { - preference = AppInstaller::Locale::GetUserPreferredLanguages(); + return IsPackageFormatBundle(first) && !IsPackageFormatBundle(second); } - if (!preference.empty() || !requirement.empty()) + private: + bool IsPackageFormatBundle(const DisplayCatalogPackage& package) { - return std::make_unique<LanguageComparator>(preference, requirement); + return + package.PackageFormat == DisplayCatalogPackageFormatEnum::AppxBundle || + package.PackageFormat == DisplayCatalogPackageFormatEnum::MsixBundle; } - else + }; + + struct LocaleComparator : public DisplayCatalogPackageComparisonField + { + LocaleComparator(std::string locale) : DisplayCatalogPackageComparisonField("Locale") { - return {}; + if (!locale.empty()) + { + m_locales.emplace_back(std::move(locale)); + m_isRequirement = true; + } + else + { + m_locales = Locale::GetUserPreferredLanguages(); + } + + AICLI_LOG(Core, Verbose, + << "Locale Comparator created with locales: " << Utility::ConvertContainerToString(m_locales) + << " , Is requirement: " << m_isRequirement); } - } - bool IsApplicable(const MSStoreCatalogPackage& package) override - { - if (!m_requirement.empty()) + bool IsApplicable(const DisplayCatalogPackage& package) override { - for (auto const& requiredLanguage : m_requirement) + if (m_isRequirement) { - double distanceScore = GetBestDistanceScoreFromList(requiredLanguage, package.Languages); - if (distanceScore >= Locale::MinimumDistanceScoreAsPerfectMatch) + for (auto const& locale : m_locales) { - return true; + double distanceScore = GetBestDistanceScoreFromList(locale, package.Languages); + if (distanceScore >= Locale::MinimumDistanceScoreAsCompatibleMatch) + { + return true; + } + } + + return false; + } + else + { + return true; + } + } + + bool IsFirstBetter(const DisplayCatalogPackage& first, const DisplayCatalogPackage& second) + { + for (auto const& locale : m_locales) + { + double firstScore = GetBestDistanceScoreFromList(locale, first.Languages); + double secondScore = GetBestDistanceScoreFromList(locale, second.Languages); + + if (firstScore >= Locale::MinimumDistanceScoreAsCompatibleMatch || secondScore >= Locale::MinimumDistanceScoreAsCompatibleMatch) + { + return firstScore > secondScore; } } return false; } - else + + private: + double GetBestDistanceScoreFromList(std::string_view targetLocale, const std::vector<std::string>& locales) { - return true; + double finalScore = 0; + for (auto const& locale : locales) + { + double currentScore = Locale::GetDistanceOfLanguage(targetLocale, locale); + if (currentScore > finalScore) + { + finalScore = currentScore; + } + } + + return finalScore; } - } - bool IsFirstBetter(const MSStoreCatalogPackage& first, const MSStoreCatalogPackage& second) + std::vector<std::string> m_locales; + bool m_isRequirement = false; + }; + + struct ArchitectureComparator : public DisplayCatalogPackageComparisonField { - if (m_preference.empty()) + ArchitectureComparator(Utility::Architecture architecture) : DisplayCatalogPackageComparisonField("Architecture") { - return false; + if (architecture != Utility::Architecture::Unknown) + { + m_architectures.emplace_back(architecture); + m_isRequirement = true; + } + else + { + m_architectures = Utility::GetApplicableArchitectures(); + } + + AICLI_LOG(Core, Verbose, + << "Architecture Comparator created with archs: " << Utility::ConvertContainerToString(m_architectures, Utility::ToString) + << " , Is requirement: " << m_isRequirement); } - for (auto const& preferredLanguage : m_preference) + bool IsApplicable(const DisplayCatalogPackage& package) override { - double firstScore = GetBestDistanceScoreFromList(preferredLanguage, first.Languages); - double secondScore = GetBestDistanceScoreFromList(preferredLanguage, second.Languages); + if (m_isRequirement) + { + for (auto arch : package.Architectures) + { + if (Utility::IsApplicableArchitecture(arch, m_architectures) > Utility::InapplicableArchitecture) + { + return true; + } + } - if (firstScore >= Locale::MinimumDistanceScoreAsCompatibleMatch || secondScore >= Locale::MinimumDistanceScoreAsCompatibleMatch) + return false; + } + else { - return firstScore > secondScore; + return true; } } - return false; - } - - private: - double GetBestDistanceScoreFromList(std::string_view targetLanguage, const std::vector<std::string>& languages) - { - double finalScore{}; - for (auto const& language : languages) + bool IsFirstBetter(const DisplayCatalogPackage& first, const DisplayCatalogPackage& second) override { - double currentScore = Locale::GetDistanceOfLanguage(targetLanguage, language); - if (currentScore > finalScore) + for (auto arch : m_architectures) { - finalScore = currentScore; - } - } + auto firstItr = std::find(first.Architectures.begin(), first.Architectures.end(), arch); + auto secondItr = std::find(second.Architectures.begin(), second.Architectures.end(), arch); - return finalScore; - } + if (firstItr != first.Architectures.end() && secondItr == second.Architectures.end()) + { + true; + } + else if (secondItr != second.Architectures.end()) + { + return false; + } + } - std::vector<std::string> m_preference; - std::vector<std::string> m_requirement; - }; + return false; + } - struct ArchitectureComparator : public details::MSStoreCatalogPackageComparisonField - { - ArchitectureComparator(std::vector<Utility::Architecture> requirement, std::vector<Utility::Architecture> preference) : - details::MSStoreCatalogPackageComparisonField("Architecture"), m_requirement(std::move(requirement)), m_preference(std::move(preference)) - { - AICLI_LOG(Core, Verbose, - << "Architecture Comparator created with required archs: " << Utility::ConvertContainerToString(m_requirement, Utility::ToString) - << " , Preferred archs: " << Utility::ConvertContainerToString(m_preference, Utility::ToString)); - } + private: + std::vector<Utility::Architecture> m_architectures; + bool m_isRequirement = false; + }; - static std::unique_ptr<ArchitectureComparator> Create(const std::vector<Utility::Architecture>& allowedArchitectures) + struct DisplayCatalogPackageComparator { - std::vector<Utility::Architecture> requiredArchitectures = allowedArchitectures; + DisplayCatalogPackageComparator(std::string requiredLocale, Utility::Architecture requiredArch) + { + // Order of comparators matters. + AddComparator(std::make_unique<LocaleComparator>(requiredLocale)); + AddComparator(std::make_unique<ArchitectureComparator>(requiredArch)); + AddComparator(std::make_unique<PackageFormatComparator>()); + } - if (requiredArchitectures.empty()) + // Gets the best installer from the manifest, if at least one is applicable. + std::optional<DisplayCatalogPackage> GetPreferredPackage(const std::vector<DisplayCatalogPackage>& packages) { - requiredArchitectures = Settings::User().Get<Settings::Setting::InstallArchitectureRequirement>(); + AICLI_LOG(Core, Verbose, << "Starting display catalog package selection."); + + const DisplayCatalogPackage* result = nullptr; + for (const auto& package : packages) + { + if (IsApplicable(package) && (!result || IsFirstBetter(package, *result))) + { + result = &package; + } + } + + if (result) + { + return *result; + } + else + { + return {}; + } } - std::vector<Utility::Architecture> optionalArchitectures = Settings::User().Get<Settings::Setting::InstallArchitecturePreference>(); + // Determines if the package is applicable. + bool IsApplicable(const DisplayCatalogPackage& package) + { + for (const auto& comparator : m_comparators) + { + if (!comparator->IsApplicable(package)) + { + return false; + } + } + + return true; + } - if (!requiredArchitectures.empty() || !optionalArchitectures.empty()) + // Determines if the first package is a better choice. + bool IsFirstBetter(const DisplayCatalogPackage& first, const DisplayCatalogPackage& second) { - return std::make_unique<ArchitectureComparator>(std::move(requiredArchitectures), std::move(optionalArchitectures)); + for (const auto& comparator : m_comparators) + { + bool forwardCompare = comparator->IsFirstBetter(first, second); + bool reverseCompare = comparator->IsFirstBetter(second, first); + + if (forwardCompare && reverseCompare) + { + AICLI_LOG(Core, Error, << "Packages are both better than each other?"); + THROW_HR(E_UNEXPECTED); + } + + if (forwardCompare && !reverseCompare) + { + AICLI_LOG(Core, Verbose, << "Package " << first.PackageId << " is better than " << second.PackageId); + return true; + } + } + + AICLI_LOG(Core, Verbose, << "Package " << first.PackageId << " is equivalent in priority to " << second.PackageId); + return false; } - else + + private: + void AddComparator(std::unique_ptr<DisplayCatalogPackageComparisonField>&& comparator) { - return {}; + if (comparator) + { + m_comparators.emplace_back(std::move(comparator)); + } } - } - bool IsApplicable(const MSStoreCatalogPackage& package) override + std::vector<std::unique_ptr<DisplayCatalogPackageComparisonField>> m_comparators; + }; + } + + // Display catalog API invocation and handling + + utility::string_t GetDisplayCatalogRestApi(std::string_view productId, std::string_view locale) + { + std::vector<Utility::LocIndString> locales; + if (!locale.empty()) { - if (!m_requirement.empty()) + locales.emplace_back(locale); + } + else + { + for (auto const& localeEntry : Locale::GetUserPreferredLanguages()) { - return ContainCommonArchitectures(m_requirement, package.Architectures); + locales.emplace_back(localeEntry); } + } + + // Neutral is always added + locales.emplace_back(Neutral); + + auto restEndpoint = AppInstaller::Utility::Format(std::string{ DisplayCatalogRestApi }, + productId, Details, AppInstaller::Runtime::GetOSRegion(), Utility::Join(Utility::LocIndView(","), locales)); + + return JSON::GetUtilityString(restEndpoint); + } + + // Response format: + // { + // "Product": { + // "DisplaySkuAvailabilities": [ + // { + // "Sku": { + // "SkuId": "0010", + // ... Sku Contents ... + // } + // } + // ] + // } + // } + std::reference_wrapper<const web::json::value> GetSkuNodeFromDisplayCatalogResponse(const web::json::value& responseObject) + { + AICLI_LOG(Core, Info, << "Started parsing display catalog response. Try to find target sku: " << TargetSkuIdValue); - return true; + if (responseObject.is_null()) + { + AICLI_LOG(Core, Error, << "Missing DisplayCatalog Response json object."); + THROW_HR(APPINSTALLER_CLI_ERROR_DISPLAYCATALOG_API_FAILED); } - bool IsFirstBetter(const MSStoreCatalogPackage& first, const MSStoreCatalogPackage& second) override + std::optional<std::reference_wrapper<const web::json::value>> product = JSON::GetJsonValueFromNode(responseObject, JSON::GetUtilityString(Product)); + if (!product) { - if (!m_preference.empty()) - { - return (ContainCommonArchitectures(first.Architectures, m_preference) && !ContainCommonArchitectures(second.Architectures, m_preference)); - } + AICLI_LOG(Core, Error, << "Missing Product node"); + THROW_HR(APPINSTALLER_CLI_ERROR_DISPLAYCATALOG_API_FAILED); + } - return false; + auto skuEntries = JSON::GetRawJsonArrayFromJsonNode(product.value().get(), JSON::GetUtilityString(DisplaySkuAvailabilities)); + if (!skuEntries) + { + AICLI_LOG(Core, Error, << "Missing DisplaySkuAvailabilities"); + THROW_HR(APPINSTALLER_CLI_ERROR_DISPLAYCATALOG_API_FAILED); } - private: - // Checks if two lists of architectures have common elements. - bool ContainCommonArchitectures(const std::vector<Utility::Architecture>& firstList, const std::vector<Utility::Architecture>& secondList) + for (const auto& skuEntry : skuEntries.value().get()) { - for (auto arch : firstList) + std::optional<std::reference_wrapper<const web::json::value>> sku = JSON::GetJsonValueFromNode(skuEntry, JSON::GetUtilityString(Sku)); + if (!sku) { - if (IsArchitectureInList(arch, secondList)) - { - return true; - } + AICLI_LOG(Core, Error, << "Missing Sku"); + THROW_HR(APPINSTALLER_CLI_ERROR_DISPLAYCATALOG_API_FAILED); } - return false; + const auto& skuValue = sku.value().get(); + auto skuId = JSON::GetRawStringValueFromJsonNode(skuValue, JSON::GetUtilityString(SkuId)).value_or(""); + if (TargetSkuIdValue == skuId) + { + AICLI_LOG(Core, Info, << "Target Sku (" << TargetSkuIdValue << ") found"); + return skuValue; + } } - bool IsArchitectureInList(Utility::Architecture arch, const std::vector<Utility::Architecture>& architectureList) + AICLI_LOG(Core, Error, << "Target Sku (" << TargetSkuIdValue << ") not found"); + THROW_HR(APPINSTALLER_CLI_ERROR_NO_APPLICABLE_DISPLAYCATALOG_PACKAGE); + } + + // Response format: + // { + // "Sku": { + // "Properties": { + // "Packages": [ + // { + // "PackageId": "package id", + // "Architectures": [ "x86", "x64" ], + // "Languages": [ "en", "fr" ], + // "PackageFormat": "AppxBundle", + // "ContentId": "guid", + // "FulfillmentData": { + // "WuCategoryId": "guid", + // } + // } + // ] + // } + // } + // } + std::vector<DisplayCatalogPackage> GetDisplayCatalogPackagesFromSkuNode(const web::json::value& jsonObject) + { + AICLI_LOG(Core, Info, << "Started extracting display catalog packages from sku."); + + std::optional<std::reference_wrapper<const web::json::value>> properties = JSON::GetJsonValueFromNode(jsonObject, JSON::GetUtilityString(Properties)); + if (!properties) { - return architectureList.end() != std::find_if( - architectureList.begin(), - architectureList.end(), - [&](const auto& a) { return a == arch; }); + AICLI_LOG(Core, Error, << "Missing Properties"); + THROW_HR(APPINSTALLER_CLI_ERROR_DISPLAYCATALOG_API_FAILED); } - std::vector<Utility::Architecture> m_requirement; - std::vector<Utility::Architecture> m_preference; - }; - } + const auto& propertiesValue = properties.value().get(); + auto packages = JSON::GetRawJsonArrayFromJsonNode(propertiesValue, JSON::GetUtilityString(Packages)); + if (!packages) + { + AICLI_LOG(Core, Error, << "Missing Packages"); + THROW_HR(APPINSTALLER_CLI_ERROR_DISPLAYCATALOG_API_FAILED); + } - DisplayCatalogPackageComparator::DisplayCatalogPackageComparator(const std::vector<std::string>& requiredLanguages, const std::vector<Utility::Architecture>& requiredArchs) - { - // Order of comparators matters. - AddComparator(LanguageComparator::Create(requiredLanguages)); - AddComparator(ArchitectureComparator::Create(requiredArchs)); - AddComparator(std::make_unique<PackageFormatComparator>()); - } + std::vector<DisplayCatalogPackage> displayCatalogPackages; - void DisplayCatalogPackageComparator::AddComparator(std::unique_ptr<details::MSStoreCatalogPackageComparisonField>&& comparator) - { - if (comparator) - { - m_comparators.emplace_back(std::move(comparator)); - } - } + for (const auto& packageEntry : packages.value().get()) + { + DisplayCatalogPackage catalogPackage; - std::optional<MSStoreCatalogPackage> DisplayCatalogPackageComparator::GetPreferredPackage(const std::vector<MSStoreCatalogPackage>& packages) - { - AICLI_LOG(Core, Verbose, << "Starting MSStore package selection."); + // Package Id + catalogPackage.PackageId = JSON::GetRawStringValueFromJsonNode(packageEntry, JSON::GetUtilityString(PackageId)).value_or(""); + // Architectures + auto architectures = JSON::GetRawStringArrayFromJsonNode(packageEntry, JSON::GetUtilityString(Architectures)); + for (const auto& arch : architectures) + { + auto archEnum = Utility::ConvertToArchitectureEnum(arch); + if (archEnum != Utility::Architecture::Unknown) + { + catalogPackage.Architectures.emplace_back(archEnum); + } + } + // Languages + auto languages = JSON::GetRawStringArrayFromJsonNode(packageEntry, JSON::GetUtilityString(Languages)); + for (const auto& language : languages) + { + catalogPackage.Languages.emplace_back(language); + } + // Package Format + auto packageFormat = JSON::GetRawStringValueFromJsonNode(packageEntry, JSON::GetUtilityString(PackageFormat)).value_or(""); + catalogPackage.PackageFormat = ConvertToPackageFormatEnum(packageFormat); + // Content Id + catalogPackage.ContentId = JSON::GetRawStringValueFromJsonNode(packageEntry, JSON::GetUtilityString(ContentId)).value_or(""); + if (catalogPackage.ContentId.empty()) + { + AICLI_LOG(Core, Warning, << "Missing ContentId"); + // ContentId is required for licensing. Skip this package if missing. + continue; + } + // WuCategoryId + std::optional<std::reference_wrapper<const web::json::value>> fulfillmentData = JSON::GetJsonValueFromNode(packageEntry, JSON::GetUtilityString(FulfillmentData)); + if (!fulfillmentData) + { + AICLI_LOG(Core, Warning, << "Missing FulfillmentData"); + // WuCategoryId is required for sfs-client. Skip this package if missing. + continue; + } + catalogPackage.WuCategoryId = JSON::GetRawStringValueFromJsonNode(fulfillmentData.value().get(), JSON::GetUtilityString(WuCategoryId)).value_or(""); + if (catalogPackage.WuCategoryId.empty()) + { + AICLI_LOG(Core, Warning, << "Missing WuCategoryId"); + // WuCategoryId is required for sfs-client. Skip this package if missing. + continue; + } - const MSStoreCatalogPackage* result = nullptr; - for (const auto& package : packages) - { - if (IsApplicable(package) && (!result || IsFirstBetter(package, *result))) - { - result = &package; + displayCatalogPackages.emplace_back(std::move(catalogPackage)); } - } - if (result) - { - return *result; - } - else - { - return {}; + return displayCatalogPackages; } - } - bool DisplayCatalogPackageComparator::IsFirstBetter(const MSStoreCatalogPackage& first, const MSStoreCatalogPackage& second) - { - for (const auto& comparator : m_comparators) + DisplayCatalogPackage CallDisplayCatalogAndGetPreferredPackage(std::string_view productId, std::string_view locale, Utility::Architecture architecture) { - bool forwardCompare = comparator->IsFirstBetter(first, second); - bool reverseCompare = comparator->IsFirstBetter(second, first); + AICLI_LOG(Core, Info, << "CallDisplayCatalogAndGetPreferredPackage with ProductId: " << productId << " Locale: " << locale << " Architecture: " << Utility::ToString(architecture)); + + auto displayCatalogApi = GetDisplayCatalogRestApi(productId, locale); + + AppInstaller::Http::HttpClientHelper httpClientHelper; - if (forwardCompare && reverseCompare) +#ifndef AICLI_DISABLE_TEST_HOOKS + if (TestHooks::s_DisplayCatalog_HttpPipelineStage_Override) { - AICLI_LOG(Core, Error, << "Packages are both better than each other?"); - THROW_HR(E_UNEXPECTED); + httpClientHelper = AppInstaller::Http::HttpClientHelper{ TestHooks::s_DisplayCatalog_HttpPipelineStage_Override }; } +#endif - if (forwardCompare && !reverseCompare) + std::optional<web::json::value> displayCatalogResponseObject = httpClientHelper.HandleGet(displayCatalogApi); + + if (!displayCatalogResponseObject) { - AICLI_LOG(Core, Verbose, << "Package " << first.PackageId << " is better than " << second.PackageId); - return true; + AICLI_LOG(Core, Error, << "No display catalog json object found"); + THROW_HR(APPINSTALLER_CLI_ERROR_DISPLAYCATALOG_API_FAILED); } - } - AICLI_LOG(Core, Verbose, << "Package " << first.PackageId << " is equivalent in priority to " << second.PackageId); - return false; - } + const auto& sku = GetSkuNodeFromDisplayCatalogResponse(displayCatalogResponseObject.value()); + auto displayCatalogPackages = GetDisplayCatalogPackagesFromSkuNode(sku.get()); - bool DisplayCatalogPackageComparator::IsApplicable(const MSStoreCatalogPackage& package) - { - for (const auto& comparator : m_comparators) - { - bool result = comparator->IsApplicable(package); - if (!result) + DisplayCatalogPackageComparison::DisplayCatalogPackageComparator packageComparator{ std::string{ locale }, architecture }; + auto preferredPackageResult = packageComparator.GetPreferredPackage(displayCatalogPackages); + + if (!preferredPackageResult) { - return false; + AICLI_LOG(Core, Error, + << "No applicable display catalog package found for ProductId: " << productId + << " , Locale: " << locale << " , Architecture: " << Utility::ToString(architecture)); + + THROW_HR(APPINSTALLER_CLI_ERROR_NO_APPLICABLE_DISPLAYCATALOG_PACKAGE); } - } - return true; - } + auto preferredPackage = preferredPackageResult.value(); - PackageFormatEnum ConvertToPackageFormatEnum(std::string_view packageFormatStr) - { - std::string packageFormat = Utility::ToLower(packageFormatStr); - if (packageFormat == "appxbundle") - { - return PackageFormatEnum::AppxBundle; - } - else if (packageFormat == "eappxbundle") - { - return PackageFormatEnum::EAppxBundle; - } - else if (packageFormat == "msixbundle") - { - return PackageFormatEnum::MsixBundle; - } - else if (packageFormat == "appx") - { - return PackageFormatEnum::Appx; - } - else if (packageFormat == "msix") - { - return PackageFormatEnum::Msix; - } + AICLI_LOG(Core, Info, + << "DisplayCatalog package selected. WuCategoryId: " << preferredPackage.WuCategoryId + << " , ContentId: " << preferredPackage.ContentId); - AICLI_LOG(Core, Info, << "ConvertToPackageFormatEnum: Unknown package format: " << packageFormatStr); - return PackageFormatEnum::Unknown; + return preferredPackage; + } } - std::vector<MSStoreCatalogPackage> DeserializeMSStoreCatalogPackages(const web::json::value& jsonObject) +#ifndef WINGET_DISABLE_FOR_FUZZING + namespace SfsClientDetails { - try + const std::string SupportedFileTypes[] = { ".msix", ".msixbundle", ".appx", ".appxbundle" }; + + Manifest::PlatformEnum ConvertFromSfsPlatform(std::string_view applicability) { - if (jsonObject.is_null()) + if (Utility::CaseInsensitiveStartsWith(applicability, "universal")) { - AICLI_LOG(Core, Error, << "Missing json object."); - return {}; + return Manifest::PlatformEnum::Universal; + } + else if (Utility::CaseInsensitiveStartsWith(applicability, "desktop")) + { + return Manifest::PlatformEnum::Desktop; + } + else if (Utility::CaseInsensitiveStartsWith(applicability, "iot")) + { + return Manifest::PlatformEnum::IoT; + } + else if (Utility::CaseInsensitiveStartsWith(applicability, "analog")) + { + return Manifest::PlatformEnum::Holographic; + } + else if (Utility::CaseInsensitiveStartsWith(applicability, "ppi")) + { + return Manifest::PlatformEnum::Team; } - std::optional<std::reference_wrapper<const web::json::value>> product = JSON::GetJsonValueFromNode(jsonObject, JSON::GetUtilityString(Product)); - if (!product) + return Manifest::PlatformEnum::Unknown; + } + + Utility::Architecture ConvertFromSfsArchitecture(SFS::Architecture sfsArchitecture) + { + switch (sfsArchitecture) { - AICLI_LOG(Core, Error, << "Missing Product"); - return {}; + case SFS::Architecture::Amd64: + return Utility::Architecture::X64; + case SFS::Architecture::x86: + return Utility::Architecture::X86; + case SFS::Architecture::Arm64: + return Utility::Architecture::Arm64; + case SFS::Architecture::Arm: + return Utility::Architecture::Arm; + case SFS::Architecture::None: + return Utility::Architecture::Neutral; } - const auto& productValue = product.value().get(); + return Utility::Architecture::Unknown; + } - std::optional<std::reference_wrapper<const web::json::value>> defaultSku; + std::vector<std::string> GetSfsPackageFileSupportedPlatforms(const SFS::AppFile& appFile, Manifest::PlatformEnum requiredPlatform) + { + std::vector<std::string> supportedPlatforms; - auto skuEntries = JSON::GetRawJsonArrayFromJsonNode(productValue, JSON::GetUtilityString(DisplaySkuAvailabilities)); - if (!skuEntries) + for (auto const& applicability : appFile.GetApplicabilityDetails().GetPlatformApplicabilityForPackage()) { - AICLI_LOG(Core, Error, << "Missing DisplaySkuAvailabilities"); - return {}; + auto platform = ConvertFromSfsPlatform(applicability); + if (platform != Manifest::PlatformEnum::Unknown && + (platform == requiredPlatform || requiredPlatform == Manifest::PlatformEnum::Unknown)) + { + supportedPlatforms.emplace_back(applicability); + } } - for (const auto& skuEntry : skuEntries.value().get()) + return supportedPlatforms; + } + + std::vector<Utility::Architecture> GetSfsPackageFileSupportedArchitectures(const SFS::AppFile& appFile, Utility::Architecture requiredArchitecture) + { + std::vector<Utility::Architecture> supportedArchitectures; + + for (auto const& sfsArchitecture : appFile.GetApplicabilityDetails().GetArchitectures()) { - std::optional<std::reference_wrapper<const web::json::value>> sku = JSON::GetJsonValueFromNode(skuEntry, JSON::GetUtilityString(Sku)); - if (!sku) + auto convertedArchitecture = ConvertFromSfsArchitecture(sfsArchitecture); + if (convertedArchitecture == Utility::Architecture::Unknown) { - AICLI_LOG(Core, Error, << "Missing Sku"); - return {}; + continue; } - const auto& skuValue = sku.value().get(); - std::optional<std::string> skuId = JSON::GetRawStringValueFromJsonNode(skuValue, JSON::GetUtilityString(SkuId)); - if (JSON::IsValidNonEmptyStringValue(skuId) && Utility::CaseInsensitiveEquals(skuId.value(), DefaultSkuIdValue)) + if (requiredArchitecture == Utility::Architecture::Unknown || // No required architecture + convertedArchitecture == requiredArchitecture) { - defaultSku = sku; - break; + supportedArchitectures.emplace_back(convertedArchitecture); } } - if (!defaultSku) + return supportedArchitectures; + } + + // 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 fileExtension = std::filesystem::path{ appFile.GetFileId() }.extension().u8string(); + + bool fileTypeSupported = false; + for (auto const& supportedFileType : SupportedFileTypes) { - AICLI_LOG(Core, Error, << "Default Sku (" << DefaultSkuIdValue << ") not found"); - return {}; + if (Utility::CaseInsensitiveEquals(supportedFileType, fileExtension)) + { + fileTypeSupported = true; + break; + } } - const auto& defaultSkuValue = defaultSku.value().get(); - std::optional<std::reference_wrapper<const web::json::value>> properties = JSON::GetJsonValueFromNode(defaultSkuValue, JSON::GetUtilityString(Properties)); - if (!properties) + if (!fileTypeSupported) { - AICLI_LOG(Core, Error, << "Missing Properties"); return {}; } - const auto& propertiesValue = properties.value().get(); - auto packages = JSON::GetRawJsonArrayFromJsonNode(propertiesValue, JSON::GetUtilityString(Packages)); - if (!packages) + return appFile.GetFileMoniker() + fileExtension; + } + + void SfsClientLoggingCallback(const SFS::LogData& logData) + { + std::string message = "Message: " + std::string{ logData.message }; + message += " File: " + std::string{ logData.file }; + message += " Line: " + std::to_string(logData.line); + message += " Function: " + std::string{ logData.function }; + + switch (logData.severity) { - AICLI_LOG(Core, Error, << "Missing Packages"); - return {}; + case SFS::LogSeverity::Verbose: + AICLI_LOG(Core, Verbose, << message); + break; + case SFS::LogSeverity::Info: + AICLI_LOG(Core, Info, << message); + break; + case SFS::LogSeverity::Warning: + AICLI_LOG(Core, Warning, << message); + break; + case SFS::LogSeverity::Error: + AICLI_LOG(Core, Error, << message); + break; } + } - std::vector<MSStoreCatalogPackage> displayCatalogPackages; + const std::unique_ptr<SFS::SFSClient>& GetSfsClientInstance() + { + static std::unique_ptr<SFS::SFSClient> s_sfsClient; + static std::once_flag s_sfsClientInitializeOnce; - for (const auto& packageEntry : packages.value().get()) - { - MSStoreCatalogPackage catalogPackage; + std::call_once(s_sfsClientInitializeOnce, + [&]() + { + SFS::ClientConfig config; + config.accountId = "storeapps"; + config.instanceId = "storeapps"; + config.logCallbackFn = SfsClientLoggingCallback; - // Package Id - std::optional<std::string> packageId = JSON::GetRawStringValueFromJsonNode(packageEntry, JSON::GetUtilityString(PackageId)); - if (packageId) + auto result = SFS::SFSClient::Make(config, s_sfsClient); + if (!result) + { + AICLI_LOG(Core, Error, << "Failed to initialize SfsClient. Error code: " << result.GetCode() << " Message: " << result.GetMsg()); + THROW_HR_MSG(APPINSTALLER_CLI_ERROR_SFSCLIENT_API_FAILED, "Failed to initialize SfsClient. ErrorCode: %lu Message: %hs", result.GetCode(), result.GetMsg().c_str()); + } + }); + + return s_sfsClient; + } + + std::vector<MSStoreDownloadFile> PopulateSfsAppFileToMSStoreDownloadFileVector( + const std::vector<SFS::AppFile>& appFiles, + Utility::Architecture requiredArchitecture = Utility::Architecture::Unknown, + Manifest::PlatformEnum requiredPlatform = Manifest::PlatformEnum::Unknown) + { + using PlatformAndArchitectureKey = std::pair<std::string, Utility::Architecture>; + + // Since the server may return multiple versions of the same package, we'll use ths map to record the one with latest version + // for each Platform|Architecture pair. + std::map<PlatformAndArchitectureKey, MSStoreDownloadFile> downloadFilesMap; + + for (auto const& appFile : appFiles) + { + // Filter out unsupported packages + auto supportedPlatforms = GetSfsPackageFileSupportedPlatforms(appFile, requiredPlatform); + if (supportedPlatforms.empty()) { - catalogPackage.PackageId = packageId.value(); + AICLI_LOG(Core, Info, << "Package skipped due to unsupported platforms. FileId:" << appFile.GetFileId()); + continue; } - - // Architectures - auto architectures = JSON::GetRawStringArrayFromJsonNode(packageEntry, JSON::GetUtilityString(Architectures)); - for (const auto& arch : architectures) + auto supportedArchitectures = GetSfsPackageFileSupportedArchitectures(appFile, requiredArchitecture); + if (supportedArchitectures.empty()) { - catalogPackage.Architectures.emplace_back(Utility::ConvertToArchitectureEnum(arch)); + AICLI_LOG(Core, Info, << "Package skipped due to unsupported architecture. FileId:" << appFile.GetFileId()); + continue; } - - // Languages - auto languages = JSON::GetRawStringArrayFromJsonNode(packageEntry, JSON::GetUtilityString(Languages)); - for (const auto& language : languages) + std::string fileName = GetSfsPackageFileName(appFile); + if (fileName.empty()) { - catalogPackage.Languages.emplace_back(language); + AICLI_LOG(Core, Info, << "Package skipped due to unsupported file type. FileId:" << appFile.GetFileId()); + continue; } - // Package Format - std::optional<std::string> packageFormat = JSON::GetRawStringValueFromJsonNode(packageEntry, JSON::GetUtilityString(PackageFormat)); - if (packageFormat) + 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()); + + // Update the platform architecture map with latest package if applicable + for (auto supportedPlatform : supportedPlatforms) { - catalogPackage.PackageFormat = ConvertToPackageFormatEnum(packageFormat.value()); + for (auto supportedArchitecture : supportedArchitectures) + { + PlatformAndArchitectureKey downloadFileKey{ supportedPlatform, supportedArchitecture }; + if (downloadFile.Version > downloadFilesMap[downloadFileKey].Version) + { + downloadFilesMap[downloadFileKey] = downloadFile; + } + } } + } - // Content Id - std::optional<std::string> contentId = JSON::GetRawStringValueFromJsonNode(packageEntry, JSON::GetUtilityString(ContentId)); - if (contentId) + // Generate MSStoreDownloadFile vector and remove duplication. + std::vector<MSStoreDownloadFile> result; + for (auto& downloadFileEntry : downloadFilesMap) + { + if (std::find_if(result.begin(), result.end(), + [&](const MSStoreDownloadFile& downloadFile) + { + return Utility::CaseInsensitiveEquals(downloadFile.FileName, downloadFileEntry.second.FileName); + }) == result.end()) { - catalogPackage.ContentId = contentId.value(); + result.emplace_back(std::move(downloadFileEntry.second)); } + } - // WuCategoryId - std::optional<std::reference_wrapper<const web::json::value>> fulfillmentData = JSON::GetJsonValueFromNode(propertiesValue, JSON::GetUtilityString(FulfillmentData)); - if (fulfillmentData) + return result; + } + + MSStoreDownloadInfo CallSfsClientAndGetMSStoreDownloadInfo(std::string_view wuCategoryId, Utility::Architecture requiredArchitecture, Manifest::PlatformEnum requiredPlatform) + { + AICLI_LOG(Core, Info, << "CallSfsClientAndGetMSStoreDownloadInfo with WuCategoryId: " << wuCategoryId << " Architecture: " << Utility::ToString(requiredArchitecture) << " Platform: " << Manifest::PlatformToString(requiredPlatform)); + + std::vector<SFS::AppContent> appContents; + +#ifndef AICLI_DISABLE_TEST_HOOKS + if (TestHooks::s_SfsClient_AppContents_Override) + { + appContents = (*TestHooks::s_SfsClient_AppContents_Override)(wuCategoryId); + } + else +#endif + { + SFS::RequestParams sfsClientRequest; + sfsClientRequest.productRequests = { {std::string{ wuCategoryId }, {}} }; + + auto requestResult = GetSfsClientInstance()->GetLatestAppDownloadInfo(sfsClientRequest, appContents); + if (!requestResult) { - const auto& fulfillmentDataValue = fulfillmentData.value().get(); - std::optional<std::string> wuCategoryId = JSON::GetRawStringValueFromJsonNode(fulfillmentDataValue, JSON::GetUtilityString(WuCategoryId)); - if (JSON::IsValidNonEmptyStringValue(wuCategoryId)) - { - catalogPackage.WuCategoryId = wuCategoryId.value(); - } + 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()); } + } + + THROW_HR_IF(E_UNEXPECTED, appContents.empty()); - displayCatalogPackages.emplace_back(catalogPackage); + MSStoreDownloadInfo result; + // Currently for app downloads, the result vector is always size 1. + const auto& appContent = appContents.at(0); + + // Populate main packages + result.MainPackages = PopulateSfsAppFileToMSStoreDownloadFileVector(appContent.GetFiles(), requiredArchitecture, requiredPlatform); + + // Populate dependency packages + for (auto const& dependencyEntry : appContent.GetPrerequisites()) + { + // Not passing in required platform for dependencies. Dependencies are mostly Windows.Universal. + auto dependencyPackages = PopulateSfsAppFileToMSStoreDownloadFileVector(dependencyEntry.GetFiles(), requiredArchitecture); + std::move(dependencyPackages.begin(), dependencyPackages.end(), std::inserter(result.DependencyPackages, result.DependencyPackages.end())); } - return displayCatalogPackages; + if (result.MainPackages.empty()) + { + AICLI_LOG(Core, Error, << "No applicable SFS main package."); + THROW_HR(APPINSTALLER_CLI_ERROR_NO_APPLICABLE_SFSCLIENT_PACKAGE); + } + + return result; } - catch (const std::exception& e) + } +#endif + + namespace LicensingDetails + { + // Json response fields + constexpr std::string_view License = "license"sv; + constexpr std::string_view Keys = "keys"sv; + constexpr std::string_view Value = "value"sv; + + // Licensing rest endpoint + constexpr std::string_view LicensingRestEndpoint = "https://licensing.md.mp.microsoft.com/v9.0/licenses/offlineContent"; + constexpr std::string_view ContentId = "contentId"sv; + constexpr std::string_view From = "From"sv; + + // Response: + // { + // "license": { + // "keys": [ // returned as array for future, for now only 1 key + // { + // "value": "base64 encoded string" + // } + // ] + // } + // } + std::vector<BYTE> GetLicensing(std::string_view contentId, const Http::HttpClientHelper::HttpRequestHeaders& authHeaders) { - AICLI_LOG(Core, Error, << "Error encountered while deserializing MSStore request. Reason: " << e.what()); + AICLI_LOG(Core, Error, << "GetLicensing with ContentId: " << contentId); + + AppInstaller::Http::HttpClientHelper httpClientHelper; + +#ifndef AICLI_DISABLE_TEST_HOOKS + if (TestHooks::s_Licensing_HttpPipelineStage_Override) + { + httpClientHelper = AppInstaller::Http::HttpClientHelper{ TestHooks::s_Licensing_HttpPipelineStage_Override }; + } +#endif + + web::json::value requestBody; + requestBody[JSON::GetUtilityString(ContentId)] = web::json::value::string(JSON::GetUtilityString(contentId)); + Http::HttpClientHelper::HttpRequestHeaders requestHeaders; + requestHeaders.insert_or_assign(JSON::GetUtilityString(From), L"winget-cli"); + + std::optional<web::json::value> licensingResponseObject = httpClientHelper.HandlePost( + JSON::GetUtilityString(LicensingRestEndpoint), requestBody, requestHeaders, authHeaders); + + if (!licensingResponseObject || licensingResponseObject->is_null()) + { + AICLI_LOG(Core, Error, << "Empty licensing response"); + THROW_HR(APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED); + } + + std::optional<std::reference_wrapper<const web::json::value>> license = JSON::GetJsonValueFromNode(licensingResponseObject.value(), JSON::GetUtilityString(License)); + if (!license) + { + AICLI_LOG(Core, Error, << "Missing license node"); + THROW_HR(APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED); + } + + auto keys = JSON::GetRawJsonArrayFromJsonNode(license.value().get(), JSON::GetUtilityString(Keys)); + if (!keys || keys->get().size() == 0) + { + AICLI_LOG(Core, Error, << "Missing keys or empty keys"); + THROW_HR(APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED); + } + + std::string base64LicenseContent = JSON::GetRawStringValueFromJsonNode(keys->get().at(0), JSON::GetUtilityString(Value)).value_or(""); + if (base64LicenseContent.empty()) + { + AICLI_LOG(Core, Error, << "Missing license content"); + THROW_HR(APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED); + } + + return JSON::Base64Decode(base64LicenseContent); } - catch (...) + } + + namespace + { + Http::HttpClientHelper::HttpRequestHeaders GetAuthHeaders(std::unique_ptr<Authentication::Authenticator>& authenticator) { - AICLI_LOG(Core, Error, << "Received invalid information."); + if (!authenticator) + { + return {}; + } + + Http::HttpClientHelper::HttpRequestHeaders result; + + auto authResult = authenticator->AuthenticateForToken(); + if (FAILED(authResult.Status)) + { + AICLI_LOG(Repo, Error, << "Authentication failed. Result: " << authResult.Status); + THROW_HR_MSG(authResult.Status, "Failed to authenticate for MicrosoftEntraId"); + } + result.insert_or_assign(web::http::header_names::authorization, JSON::GetUtilityString(Authentication::CreateBearerToken(authResult.Token))); + + return result; } } - std::string GetMSStoreCatalogRestApi(const std::string& productId, const std::string& locale) + MSStoreDownloadContext::MSStoreDownloadContext( + std::string productId, + AppInstaller::Utility::Architecture architecture, + Manifest::PlatformEnum platform, + std::string locale, + AppInstaller::Authentication::AuthenticationArguments authArgs) : + m_productId(std::move(productId)), m_architecture(architecture), m_platform(platform), m_locale(std::move(locale)) { - std::string languageValue = !locale.empty() ? locale : std::string{ Neutral }; - std::string market = AppInstaller::Runtime::GetOSRegion(); +#ifndef AICLI_DISABLE_TEST_HOOKS + if (!TestHooks::s_Licensing_HttpPipelineStage_Override) +#endif + { + Authentication::MicrosoftEntraIdAuthenticationInfo licensingMicrosoftEntraIdAuthInfo; + licensingMicrosoftEntraIdAuthInfo.Resource = "c5e1cb0d-5d24-4b1a-b291-ec684152b2ba"; + Authentication::AuthenticationInfo licensingAuthInfo; + licensingAuthInfo.Type = Authentication::AuthenticationType::MicrosoftEntraId; + licensingAuthInfo.MicrosoftEntraIdInfo = std::move(licensingMicrosoftEntraIdAuthInfo); + + // Not moving authArgs because we'll have auth for display catalog and sfs client in the near future. + m_licensingAuthenticator = std::make_unique<Authentication::Authenticator>(std::move(licensingAuthInfo), authArgs); + } + } - auto restEndpoint = AppInstaller::Utility::Format(std::string{ MSStoreCatalogRestApi }, productId, Details, market, languageValue); - THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_URL, !AppInstaller::Rest::IsValidUri(JSON::GetUtilityString(restEndpoint))); - return restEndpoint; + MSStoreDownloadInfo MSStoreDownloadContext::GetDownloadInfo() + { +#ifndef WINGET_DISABLE_FOR_FUZZING + auto displayCatalogPackage = DisplayCatalogDetails::CallDisplayCatalogAndGetPreferredPackage(m_productId, m_locale, m_architecture); + auto downloadInfo = SfsClientDetails::CallSfsClientAndGetMSStoreDownloadInfo(displayCatalogPackage.WuCategoryId, m_architecture, m_platform); + downloadInfo.ContentId = displayCatalogPackage.ContentId; + return downloadInfo; +#else + return {}; +#endif } + std::vector<BYTE> MSStoreDownloadContext::GetLicense(std::string_view contentId) + { + return LicensingDetails::GetLicensing(contentId, GetAuthHeaders(m_licensingAuthenticator)); + } } diff --git a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp @@ -222,20 +222,48 @@ namespace AppInstaller::Manifest return result; } - PlatformEnum ConvertToPlatformEnum(const std::string& in) + PlatformEnum ConvertToPlatformEnum(std::string_view in) { - PlatformEnum result = PlatformEnum::Unknown; + std::string inStrLower = Utility::ToLower(in); - if (Utility::CaseInsensitiveEquals(in, "windows.desktop")) + if (inStrLower == "windows.desktop") { - result = PlatformEnum::Desktop; + return PlatformEnum::Desktop; } - else if (Utility::CaseInsensitiveEquals(in, "windows.universal")) + else if (inStrLower == "windows.universal") { - result = PlatformEnum::Universal; + return PlatformEnum::Universal; } - return result; + return PlatformEnum::Unknown; + } + + PlatformEnum ConvertToPlatformEnumForMSStoreDownload(std::string_view in) + { + std::string inStrLower = Utility::ToLower(in); + + if (inStrLower == "windows.desktop") + { + return PlatformEnum::Desktop; + } + else if (inStrLower == "windows.universal") + { + return PlatformEnum::Universal; + } + else if (inStrLower == "windows.iot") + { + return PlatformEnum::IoT; + } + else if (inStrLower == "windows.team") + { + return PlatformEnum::Team; + } + else if (inStrLower == "windows.holographic") + { + return PlatformEnum::Holographic; + } + + return PlatformEnum::Unknown; } ElevationRequirementEnum ConvertToElevationRequirementEnum(const std::string& in) @@ -638,6 +666,12 @@ namespace AppInstaller::Manifest return "Windows.Desktop"sv; case PlatformEnum::Universal: return "Windows.Universal"sv; + case PlatformEnum::IoT: + return "Windows.IoT"sv; + case PlatformEnum::Holographic: + return "Windows.Holographic"sv; + case PlatformEnum::Team: + return "Windows.Team"sv; } return "Unknown"sv; diff --git a/src/AppInstallerCommonCore/MsixInfo.cpp b/src/AppInstallerCommonCore/MsixInfo.cpp @@ -465,6 +465,34 @@ namespace AppInstaller::Msix return { result }; } + Utility::UInt64Version GetPackageVersionFromFullName(std::string_view fullName) + { + std::wstring fullNameWide = Utility::ConvertToUTF16(fullName); + + UINT32 length = 0; + LONG returnVal = PackageIdFromFullName(fullNameWide.c_str(), PACKAGE_INFORMATION_BASIC, &length, nullptr); + if (returnVal != ERROR_INSUFFICIENT_BUFFER) + { + LOG_WIN32(returnVal); + return 0; + } + + THROW_HR_IF(E_UNEXPECTED, length == 0); + + std::unique_ptr<BYTE[]> packageIdContent = std::make_unique<BYTE[]>(length); + + returnVal = PackageIdFromFullName(fullNameWide.c_str(), PACKAGE_INFORMATION_BASIC, &length, packageIdContent.get()); + if (returnVal != ERROR_SUCCESS) + { + LOG_WIN32(returnVal); + return 0; + } + + PACKAGE_ID* packageId = (PACKAGE_ID*)packageIdContent.get(); + + return packageId->version.Version; + } + GetCertContextResult GetCertContextFromMsix(const std::filesystem::path& msixPath) { // Retrieve raw signature from msix diff --git a/src/AppInstallerCommonCore/Public/AppInstallerMsixInfo.h b/src/AppInstallerCommonCore/Public/AppInstallerMsixInfo.h @@ -5,6 +5,7 @@ #include "winget/ManagedFile.h" #include "winget/Manifest.h" #include "winget/MsixManifest.h" +#include <AppInstallerVersions.h> #include <AppxPackaging.h> @@ -46,6 +47,9 @@ namespace AppInstaller::Msix // Gets the package location from the given full name. std::optional<std::filesystem::path> GetPackageLocationFromFullName(std::string_view fullName); + // Gets the package version from the given full name. + AppInstaller::Utility::UInt64Version GetPackageVersionFromFullName(std::string_view fullName); + // MsixInfo class handles all appx/msix related query. struct MsixInfo { @@ -121,4 +125,4 @@ namespace AppInstaller::Msix private: Utility::ManagedFile m_file; }; -}- \ No newline at end of file +} diff --git a/src/AppInstallerCommonCore/Public/winget/MSStoreDownload.h b/src/AppInstallerCommonCore/Public/winget/MSStoreDownload.h @@ -1,8 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once -#include <winget/JsonUtil.h> #include <AppInstallerArchitecture.h> +#include <AppInstallerSHA256.h> +#include <AppInstallerVersions.h> +#include "winget/Authentication.h" +#include "winget/ManifestCommon.h" #include <string> #include <optional> @@ -12,76 +15,43 @@ namespace AppInstaller::MSStore { - enum class PackageFormatEnum + // Struct representing 1 MSStore package file download info + struct MSStoreDownloadFile { - Unknown = -1, - AppxBundle, - EAppxBundle, - MsixBundle, - Appx, - Msix, + std::string Url; + AppInstaller::Utility::SHA256::HashBuffer Sha256; + std::string FileName; + AppInstaller::Utility::UInt64Version Version = 0; }; - struct MSStoreCatalogPackage + struct MSStoreDownloadInfo { - std::string PackageId; - - std::vector<AppInstaller::Utility::Architecture> Architectures; - - std::vector<std::string> Languages; - - PackageFormatEnum PackageFormat = PackageFormatEnum::Unknown; - - std::string WuCategoryId; + std::vector<MSStoreDownloadFile> MainPackages; + std::vector<MSStoreDownloadFile> DependencyPackages; std::string ContentId; }; - namespace details - { - struct MSStoreCatalogPackageComparisonField - { - MSStoreCatalogPackageComparisonField(std::string_view name) : m_name(name) {} - - virtual ~MSStoreCatalogPackageComparisonField() = default; - - std::string_view Name() const { return m_name; } - - virtual bool IsApplicable(const MSStoreCatalogPackage& package) = 0; - - virtual bool IsFirstBetter(const MSStoreCatalogPackage& first, const MSStore::MSStoreCatalogPackage& second) = 0; - - private: - std::string_view m_name; - }; - } - - // Class for comparing MSStore packages. - struct DisplayCatalogPackageComparator + struct MSStoreDownloadContext { - DisplayCatalogPackageComparator(const std::vector<std::string>& requiredLanguages, const std::vector<AppInstaller::Utility::Architecture>& requiredArchs); - - // Gets the best installer from the manifest, if at least one is applicable. - std::optional<MSStoreCatalogPackage> GetPreferredPackage(const std::vector<MSStoreCatalogPackage>& package); + MSStoreDownloadContext( + std::string productId, + AppInstaller::Utility::Architecture architecture, + AppInstaller::Manifest::PlatformEnum platform, + std::string locale, + AppInstaller::Authentication::AuthenticationArguments authArgs); - // Determines if an installer is applicable. - bool IsApplicable(const MSStoreCatalogPackage& package); + // Calls display catalog API and sfs-client to get download info. + MSStoreDownloadInfo GetDownloadInfo(); - //// Determines if the first installer is a better choice. - bool IsFirstBetter(const MSStoreCatalogPackage& first, const MSStoreCatalogPackage& second); + // Gets license for the corresponding packages + std::vector<BYTE> GetLicense(std::string_view contentId); private: - void AddComparator(std::unique_ptr<details::MSStoreCatalogPackageComparisonField>&& comparator); - - std::vector<std::unique_ptr<details::MSStoreCatalogPackageComparisonField>> m_comparators; + std::string m_productId; + AppInstaller::Utility::Architecture m_architecture = AppInstaller::Utility::Architecture::Unknown; + AppInstaller::Manifest::PlatformEnum m_platform = AppInstaller::Manifest::PlatformEnum::Unknown; + std::string m_locale; + std::unique_ptr<AppInstaller::Authentication::Authenticator> m_licensingAuthenticator; }; - - // Deserializes the display catalog packages from the provided json object. - std::vector<MSStoreCatalogPackage> DeserializeMSStoreCatalogPackages(const web::json::value& jsonObject); - - // Constructs the MSStore catalog rest api with the provided product id and language. - std::string GetMSStoreCatalogRestApi(const std::string& productId, const std::string& language); - - // Coverts the package format string to the corresponding PackageFormatEnum. - PackageFormatEnum ConvertToPackageFormatEnum(std::string_view packageFormatStr); } diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h @@ -168,6 +168,9 @@ namespace AppInstaller::Manifest Unknown, Universal, Desktop, + IoT, + Team, + Holographic, }; enum class ElevationRequirementEnum @@ -363,7 +366,9 @@ namespace AppInstaller::Manifest InstallModeEnum ConvertToInstallModeEnum(const std::string& in); - PlatformEnum ConvertToPlatformEnum(const std::string& in); + PlatformEnum ConvertToPlatformEnum(std::string_view in); + + PlatformEnum ConvertToPlatformEnumForMSStoreDownload(std::string_view in); ElevationRequirementEnum ConvertToElevationRequirementEnum(const std::string& in); @@ -451,4 +456,4 @@ namespace AppInstaller::Manifest // Get a list of default return codes for known installer types std::map<DWORD, ExpectedReturnCodeEnum> GetDefaultKnownReturnCodes(InstallerTypeEnum installerType); -}- \ No newline at end of file +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchRequestSerializer_1_0.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchRequestSerializer_1_0.cpp @@ -50,7 +50,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json { std::optional<web::json::value> result = SerializeSearchRequest(searchRequest); - THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INTERNAL_ERROR, !result); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTAPI_INTERNAL_ERROR, !result); return result.value(); } diff --git a/src/AppInstallerSharedLib/Errors.cpp b/src/AppInstallerSharedLib/Errors.cpp @@ -146,16 +146,16 @@ namespace AppInstaller WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE, "The configured rest source is not supported"), WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA, "Invalid data returned by rest source"), WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY, "Operation is blocked by Group Policy"), - WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_RESTSOURCE_INTERNAL_ERROR, "Rest source internal error"), + WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_RESTAPI_INTERNAL_ERROR, "Rest API internal error"), WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_URL, "Invalid rest source url"), - WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_RESTSOURCE_UNSUPPORTED_MIME_TYPE, "Unsupported MIME type returned by rest source"), + WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_RESTAPI_UNSUPPORTED_MIME_TYPE, "Unsupported MIME type returned by rest API"), WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_VERSION, "Invalid rest source contract version"), WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE, "The source data is corrupted or tampered"), WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_STREAM_READ_FAILURE, "Error reading from the stream"), WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_PACKAGE_AGREEMENTS_NOT_ACCEPTED, "Package agreements were not agreed to"), WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_PROMPT_INPUT_ERROR, "Error reading input in prompt"), WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_UNSUPPORTED_SOURCE_REQUEST, "The search request is not supported by one or more sources"), - WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_RESTSOURCE_ENDPOINT_NOT_FOUND, "The rest source endpoint is not found."), + WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_RESTAPI_ENDPOINT_NOT_FOUND, "The rest API endpoint is not found."), WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_SOURCE_OPEN_FAILED, "Failed to open the source."), WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_SOURCE_AGREEMENTS_NOT_ACCEPTED, "Source agreements were not agreed to"), WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_CUSTOMHEADER_EXCEEDS_MAXLENGTH, "Header size exceeds the allowable limit of 1024 characters. Please reduce the size and try again."), @@ -214,7 +214,11 @@ namespace AppInstaller WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_REPAIR_NOT_SUPPORTED, "The installer technology in use doesn't support repair."), WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED, "Repair operations involving administrator privileges are not permitted on packages installed within the user scope."), WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_SQLITE_CONNECTION_TERMINATED, "The SQLite connection was terminated to prevent corruption."), - WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_MSSTORE_NO_APPLICABLE_PACKAGE, "No applicable MSStore package found."), + WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_DISPLAYCATALOG_API_FAILED, "Failed to get Microsoft Store package catalog."), + WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_NO_APPLICABLE_DISPLAYCATALOG_PACKAGE, "No applicable Microsoft Store package found from Microsoft Store package catalog."), + WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_SFSCLIENT_API_FAILED, "Failed to get Microsoft Store package download infomation."), + WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_NO_APPLICABLE_SFSCLIENT_PACKAGE, "No applicable Microsoft Store package download infomation found."), + WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED, "Failed to retrieve Microsoft Store package license."), // 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 @@ -76,16 +76,16 @@ #define APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE ((HRESULT)0x8A150038) #define APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA ((HRESULT)0x8A150039) #define APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY ((HRESULT)0x8A15003A) -#define APPINSTALLER_CLI_ERROR_RESTSOURCE_INTERNAL_ERROR ((HRESULT)0x8A15003B) +#define APPINSTALLER_CLI_ERROR_RESTAPI_INTERNAL_ERROR ((HRESULT)0x8A15003B) #define APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_URL ((HRESULT)0x8A15003C) -#define APPINSTALLER_CLI_ERROR_RESTSOURCE_UNSUPPORTED_MIME_TYPE ((HRESULT)0x8A15003D) +#define APPINSTALLER_CLI_ERROR_RESTAPI_UNSUPPORTED_MIME_TYPE ((HRESULT)0x8A15003D) #define APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_VERSION ((HRESULT)0x8A15003E) #define APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE ((HRESULT)0x8A15003F) #define APPINSTALLER_CLI_ERROR_STREAM_READ_FAILURE ((HRESULT)0x8A150040) #define APPINSTALLER_CLI_ERROR_PACKAGE_AGREEMENTS_NOT_ACCEPTED ((HRESULT)0x8A150041) #define APPINSTALLER_CLI_ERROR_PROMPT_INPUT_ERROR ((HRESULT)0x8A150042) #define APPINSTALLER_CLI_ERROR_UNSUPPORTED_SOURCE_REQUEST ((HRESULT)0x8A150043) -#define APPINSTALLER_CLI_ERROR_RESTSOURCE_ENDPOINT_NOT_FOUND ((HRESULT)0x8A150044) +#define APPINSTALLER_CLI_ERROR_RESTAPI_ENDPOINT_NOT_FOUND ((HRESULT)0x8A150044) #define APPINSTALLER_CLI_ERROR_SOURCE_OPEN_FAILED ((HRESULT)0x8A150045) #define APPINSTALLER_CLI_ERROR_SOURCE_AGREEMENTS_NOT_ACCEPTED ((HRESULT)0x8A150046) #define APPINSTALLER_CLI_ERROR_CUSTOMHEADER_EXCEEDS_MAXLENGTH ((HRESULT)0x8A150047) @@ -144,7 +144,11 @@ #define APPINSTALLER_CLI_ERROR_REPAIR_NOT_SUPPORTED ((HRESULT)0x8A15007C) #define APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED ((HRESULT)0x8A15007D) #define APPINSTALLER_CLI_ERROR_SQLITE_CONNECTION_TERMINATED ((HRESULT)0x8A15007E) -#define APPINSTALLER_CLI_ERROR_MSSTORE_NO_APPLICABLE_PACKAGE ((HRESULT)0x8A15007F) +#define APPINSTALLER_CLI_ERROR_DISPLAYCATALOG_API_FAILED ((HRESULT)0x8A15007F) +#define APPINSTALLER_CLI_ERROR_NO_APPLICABLE_DISPLAYCATALOG_PACKAGE ((HRESULT)0x8A150080) +#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) // Install errors. #define APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE ((HRESULT)0x8A150101) diff --git a/src/Microsoft.Management.Deployment/Converters.cpp b/src/Microsoft.Management.Deployment/Converters.cpp @@ -185,8 +185,9 @@ namespace winrt::Microsoft::Management::Deployment::implementation break; case APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE: case APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA: - case APPINSTALLER_CLI_ERROR_RESTSOURCE_INTERNAL_ERROR: - case APPINSTALLER_CLI_ERROR_RESTSOURCE_UNSUPPORTED_MIME_TYPE: + case APPINSTALLER_CLI_ERROR_RESTAPI_ENDPOINT_NOT_FOUND: + case APPINSTALLER_CLI_ERROR_RESTAPI_INTERNAL_ERROR: + case APPINSTALLER_CLI_ERROR_RESTAPI_UNSUPPORTED_MIME_TYPE: case APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_VERSION: case APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE: resultStatus = winrt::Microsoft::Management::Deployment::FindPackagesResultStatus::CatalogError;