winget-cli

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

commit 4139d79bfc3fd23d3e120716453e3f4c4b9a5735
parent 0aaccd646d1f36c2e7284c58688ddcee60fddb4d
Author: yao-msft <50888816+yao-msft@users.noreply.github.com>
Date:   Thu, 25 Apr 2024 23:32:54 -0700

Merge pull request #4408 from yao-msft/EnlistSfsClient

Pull in microsoft sfs-client repo as subtree
Diffstat:
M.github/actions/spelling/allow.txt | 2++
M.github/actions/spelling/excludes.txt | 1+
M.github/actions/spelling/expect.txt | 3++-
M.gitignore | 3+++
Mazure-pipelines.nuget.yml | 8++++++++
Mazure-pipelines.yml | 8++++++++
Mcgmanifest.json | 11++++++++++-
Msrc/AppInstallerCLI.sln | 325++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 44++++++++++++++++++++++++++++++++++++++++++--
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj | 24++++++++++++------------
Asrc/SfsClient/SfsClient.vcxproj | 436+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/SfsClient.vcxproj.filters | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/readme.md | 14++++++++++++++
Asrc/SfsClient/sfs-client/.clang-format | 29+++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/.cmake-format.json | 8++++++++
Asrc/SfsClient/sfs-client/.github/CODEOWNERS | 3+++
Asrc/SfsClient/sfs-client/.github/ISSUE_TEMPLATE/bug_report.md | 24++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/.github/ISSUE_TEMPLATE/feature_request.md | 20++++++++++++++++++++
Asrc/SfsClient/sfs-client/.github/dependabot.yml | 19+++++++++++++++++++
Asrc/SfsClient/sfs-client/.github/pull_request_template.md | 15+++++++++++++++
Asrc/SfsClient/sfs-client/.github/workflows/install-winget/action.yml | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/.github/workflows/main-build-ubuntu.yml | 38++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/.github/workflows/main-build-windows.yml | 45+++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/.github/workflows/pr.yml | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/.gitignore | 45+++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/API.md | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/CMakeLists.txt | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/CODE_OF_CONDUCT.md | 9+++++++++
Asrc/SfsClient/sfs-client/DEVELOPMENT.md | 6++++++
Asrc/SfsClient/sfs-client/LICENSE | 21+++++++++++++++++++++
Asrc/SfsClient/sfs-client/NOTICE.md | 441+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/README.md | 195+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/SECURITY.md | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/SUPPORT.md | 11+++++++++++
Asrc/SfsClient/sfs-client/TEST.md | 12++++++++++++
Asrc/SfsClient/sfs-client/cgmanifest.json | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/CMakeLists.txt | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/include/sfsclient/AppContent.h | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/include/sfsclient/AppFile.h | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/include/sfsclient/ApplicabilityDetails.h | 42++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/include/sfsclient/ClientConfig.h | 34++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/include/sfsclient/Content.h | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/include/sfsclient/ContentId.h | 48++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/include/sfsclient/File.h | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/include/sfsclient/Logging.h | 33+++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/include/sfsclient/RequestParams.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/include/sfsclient/Result.h | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/include/sfsclient/SFSClient.h | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/AppContent.cpp | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/AppFile.cpp | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/ApplicabilityDetails.cpp | 35+++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/Content.cpp | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/ContentId.cpp | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/File.cpp | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/Logging.cpp | 22++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/Result.cpp | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/SFSClient.cpp | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/ContentUtil.cpp | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/ContentUtil.h | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/CorrelationVector.cpp | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/CorrelationVector.h | 40++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/Env.cpp | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/Env.h | 40++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/ErrorHandling.cpp | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/ErrorHandling.h | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/OSInfo.cpp | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/OSInfo.h | 12++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/ReportingHandler.cpp | 28++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/ReportingHandler.h | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/SFSClientImpl.cpp | 415+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/SFSClientImpl.h | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/SFSClientInterface.h | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/SFSException.cpp | 28++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/SFSException.h | 28++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/SFSUrlBuilder.cpp | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/SFSUrlBuilder.h | 47+++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/TestOverride.cpp | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/TestOverride.h | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/UrlBuilder.cpp | 167+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/UrlBuilder.h | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/Util.cpp | 21+++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/Util.h | 12++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/connection/Connection.cpp | 21+++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/connection/Connection.h | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/connection/ConnectionConfig.cpp | 15+++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/connection/ConnectionConfig.h | 27+++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/connection/ConnectionManager.cpp | 16++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/connection/ConnectionManager.h | 28++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/connection/CurlConnection.cpp | 427+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/connection/CurlConnection.h | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/connection/CurlConnectionManager.cpp | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/connection/CurlConnectionManager.h | 24++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/connection/HttpHeader.cpp | 38++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/connection/HttpHeader.h | 21+++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/connection/mock/MockConnection.cpp | 26++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/connection/mock/MockConnection.h | 23+++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/connection/mock/MockConnectionManager.cpp | 21+++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/connection/mock/MockConnectionManager.h | 24++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/entity/ContentType.cpp | 38++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/entity/ContentType.h | 17+++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/entity/FileEntity.cpp | 279+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/entity/FileEntity.h | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/entity/VersionEntity.cpp | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/src/details/entity/VersionEntity.h | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/CMakeLists.txt | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/functional/SFSClientTests.cpp | 440+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/functional/details/CurlConnectionTests.cpp | 554+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/functional/details/SFSClientImplTests.cpp | 358+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/mock/MockWebServer.cpp | 835+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/mock/MockWebServer.h | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/unit/AppContentTests.cpp | 215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/unit/AppFileTests.cpp | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/unit/ApplicabilityDetailsTests.cpp | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/unit/ContentIdTests.cpp | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/unit/ContentTests.cpp | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/unit/FileTests.cpp | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/unit/ResultTests.cpp | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/unit/SFSClientTests.cpp | 300+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/unit/details/CurlConnectionManagerTests.cpp | 48++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/unit/details/CurlConnectionTests.cpp | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/unit/details/EnvTests.cpp | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/unit/details/ErrorHandlingTests.cpp | 255+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/unit/details/ReportingHandlerTests.cpp | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/unit/details/SFSClientImplTests.cpp | 410+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/unit/details/SFSUrlBuilderTests.cpp | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/unit/details/TestOverrideTests.cpp | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/unit/details/UrlBuilderTests.cpp | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/unit/details/UtilTests.cpp | 32++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/unit/details/entity/FileEntityTests.cpp | 639+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/unit/details/entity/VersionEntityTests.cpp | 305++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/util/SFSExceptionMatcher.cpp | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/util/SFSExceptionMatcher.h | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/util/TestHelper.cpp | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/client/tests/util/TestHelper.h | 26++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/cmake/SFSOptions.cmake | 15+++++++++++++++
Asrc/SfsClient/sfs-client/cmake/sfsclient-config.cmake.in | 17+++++++++++++++++
Asrc/SfsClient/sfs-client/pre-commit-wrapper.sh | 18++++++++++++++++++
Asrc/SfsClient/sfs-client/pre-commit.sh | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/samples/CMakeLists.txt | 5+++++
Asrc/SfsClient/sfs-client/samples/README.md | 9+++++++++
Asrc/SfsClient/sfs-client/samples/integration-do-client/CMakeLists.txt | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/samples/integration-do-client/IntegrationDOClient.cpp | 332+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/samples/tool/CMakeLists.txt | 21+++++++++++++++++++++
Asrc/SfsClient/sfs-client/samples/tool/SFSClientTool.cpp | 475+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/scripts/Build.ps1 | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/scripts/Setup.ps1 | 156+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/scripts/Test.ps1 | 28++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/scripts/build.sh | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/scripts/check-format.py | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/scripts/pip.requirements.txt | 3+++
Asrc/SfsClient/sfs-client/scripts/setup.sh | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/scripts/test.sh | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/sfs-client-vcpkg-port/sfs-client/portfile.cmake | 37+++++++++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/sfs-client-vcpkg-port/sfs-client/usage | 4++++
Asrc/SfsClient/sfs-client/sfs-client-vcpkg-port/sfs-client/vcpkg.json | 31+++++++++++++++++++++++++++++++
Asrc/SfsClient/sfs-client/vcpkg-custom-triplets/x64-windows-static-custom.cmake | 6++++++
Asrc/SfsClient/sfs-client/vcpkg.json | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/VcpkgCustomTriplets/arm-release-static.cmake | 6++++++
Asrc/VcpkgCustomTriplets/arm-release.cmake | 6++++++
Asrc/VcpkgCustomTriplets/arm.cmake | 3+++
Asrc/VcpkgCustomTriplets/arm64-release-static.cmake | 6++++++
Asrc/VcpkgCustomTriplets/arm64-release.cmake | 6++++++
Asrc/VcpkgCustomTriplets/arm64.cmake | 3+++
Asrc/VcpkgCustomTriplets/x64-release-static.cmake | 6++++++
Asrc/VcpkgCustomTriplets/x64-release.cmake | 6++++++
Asrc/VcpkgCustomTriplets/x64.cmake | 3+++
Asrc/VcpkgCustomTriplets/x86-release-static.cmake | 6++++++
Asrc/VcpkgCustomTriplets/x86-release.cmake | 6++++++
Asrc/VcpkgCustomTriplets/x86.cmake | 3+++
Msrc/WindowsPackageManager/WindowsPackageManager.vcxproj | 62+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Asrc/vcpkg.json | 37+++++++++++++++++++++++++++++++++++++
171 files changed, 15343 insertions(+), 161 deletions(-)

diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt @@ -241,6 +241,7 @@ publiccontainer PUCHAR pwa QCol +Qspectre rclsid REBOOTPROMPT Redistributable @@ -362,6 +363,7 @@ userguide usersources utext valijson +vcpkg vcvars vcxitems VERYSILENT diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt @@ -84,6 +84,7 @@ ^src/JsonCppLib/ ^src/PowerShell/ExternalModules ^src/PureLib/ +^src/SfsClient/ ^src/UndockedRegFreeWinRT/ ^src/Valijson/ ^src/Xlang/ diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -292,7 +292,8 @@ NESTEDINSTALLER netfx netlify NETSDK -Newtonsoft +Newtonsoft +nlohmann NNS NOAGGREGATION NOCLOSE diff --git a/.gitignore b/.gitignore @@ -191,6 +191,9 @@ PublishScripts/ *.nuget.props *.nuget.targets +# Vcpkg install directory +**/vcpkg_installed/* + # Microsoft Azure Build Output csx/ *.build.csdef diff --git a/azure-pipelines.nuget.yml b/azure-pipelines.nuget.yml @@ -13,6 +13,7 @@ variables: packageName: Microsoft.WindowsPackageManager.Utils buildVer: $[counter(${{ parameters.version }}, 1)] version: ${{ parameters.version }}.$(buildVer) + EnableDetectorVcpkg: true jobs: - job: "Build" @@ -58,6 +59,13 @@ jobs: command: "restore" projects: "**/*.csproj" + - task: CmdLine@2 + displayName: Enable Vcpkg Install + inputs: + script: | + $(VCPKG_INSTALLATION_ROOT)\vcpkg.exe integrate install + workingDirectory: '$(VCPKG_INSTALLATION_ROOT)' + - task: PowerShell@2 displayName: Update Binary Version condition: not(eq(variables['Build.Reason'], 'PullRequest')) diff --git a/azure-pipelines.yml b/azure-pipelines.yml @@ -19,6 +19,7 @@ pool: variables: solution: 'src\AppInstallerCLI.sln' appxPackageDir: '$(Build.ArtifactStagingDirectory)/AppxPackages/' + EnableDetectorVcpkg: true # Do not set the build version for a PR build. @@ -89,6 +90,13 @@ jobs: command: 'restore' projects: '**/*.csproj' + - task: CmdLine@2 + displayName: Enable Vcpkg Install + inputs: + script: | + $(VCPKG_INSTALLATION_ROOT)\vcpkg.exe integrate install + workingDirectory: '$(VCPKG_INSTALLATION_ROOT)' + - task: PowerShell@2 displayName: Update Binary Version condition: not(eq(variables['Build.Reason'], 'PullRequest')) diff --git a/cgmanifest.json b/cgmanifest.json @@ -54,7 +54,16 @@ "commitHash": "fd54913e65338e678440ae66b3b5022ab23b761b" } } - } + }, + { + "component": { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/microsoft/sfs-client.git", + "commitHash": "cf18b357f43aa9bbaba7d8b3b3774b39140aa00f" + } + } + } ], "Version": 1 } diff --git a/src/AppInstallerCLI.sln b/src/AppInstallerCLI.sln @@ -103,6 +103,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution CodeAnalysis.ruleset = CodeAnalysis.ruleset nuget.config = nuget.config stylecop.json = stylecop.json + vcpkg.json = vcpkg.json EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Management.Deployment", "Microsoft.Management.Deployment\Microsoft.Management.Deployment.vcxproj", "{1CC41A9A-AE66-459D-9210-1E572DD7BE69}" @@ -188,6 +189,24 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{5A52D9FC PowerShell\tests\RunTests.ps1 = PowerShell\tests\RunTests.ps1 EndProjectSection EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SfsClient", "SfsClient\SfsClient.vcxproj", "{1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "VcpkgCustomTriplets", "VcpkgCustomTriplets", "{76B26B2C-602A-4AD0-9736-4162D3FCA92A}" + ProjectSection(SolutionItems) = preProject + VcpkgCustomTriplets\arm-release-static.cmake = VcpkgCustomTriplets\arm-release-static.cmake + VcpkgCustomTriplets\arm-release.cmake = VcpkgCustomTriplets\arm-release.cmake + VcpkgCustomTriplets\arm.cmake = VcpkgCustomTriplets\arm.cmake + VcpkgCustomTriplets\arm64-release-static.cmake = VcpkgCustomTriplets\arm64-release-static.cmake + VcpkgCustomTriplets\arm64-release.cmake = VcpkgCustomTriplets\arm64-release.cmake + VcpkgCustomTriplets\arm64.cmake = VcpkgCustomTriplets\arm64.cmake + VcpkgCustomTriplets\x64-release-static.cmake = VcpkgCustomTriplets\x64-release-static.cmake + VcpkgCustomTriplets\x64-release.cmake = VcpkgCustomTriplets\x64-release.cmake + VcpkgCustomTriplets\x64.cmake = VcpkgCustomTriplets\x64.cmake + VcpkgCustomTriplets\x86-release-static.cmake = VcpkgCustomTriplets\x86-release-static.cmake + VcpkgCustomTriplets\x86-release.cmake = VcpkgCustomTriplets\x86-release.cmake + VcpkgCustomTriplets\x86.cmake = VcpkgCustomTriplets\x86.cmake + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -277,7 +296,7 @@ Global {1C6E0108-2860-4B17-9F7E-FA5C6C1F3D3D}.Debug|x64.Build.0 = Debug|x64 {1C6E0108-2860-4B17-9F7E-FA5C6C1F3D3D}.Debug|x86.ActiveCfg = Debug|Win32 {1C6E0108-2860-4B17-9F7E-FA5C6C1F3D3D}.Debug|x86.Build.0 = Debug|Win32 - {1C6E0108-2860-4B17-9F7E-FA5C6C1F3D3D}.Fuzzing|Any CPU.ActiveCfg = ReleaseStatic|x64 + {1C6E0108-2860-4B17-9F7E-FA5C6C1F3D3D}.Fuzzing|Any CPU.ActiveCfg = Release|x64 {1C6E0108-2860-4B17-9F7E-FA5C6C1F3D3D}.Fuzzing|ARM64.ActiveCfg = Release|ARM64 {1C6E0108-2860-4B17-9F7E-FA5C6C1F3D3D}.Fuzzing|x64.ActiveCfg = Release|x64 {1C6E0108-2860-4B17-9F7E-FA5C6C1F3D3D}.Fuzzing|x86.ActiveCfg = Release|Win32 @@ -303,21 +322,21 @@ Global {1C6E0108-2860-4B17-9F7E-FA5C6C1F3D3D}.TestRelease|x86.ActiveCfg = Release|Win32 {1C6E0108-2860-4B17-9F7E-FA5C6C1F3D3D}.TestRelease|x86.Build.0 = Release|Win32 {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.Debug|Any CPU.ActiveCfg = Debug|x64 - {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.Debug|ARM64.ActiveCfg = Debug|Win32 + {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.Debug|ARM64.ActiveCfg = Debug|x64 {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.Debug|x64.ActiveCfg = Debug|x64 {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.Debug|x64.Build.0 = Debug|x64 {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.Debug|x86.ActiveCfg = Debug|Win32 {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.Debug|x86.Build.0 = Debug|Win32 {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.Fuzzing|Any CPU.ActiveCfg = Release|x64 - {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.Fuzzing|ARM64.ActiveCfg = Release|Win32 + {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.Fuzzing|ARM64.ActiveCfg = Release|x64 {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.Fuzzing|x64.ActiveCfg = Release|x64 {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.Fuzzing|x86.ActiveCfg = Release|Win32 {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.PowerShell|Any CPU.ActiveCfg = Release|x64 - {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.PowerShell|ARM64.ActiveCfg = Release|Win32 + {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.PowerShell|ARM64.ActiveCfg = Release|x64 {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.PowerShell|x64.ActiveCfg = Release|x64 {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.PowerShell|x86.ActiveCfg = Release|Win32 {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.Release|Any CPU.ActiveCfg = Release|x64 - {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.Release|ARM64.ActiveCfg = Release|Win32 + {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.Release|ARM64.ActiveCfg = Release|x64 {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.Release|x64.ActiveCfg = Release|x64 {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.Release|x64.Build.0 = Release|x64 {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230}.Release|x86.ActiveCfg = Release|Win32 @@ -333,7 +352,7 @@ Global {5EB88068-5FB9-4E69-89B2-72DBC5E068F9}.Debug|x64.Build.0 = Debug|x64 {5EB88068-5FB9-4E69-89B2-72DBC5E068F9}.Debug|x86.ActiveCfg = Debug|Win32 {5EB88068-5FB9-4E69-89B2-72DBC5E068F9}.Debug|x86.Build.0 = Debug|Win32 - {5EB88068-5FB9-4E69-89B2-72DBC5E068F9}.Fuzzing|Any CPU.ActiveCfg = ReleaseStatic|x64 + {5EB88068-5FB9-4E69-89B2-72DBC5E068F9}.Fuzzing|Any CPU.ActiveCfg = Release|x64 {5EB88068-5FB9-4E69-89B2-72DBC5E068F9}.Fuzzing|ARM64.ActiveCfg = Release|ARM64 {5EB88068-5FB9-4E69-89B2-72DBC5E068F9}.Fuzzing|x64.ActiveCfg = Release|x64 {5EB88068-5FB9-4E69-89B2-72DBC5E068F9}.Fuzzing|x86.ActiveCfg = Release|Win32 @@ -366,12 +385,12 @@ Global {8BB94BB8-374F-4294-BCA1-C7811514A6B7}.Debug|x86.ActiveCfg = Debug|Win32 {8BB94BB8-374F-4294-BCA1-C7811514A6B7}.Debug|x86.Build.0 = Debug|Win32 {8BB94BB8-374F-4294-BCA1-C7811514A6B7}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|x64 - {8BB94BB8-374F-4294-BCA1-C7811514A6B7}.Fuzzing|ARM64.ActiveCfg = Release|ARM64 + {8BB94BB8-374F-4294-BCA1-C7811514A6B7}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 {8BB94BB8-374F-4294-BCA1-C7811514A6B7}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {8BB94BB8-374F-4294-BCA1-C7811514A6B7}.Fuzzing|x64.Build.0 = Fuzzing|x64 {8BB94BB8-374F-4294-BCA1-C7811514A6B7}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {8BB94BB8-374F-4294-BCA1-C7811514A6B7}.Fuzzing|x86.Build.0 = Fuzzing|Win32 - {8BB94BB8-374F-4294-BCA1-C7811514A6B7}.PowerShell|Any CPU.ActiveCfg = Release|x64 + {8BB94BB8-374F-4294-BCA1-C7811514A6B7}.PowerShell|Any CPU.ActiveCfg = ReleaseStatic|x64 {8BB94BB8-374F-4294-BCA1-C7811514A6B7}.PowerShell|ARM64.ActiveCfg = ReleaseStatic|ARM64 {8BB94BB8-374F-4294-BCA1-C7811514A6B7}.PowerShell|ARM64.Build.0 = ReleaseStatic|ARM64 {8BB94BB8-374F-4294-BCA1-C7811514A6B7}.PowerShell|x64.ActiveCfg = ReleaseStatic|x64 @@ -405,7 +424,7 @@ Global {5890D6ED-7C3B-40F3-B436-B54F640D9E65}.Fuzzing|x64.Build.0 = Fuzzing|x64 {5890D6ED-7C3B-40F3-B436-B54F640D9E65}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {5890D6ED-7C3B-40F3-B436-B54F640D9E65}.Fuzzing|x86.Build.0 = Fuzzing|Win32 - {5890D6ED-7C3B-40F3-B436-B54F640D9E65}.PowerShell|Any CPU.ActiveCfg = Release|x64 + {5890D6ED-7C3B-40F3-B436-B54F640D9E65}.PowerShell|Any CPU.ActiveCfg = ReleaseStatic|x64 {5890D6ED-7C3B-40F3-B436-B54F640D9E65}.PowerShell|ARM64.ActiveCfg = ReleaseStatic|ARM64 {5890D6ED-7C3B-40F3-B436-B54F640D9E65}.PowerShell|ARM64.Build.0 = ReleaseStatic|ARM64 {5890D6ED-7C3B-40F3-B436-B54F640D9E65}.PowerShell|x64.ActiveCfg = ReleaseStatic|x64 @@ -479,13 +498,13 @@ Global {6CB84692-5994-407D-B9BD-9216AF77FE83}.TestRelease|x64.ActiveCfg = Release|x64 {6CB84692-5994-407D-B9BD-9216AF77FE83}.TestRelease|x86.ActiveCfg = Release|Win32 {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.Debug|Any CPU.ActiveCfg = Debug|x64 - {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.Debug|ARM64.ActiveCfg = Debug|x86 + {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.Debug|ARM64.ActiveCfg = Debug|x64 {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.Debug|x64.ActiveCfg = Debug|x64 {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.Debug|x64.Build.0 = Debug|x64 {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.Debug|x86.ActiveCfg = Debug|x86 {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.Debug|x86.Build.0 = Debug|x86 {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.Fuzzing|Any CPU.ActiveCfg = Release|x64 - {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.Fuzzing|ARM64.ActiveCfg = Release|x86 + {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.Fuzzing|ARM64.ActiveCfg = Release|x64 {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.Fuzzing|x64.ActiveCfg = Release|x64 {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.Fuzzing|x86.ActiveCfg = Release|x86 {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.PowerShell|Any CPU.ActiveCfg = Release|x64 @@ -493,13 +512,13 @@ Global {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.PowerShell|x64.ActiveCfg = Release|x64 {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.PowerShell|x86.ActiveCfg = Release|x86 {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.Release|Any CPU.ActiveCfg = Release|x64 - {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.Release|ARM64.ActiveCfg = Release|x86 + {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.Release|ARM64.ActiveCfg = Release|x64 {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.Release|x64.ActiveCfg = Release|x64 {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.Release|x64.Build.0 = Release|x64 {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.Release|x86.ActiveCfg = Release|x86 {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.Release|x86.Build.0 = Release|x86 - {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.TestRelease|Any CPU.ActiveCfg = TestOnly|x64 - {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.TestRelease|ARM64.ActiveCfg = TestOnly|x64 + {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.TestRelease|Any CPU.ActiveCfg = Release|x64 + {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.TestRelease|ARM64.ActiveCfg = Release|x64 {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.TestRelease|x64.ActiveCfg = Release|x64 {3C0269FA-E582-4CA7-9E33-3881A005CA0C}.TestRelease|x86.ActiveCfg = Release|x86 {3E2CBA31-CEBA-4D63-BF52-49C0718E19EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -555,21 +574,21 @@ Global {C1624B2F-2BF6-4E28-92FA-1BF85C6B62A8}.TestRelease|x64.ActiveCfg = Release {C1624B2F-2BF6-4E28-92FA-1BF85C6B62A8}.TestRelease|x86.ActiveCfg = Release {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.Debug|Any CPU.ActiveCfg = Debug|x64 - {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.Debug|ARM64.ActiveCfg = Debug|x86 + {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.Debug|ARM64.ActiveCfg = Debug|x64 {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.Debug|x64.ActiveCfg = Debug|x64 {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.Debug|x64.Build.0 = Debug|x64 {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.Debug|x86.ActiveCfg = Debug|x86 {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.Debug|x86.Build.0 = Debug|x86 {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.Fuzzing|Any CPU.ActiveCfg = Release|x64 - {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.Fuzzing|ARM64.ActiveCfg = Release|x86 + {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.Fuzzing|ARM64.ActiveCfg = Release|x64 {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.Fuzzing|x64.ActiveCfg = Release|x64 {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.Fuzzing|x86.ActiveCfg = Release|x86 {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.PowerShell|Any CPU.ActiveCfg = Release|x64 - {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.PowerShell|ARM64.ActiveCfg = Release|x86 + {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.PowerShell|ARM64.ActiveCfg = Release|x64 {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.PowerShell|x64.ActiveCfg = Release|x64 {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.PowerShell|x86.ActiveCfg = Release|x86 {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.Release|Any CPU.ActiveCfg = Release|x64 - {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.Release|ARM64.ActiveCfg = Release|x86 + {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.Release|ARM64.ActiveCfg = Release|x64 {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.Release|x64.ActiveCfg = Release|x64 {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.Release|x64.Build.0 = Release|x64 {3B8466CF-4FDD-4329-9C80-91321C4AAC99}.Release|x86.ActiveCfg = Release|x86 @@ -591,7 +610,7 @@ Global {82B39FDA-E86B-4713-A873-9D56DE00247A}.Fuzzing|x64.Build.0 = Fuzzing|x64 {82B39FDA-E86B-4713-A873-9D56DE00247A}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {82B39FDA-E86B-4713-A873-9D56DE00247A}.Fuzzing|x86.Build.0 = Fuzzing|Win32 - {82B39FDA-E86B-4713-A873-9D56DE00247A}.PowerShell|Any CPU.ActiveCfg = Release|x64 + {82B39FDA-E86B-4713-A873-9D56DE00247A}.PowerShell|Any CPU.ActiveCfg = ReleaseStatic|x64 {82B39FDA-E86B-4713-A873-9D56DE00247A}.PowerShell|ARM64.ActiveCfg = ReleaseStatic|ARM64 {82B39FDA-E86B-4713-A873-9D56DE00247A}.PowerShell|ARM64.Build.0 = ReleaseStatic|ARM64 {82B39FDA-E86B-4713-A873-9D56DE00247A}.PowerShell|x64.ActiveCfg = ReleaseStatic|x64 @@ -613,43 +632,43 @@ Global {82B39FDA-E86B-4713-A873-9D56DE00247A}.TestRelease|x86.ActiveCfg = Release|Win32 {82B39FDA-E86B-4713-A873-9D56DE00247A}.TestRelease|x86.Build.0 = Release|Win32 {1622DA16-914F-4F57-A259-D5169003CC8C}.Debug|Any CPU.ActiveCfg = Debug|x64 - {1622DA16-914F-4F57-A259-D5169003CC8C}.Debug|ARM64.ActiveCfg = Fuzzing|x64 + {1622DA16-914F-4F57-A259-D5169003CC8C}.Debug|ARM64.ActiveCfg = Debug|x64 {1622DA16-914F-4F57-A259-D5169003CC8C}.Debug|x64.ActiveCfg = Debug|x64 - {1622DA16-914F-4F57-A259-D5169003CC8C}.Debug|x86.ActiveCfg = Fuzzing|x64 + {1622DA16-914F-4F57-A259-D5169003CC8C}.Debug|x86.ActiveCfg = Fuzzing|Win32 {1622DA16-914F-4F57-A259-D5169003CC8C}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|x64 {1622DA16-914F-4F57-A259-D5169003CC8C}.Fuzzing|ARM64.ActiveCfg = Fuzzing|x64 {1622DA16-914F-4F57-A259-D5169003CC8C}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {1622DA16-914F-4F57-A259-D5169003CC8C}.Fuzzing|x64.Build.0 = Fuzzing|x64 {1622DA16-914F-4F57-A259-D5169003CC8C}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {1622DA16-914F-4F57-A259-D5169003CC8C}.Fuzzing|x86.Build.0 = Fuzzing|Win32 - {1622DA16-914F-4F57-A259-D5169003CC8C}.PowerShell|Any CPU.ActiveCfg = Debug|x64 + {1622DA16-914F-4F57-A259-D5169003CC8C}.PowerShell|Any CPU.ActiveCfg = Fuzzing|x64 {1622DA16-914F-4F57-A259-D5169003CC8C}.PowerShell|ARM64.ActiveCfg = Fuzzing|x64 {1622DA16-914F-4F57-A259-D5169003CC8C}.PowerShell|x64.ActiveCfg = Fuzzing|x64 - {1622DA16-914F-4F57-A259-D5169003CC8C}.PowerShell|x86.ActiveCfg = Fuzzing|x64 + {1622DA16-914F-4F57-A259-D5169003CC8C}.PowerShell|x86.ActiveCfg = Fuzzing|Win32 {1622DA16-914F-4F57-A259-D5169003CC8C}.Release|Any CPU.ActiveCfg = Debug|x64 {1622DA16-914F-4F57-A259-D5169003CC8C}.Release|ARM64.ActiveCfg = Fuzzing|x64 {1622DA16-914F-4F57-A259-D5169003CC8C}.Release|x64.ActiveCfg = Fuzzing|x64 - {1622DA16-914F-4F57-A259-D5169003CC8C}.Release|x86.ActiveCfg = Fuzzing|x64 - {1622DA16-914F-4F57-A259-D5169003CC8C}.TestRelease|Any CPU.ActiveCfg = Debug|x64 - {1622DA16-914F-4F57-A259-D5169003CC8C}.TestRelease|ARM64.ActiveCfg = Debug|x64 - {1622DA16-914F-4F57-A259-D5169003CC8C}.TestRelease|x64.ActiveCfg = Debug|x64 - {1622DA16-914F-4F57-A259-D5169003CC8C}.TestRelease|x86.ActiveCfg = Debug|x64 + {1622DA16-914F-4F57-A259-D5169003CC8C}.Release|x86.ActiveCfg = Fuzzing|Win32 + {1622DA16-914F-4F57-A259-D5169003CC8C}.TestRelease|Any CPU.ActiveCfg = Fuzzing|x64 + {1622DA16-914F-4F57-A259-D5169003CC8C}.TestRelease|ARM64.ActiveCfg = Fuzzing|x64 + {1622DA16-914F-4F57-A259-D5169003CC8C}.TestRelease|x64.ActiveCfg = Fuzzing|x64 + {1622DA16-914F-4F57-A259-D5169003CC8C}.TestRelease|x86.ActiveCfg = Fuzzing|Win32 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Debug|Any CPU.ActiveCfg = Debug|x64 - {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Debug|ARM64.ActiveCfg = Debug|x86 + {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Debug|ARM64.ActiveCfg = Debug|x64 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Debug|x64.ActiveCfg = Debug|x64 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Debug|x64.Build.0 = Debug|x64 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Debug|x86.ActiveCfg = Debug|x86 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Debug|x86.Build.0 = Debug|x86 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Fuzzing|Any CPU.ActiveCfg = Release|x64 - {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Fuzzing|ARM64.ActiveCfg = Release|x86 + {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Fuzzing|ARM64.ActiveCfg = Release|x64 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Fuzzing|x64.ActiveCfg = Release|x64 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Fuzzing|x86.ActiveCfg = Release|x86 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.PowerShell|Any CPU.ActiveCfg = Release|x64 - {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.PowerShell|ARM64.ActiveCfg = Release|x86 + {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.PowerShell|ARM64.ActiveCfg = Release|x64 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.PowerShell|x64.ActiveCfg = Release|x64 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.PowerShell|x86.ActiveCfg = Release|x86 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Release|Any CPU.ActiveCfg = Release|x64 - {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Release|ARM64.ActiveCfg = Release|x86 + {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Release|ARM64.ActiveCfg = Release|x64 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Release|x64.ActiveCfg = Release|x64 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Release|x64.Build.0 = Release|x64 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Release|x86.ActiveCfg = Release|x86 @@ -698,10 +717,10 @@ Global {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.Debug|x86.ActiveCfg = Debug|Win32 {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.Debug|x86.Build.0 = Debug|Win32 {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.Fuzzing|Any CPU.ActiveCfg = Release|x64 - {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.Fuzzing|ARM64.ActiveCfg = Debug|ARM64 - {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.Fuzzing|x64.ActiveCfg = Debug|x64 - {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.Fuzzing|x86.ActiveCfg = Debug|Win32 - {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.PowerShell|Any CPU.ActiveCfg = Release|x64 + {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.Fuzzing|ARM64.ActiveCfg = Release|ARM64 + {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.Fuzzing|x64.ActiveCfg = Release|x64 + {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.Fuzzing|x86.ActiveCfg = Release|Win32 + {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.PowerShell|Any CPU.ActiveCfg = ReleaseStatic|x64 {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.PowerShell|ARM64.ActiveCfg = ReleaseStatic|ARM64 {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.PowerShell|ARM64.Build.0 = ReleaseStatic|ARM64 {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.PowerShell|x64.ActiveCfg = ReleaseStatic|x64 @@ -730,9 +749,9 @@ Global {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.Debug|x86.ActiveCfg = Debug|Win32 {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.Debug|x86.Build.0 = Debug|Win32 {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.Fuzzing|Any CPU.ActiveCfg = Release|x64 - {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.Fuzzing|ARM64.ActiveCfg = Debug|ARM64 - {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.Fuzzing|x64.ActiveCfg = Debug|x64 - {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.Fuzzing|x86.ActiveCfg = Debug|Win32 + {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.Fuzzing|ARM64.ActiveCfg = Release|ARM64 + {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.Fuzzing|x64.ActiveCfg = Release|x64 + {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.Fuzzing|x86.ActiveCfg = Release|Win32 {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.PowerShell|Any CPU.ActiveCfg = Release|x64 {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.PowerShell|ARM64.ActiveCfg = Release|ARM64 {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.PowerShell|ARM64.Build.0 = Release|ARM64 @@ -759,10 +778,9 @@ Global {846FB88B-BF1B-4F33-9883-E589CEC99739}.Debug|x86.ActiveCfg = Debug|Any CPU {846FB88B-BF1B-4F33-9883-E589CEC99739}.Debug|x86.Build.0 = Debug|Any CPU {846FB88B-BF1B-4F33-9883-E589CEC99739}.Fuzzing|Any CPU.ActiveCfg = Release|Any CPU - {846FB88B-BF1B-4F33-9883-E589CEC99739}.Fuzzing|ARM64.ActiveCfg = Debug|Any CPU - {846FB88B-BF1B-4F33-9883-E589CEC99739}.Fuzzing|ARM64.Build.0 = Debug|Any CPU - {846FB88B-BF1B-4F33-9883-E589CEC99739}.Fuzzing|x64.ActiveCfg = Debug|Any CPU - {846FB88B-BF1B-4F33-9883-E589CEC99739}.Fuzzing|x86.ActiveCfg = Debug|Any CPU + {846FB88B-BF1B-4F33-9883-E589CEC99739}.Fuzzing|ARM64.ActiveCfg = Release|Any CPU + {846FB88B-BF1B-4F33-9883-E589CEC99739}.Fuzzing|x64.ActiveCfg = Release|Any CPU + {846FB88B-BF1B-4F33-9883-E589CEC99739}.Fuzzing|x86.ActiveCfg = Release|Any CPU {846FB88B-BF1B-4F33-9883-E589CEC99739}.PowerShell|Any CPU.ActiveCfg = Release|Any CPU {846FB88B-BF1B-4F33-9883-E589CEC99739}.PowerShell|ARM64.ActiveCfg = Release|Any CPU {846FB88B-BF1B-4F33-9883-E589CEC99739}.PowerShell|x64.ActiveCfg = Release|Any CPU @@ -785,13 +803,13 @@ Global {68808357-902B-406C-8C19-E8E26A69DE8A}.Debug|x86.ActiveCfg = Debug|x86 {68808357-902B-406C-8C19-E8E26A69DE8A}.Debug|x86.Build.0 = Debug|x86 {68808357-902B-406C-8C19-E8E26A69DE8A}.Fuzzing|Any CPU.ActiveCfg = Release|Any CPU - {68808357-902B-406C-8C19-E8E26A69DE8A}.Fuzzing|ARM64.ActiveCfg = Debug|Any CPU - {68808357-902B-406C-8C19-E8E26A69DE8A}.Fuzzing|x64.ActiveCfg = Debug|Any CPU - {68808357-902B-406C-8C19-E8E26A69DE8A}.Fuzzing|x86.ActiveCfg = Debug|Any CPU + {68808357-902B-406C-8C19-E8E26A69DE8A}.Fuzzing|ARM64.ActiveCfg = Release|Any CPU + {68808357-902B-406C-8C19-E8E26A69DE8A}.Fuzzing|x64.ActiveCfg = Release|Any CPU + {68808357-902B-406C-8C19-E8E26A69DE8A}.Fuzzing|x86.ActiveCfg = Release|x86 {68808357-902B-406C-8C19-E8E26A69DE8A}.PowerShell|Any CPU.ActiveCfg = Release|Any CPU {68808357-902B-406C-8C19-E8E26A69DE8A}.PowerShell|ARM64.ActiveCfg = Release|Any CPU {68808357-902B-406C-8C19-E8E26A69DE8A}.PowerShell|x64.ActiveCfg = Release|Any CPU - {68808357-902B-406C-8C19-E8E26A69DE8A}.PowerShell|x86.ActiveCfg = Release|Any CPU + {68808357-902B-406C-8C19-E8E26A69DE8A}.PowerShell|x86.ActiveCfg = Release|x86 {68808357-902B-406C-8C19-E8E26A69DE8A}.Release|Any CPU.ActiveCfg = Release|Any CPU {68808357-902B-406C-8C19-E8E26A69DE8A}.Release|ARM64.ActiveCfg = Release|ARM64 {68808357-902B-406C-8C19-E8E26A69DE8A}.Release|x64.ActiveCfg = Release|x64 @@ -810,11 +828,10 @@ Global {2046B5AF-666D-4CE8-8D3E-C32C57908A56}.Debug|x86.ActiveCfg = Debug|Win32 {2046B5AF-666D-4CE8-8D3E-C32C57908A56}.Debug|x86.Build.0 = Debug|Win32 {2046B5AF-666D-4CE8-8D3E-C32C57908A56}.Fuzzing|Any CPU.ActiveCfg = Release|x64 - {2046B5AF-666D-4CE8-8D3E-C32C57908A56}.Fuzzing|ARM64.ActiveCfg = Debug|ARM64 - {2046B5AF-666D-4CE8-8D3E-C32C57908A56}.Fuzzing|ARM64.Build.0 = Debug|ARM64 - {2046B5AF-666D-4CE8-8D3E-C32C57908A56}.Fuzzing|x64.ActiveCfg = Debug|x64 - {2046B5AF-666D-4CE8-8D3E-C32C57908A56}.Fuzzing|x86.ActiveCfg = Debug|Win32 - {2046B5AF-666D-4CE8-8D3E-C32C57908A56}.PowerShell|Any CPU.ActiveCfg = Release|x64 + {2046B5AF-666D-4CE8-8D3E-C32C57908A56}.Fuzzing|ARM64.ActiveCfg = Release|ARM64 + {2046B5AF-666D-4CE8-8D3E-C32C57908A56}.Fuzzing|x64.ActiveCfg = Release|x64 + {2046B5AF-666D-4CE8-8D3E-C32C57908A56}.Fuzzing|x86.ActiveCfg = Release|Win32 + {2046B5AF-666D-4CE8-8D3E-C32C57908A56}.PowerShell|Any CPU.ActiveCfg = ReleaseStatic|x64 {2046B5AF-666D-4CE8-8D3E-C32C57908A56}.PowerShell|ARM64.ActiveCfg = ReleaseStatic|ARM64 {2046B5AF-666D-4CE8-8D3E-C32C57908A56}.PowerShell|ARM64.Build.0 = ReleaseStatic|ARM64 {2046B5AF-666D-4CE8-8D3E-C32C57908A56}.PowerShell|x64.ActiveCfg = ReleaseStatic|x64 @@ -836,17 +853,16 @@ Global {2046B5AF-666D-4CE8-8D3E-C32C57908A56}.TestRelease|x86.ActiveCfg = Release|Win32 {2046B5AF-666D-4CE8-8D3E-C32C57908A56}.TestRelease|x86.Build.0 = Release|Win32 {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.Debug|Any CPU.ActiveCfg = Debug|x64 - {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.Debug|ARM64.ActiveCfg = Debug|Win32 + {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.Debug|ARM64.ActiveCfg = Debug|ARM64 {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.Debug|x64.ActiveCfg = Debug|x64 {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.Debug|x64.Build.0 = Debug|x64 {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.Debug|x86.ActiveCfg = Debug|Win32 {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.Debug|x86.Build.0 = Debug|Win32 {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.Fuzzing|Any CPU.ActiveCfg = Release|x64 - {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.Fuzzing|ARM64.ActiveCfg = Debug|Win32 - {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.Fuzzing|ARM64.Build.0 = Debug|Win32 - {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.Fuzzing|x64.ActiveCfg = Debug|x64 - {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.Fuzzing|x86.ActiveCfg = Debug|Win32 - {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.PowerShell|Any CPU.ActiveCfg = Release|x64 + {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.Fuzzing|ARM64.ActiveCfg = Release|ARM64 + {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.Fuzzing|x64.ActiveCfg = Release|x64 + {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.Fuzzing|x86.ActiveCfg = Release|Win32 + {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.PowerShell|Any CPU.ActiveCfg = ReleaseStatic|x64 {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.PowerShell|ARM64.ActiveCfg = ReleaseStatic|ARM64 {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.PowerShell|ARM64.Build.0 = ReleaseStatic|ARM64 {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.PowerShell|x64.ActiveCfg = ReleaseStatic|x64 @@ -854,13 +870,13 @@ Global {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.PowerShell|x86.ActiveCfg = ReleaseStatic|Win32 {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.PowerShell|x86.Build.0 = ReleaseStatic|Win32 {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.Release|Any CPU.ActiveCfg = Release|x64 - {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.Release|ARM64.ActiveCfg = Release|Win32 + {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.Release|ARM64.ActiveCfg = Release|ARM64 {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.Release|x64.ActiveCfg = Release|x64 {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.Release|x64.Build.0 = Release|x64 {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.Release|x86.ActiveCfg = Release|Win32 {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.Release|x86.Build.0 = Release|Win32 {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.TestRelease|Any CPU.ActiveCfg = Release|x64 - {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.TestRelease|ARM64.ActiveCfg = Debug|Win32 + {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.TestRelease|ARM64.ActiveCfg = Release|x64 {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.TestRelease|x64.ActiveCfg = Release|x64 {9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}.TestRelease|x86.ActiveCfg = Release|Win32 {463C0EF3-DF38-4C3D-8E7E-D4901E0CDC6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -871,11 +887,10 @@ Global {463C0EF3-DF38-4C3D-8E7E-D4901E0CDC6C}.Debug|x86.ActiveCfg = Debug|Any CPU {463C0EF3-DF38-4C3D-8E7E-D4901E0CDC6C}.Debug|x86.Build.0 = Debug|Any CPU {463C0EF3-DF38-4C3D-8E7E-D4901E0CDC6C}.Fuzzing|Any CPU.ActiveCfg = Release|Any CPU - {463C0EF3-DF38-4C3D-8E7E-D4901E0CDC6C}.Fuzzing|ARM64.ActiveCfg = Debug|Any CPU - {463C0EF3-DF38-4C3D-8E7E-D4901E0CDC6C}.Fuzzing|ARM64.Build.0 = Debug|Any CPU - {463C0EF3-DF38-4C3D-8E7E-D4901E0CDC6C}.Fuzzing|x64.ActiveCfg = Debug|Any CPU - {463C0EF3-DF38-4C3D-8E7E-D4901E0CDC6C}.Fuzzing|x86.ActiveCfg = Debug|Any CPU - {463C0EF3-DF38-4C3D-8E7E-D4901E0CDC6C}.PowerShell|Any CPU.ActiveCfg = Release|Any CPU + {463C0EF3-DF38-4C3D-8E7E-D4901E0CDC6C}.Fuzzing|ARM64.ActiveCfg = Release|Any CPU + {463C0EF3-DF38-4C3D-8E7E-D4901E0CDC6C}.Fuzzing|x64.ActiveCfg = Release|Any CPU + {463C0EF3-DF38-4C3D-8E7E-D4901E0CDC6C}.Fuzzing|x86.ActiveCfg = Release|Any CPU + {463C0EF3-DF38-4C3D-8E7E-D4901E0CDC6C}.PowerShell|Any CPU.ActiveCfg = ReleaseStatic|Any CPU {463C0EF3-DF38-4C3D-8E7E-D4901E0CDC6C}.PowerShell|ARM64.ActiveCfg = Release|Any CPU {463C0EF3-DF38-4C3D-8E7E-D4901E0CDC6C}.PowerShell|x64.ActiveCfg = ReleaseStatic|Any CPU {463C0EF3-DF38-4C3D-8E7E-D4901E0CDC6C}.PowerShell|x64.Build.0 = ReleaseStatic|Any CPU @@ -893,30 +908,30 @@ Global {463C0EF3-DF38-4C3D-8E7E-D4901E0CDC6C}.TestRelease|x64.ActiveCfg = Release|Any CPU {463C0EF3-DF38-4C3D-8E7E-D4901E0CDC6C}.TestRelease|x86.ActiveCfg = Release|Any CPU {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.Debug|Any CPU.ActiveCfg = Debug|x64 - {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.Debug|ARM64.ActiveCfg = Debug|x86 + {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.Debug|ARM64.ActiveCfg = Debug|arm64 {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.Debug|x64.ActiveCfg = Debug|x64 {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.Debug|x64.Build.0 = Debug|x64 {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.Debug|x86.ActiveCfg = Debug|x86 {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.Debug|x86.Build.0 = Debug|x86 - {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.Fuzzing|Any CPU.ActiveCfg = TestOnly|x64 - {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.Fuzzing|ARM64.ActiveCfg = Release|x86 + {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.Fuzzing|Any CPU.ActiveCfg = Release|x64 + {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.Fuzzing|ARM64.ActiveCfg = Release|arm64 {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.Fuzzing|x64.ActiveCfg = Release|x64 {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.Fuzzing|x86.ActiveCfg = Release|x86 - {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.PowerShell|Any CPU.ActiveCfg = TestOnly|x64 - {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.PowerShell|ARM64.ActiveCfg = Release|x86 - {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.PowerShell|ARM64.Build.0 = Release|x86 + {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.PowerShell|Any CPU.ActiveCfg = ReleaseStatic|x64 + {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.PowerShell|ARM64.ActiveCfg = ReleaseStatic|arm64 + {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.PowerShell|ARM64.Build.0 = ReleaseStatic|arm64 {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.PowerShell|x64.ActiveCfg = ReleaseStatic|x64 {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.PowerShell|x64.Build.0 = ReleaseStatic|x64 {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.PowerShell|x86.ActiveCfg = ReleaseStatic|x86 {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.PowerShell|x86.Build.0 = ReleaseStatic|x86 {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.Release|Any CPU.ActiveCfg = Release|x64 - {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.Release|ARM64.ActiveCfg = Release|x86 + {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.Release|ARM64.ActiveCfg = Release|arm64 {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.Release|x64.ActiveCfg = Release|x64 {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.Release|x64.Build.0 = Release|x64 {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.Release|x86.ActiveCfg = Release|x86 {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.Release|x86.Build.0 = Release|x86 - {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.TestRelease|Any CPU.ActiveCfg = TestOnly|x64 - {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.TestRelease|ARM64.ActiveCfg = TestOnly|x64 + {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.TestRelease|Any CPU.ActiveCfg = Release|x64 + {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.TestRelease|ARM64.ActiveCfg = Release|x64 {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.TestRelease|x64.ActiveCfg = Release|x64 {0B104762-5CD8-47EE-A904-71C1C3F84DCD}.TestRelease|x86.ActiveCfg = Release|x86 {31ED69A8-5310-45A9-953F-56C351D2C3E1}.Debug|Any CPU.ActiveCfg = Debug|x64 @@ -927,10 +942,10 @@ Global {31ED69A8-5310-45A9-953F-56C351D2C3E1}.Debug|x86.ActiveCfg = Debug|Win32 {31ED69A8-5310-45A9-953F-56C351D2C3E1}.Debug|x86.Build.0 = Debug|Win32 {31ED69A8-5310-45A9-953F-56C351D2C3E1}.Fuzzing|Any CPU.ActiveCfg = Release|x64 - {31ED69A8-5310-45A9-953F-56C351D2C3E1}.Fuzzing|ARM64.ActiveCfg = Debug|ARM64 - {31ED69A8-5310-45A9-953F-56C351D2C3E1}.Fuzzing|x64.ActiveCfg = Debug|x64 - {31ED69A8-5310-45A9-953F-56C351D2C3E1}.Fuzzing|x86.ActiveCfg = Debug|Win32 - {31ED69A8-5310-45A9-953F-56C351D2C3E1}.PowerShell|Any CPU.ActiveCfg = Release|x64 + {31ED69A8-5310-45A9-953F-56C351D2C3E1}.Fuzzing|ARM64.ActiveCfg = Release|ARM64 + {31ED69A8-5310-45A9-953F-56C351D2C3E1}.Fuzzing|x64.ActiveCfg = Release|x64 + {31ED69A8-5310-45A9-953F-56C351D2C3E1}.Fuzzing|x86.ActiveCfg = Release|Win32 + {31ED69A8-5310-45A9-953F-56C351D2C3E1}.PowerShell|Any CPU.ActiveCfg = ReleaseStatic|x64 {31ED69A8-5310-45A9-953F-56C351D2C3E1}.PowerShell|ARM64.ActiveCfg = ReleaseStatic|ARM64 {31ED69A8-5310-45A9-953F-56C351D2C3E1}.PowerShell|ARM64.Build.0 = ReleaseStatic|ARM64 {31ED69A8-5310-45A9-953F-56C351D2C3E1}.PowerShell|x64.ActiveCfg = ReleaseStatic|x64 @@ -946,6 +961,7 @@ Global {31ED69A8-5310-45A9-953F-56C351D2C3E1}.Release|x86.Build.0 = Release|Win32 {31ED69A8-5310-45A9-953F-56C351D2C3E1}.TestRelease|Any CPU.ActiveCfg = Release|x64 {31ED69A8-5310-45A9-953F-56C351D2C3E1}.TestRelease|ARM64.ActiveCfg = Release|ARM64 + {31ED69A8-5310-45A9-953F-56C351D2C3E1}.TestRelease|ARM64.Build.0 = Release|ARM64 {31ED69A8-5310-45A9-953F-56C351D2C3E1}.TestRelease|x64.ActiveCfg = Release|x64 {31ED69A8-5310-45A9-953F-56C351D2C3E1}.TestRelease|x64.Build.0 = Release|x64 {31ED69A8-5310-45A9-953F-56C351D2C3E1}.TestRelease|x86.ActiveCfg = Release|Win32 @@ -958,10 +974,10 @@ Global {787EC629-C0FB-4BA9-9746-4A82CD06B73E}.Debug|x86.ActiveCfg = Debug|Win32 {787EC629-C0FB-4BA9-9746-4A82CD06B73E}.Debug|x86.Build.0 = Debug|Win32 {787EC629-C0FB-4BA9-9746-4A82CD06B73E}.Fuzzing|Any CPU.ActiveCfg = Release|x64 - {787EC629-C0FB-4BA9-9746-4A82CD06B73E}.Fuzzing|ARM64.ActiveCfg = Debug|ARM64 - {787EC629-C0FB-4BA9-9746-4A82CD06B73E}.Fuzzing|x64.ActiveCfg = Debug|x64 - {787EC629-C0FB-4BA9-9746-4A82CD06B73E}.Fuzzing|x86.ActiveCfg = Debug|Win32 - {787EC629-C0FB-4BA9-9746-4A82CD06B73E}.PowerShell|Any CPU.ActiveCfg = Release|x64 + {787EC629-C0FB-4BA9-9746-4A82CD06B73E}.Fuzzing|ARM64.ActiveCfg = Release|ARM64 + {787EC629-C0FB-4BA9-9746-4A82CD06B73E}.Fuzzing|x64.ActiveCfg = Release|x64 + {787EC629-C0FB-4BA9-9746-4A82CD06B73E}.Fuzzing|x86.ActiveCfg = Release|Win32 + {787EC629-C0FB-4BA9-9746-4A82CD06B73E}.PowerShell|Any CPU.ActiveCfg = ReleaseStatic|x64 {787EC629-C0FB-4BA9-9746-4A82CD06B73E}.PowerShell|ARM64.ActiveCfg = ReleaseStatic|ARM64 {787EC629-C0FB-4BA9-9746-4A82CD06B73E}.PowerShell|ARM64.Build.0 = ReleaseStatic|ARM64 {787EC629-C0FB-4BA9-9746-4A82CD06B73E}.PowerShell|x64.ActiveCfg = ReleaseStatic|x64 @@ -977,6 +993,7 @@ Global {787EC629-C0FB-4BA9-9746-4A82CD06B73E}.Release|x86.Build.0 = Release|Win32 {787EC629-C0FB-4BA9-9746-4A82CD06B73E}.TestRelease|Any CPU.ActiveCfg = Release|x64 {787EC629-C0FB-4BA9-9746-4A82CD06B73E}.TestRelease|ARM64.ActiveCfg = Release|ARM64 + {787EC629-C0FB-4BA9-9746-4A82CD06B73E}.TestRelease|ARM64.Build.0 = Release|ARM64 {787EC629-C0FB-4BA9-9746-4A82CD06B73E}.TestRelease|x64.ActiveCfg = Release|x64 {787EC629-C0FB-4BA9-9746-4A82CD06B73E}.TestRelease|x64.Build.0 = Release|x64 {787EC629-C0FB-4BA9-9746-4A82CD06B73E}.TestRelease|x86.ActiveCfg = Release|Win32 @@ -989,10 +1006,10 @@ Global {BB14D603-F44E-4415-8770-BF3E13F4C17F}.Debug|x86.ActiveCfg = Debug|Win32 {BB14D603-F44E-4415-8770-BF3E13F4C17F}.Debug|x86.Build.0 = Debug|Win32 {BB14D603-F44E-4415-8770-BF3E13F4C17F}.Fuzzing|Any CPU.ActiveCfg = Release|x64 - {BB14D603-F44E-4415-8770-BF3E13F4C17F}.Fuzzing|ARM64.ActiveCfg = Debug|ARM64 - {BB14D603-F44E-4415-8770-BF3E13F4C17F}.Fuzzing|x64.ActiveCfg = Debug|x64 - {BB14D603-F44E-4415-8770-BF3E13F4C17F}.Fuzzing|x86.ActiveCfg = Debug|Win32 - {BB14D603-F44E-4415-8770-BF3E13F4C17F}.PowerShell|Any CPU.ActiveCfg = Release|x64 + {BB14D603-F44E-4415-8770-BF3E13F4C17F}.Fuzzing|ARM64.ActiveCfg = Release|ARM64 + {BB14D603-F44E-4415-8770-BF3E13F4C17F}.Fuzzing|x64.ActiveCfg = Release|x64 + {BB14D603-F44E-4415-8770-BF3E13F4C17F}.Fuzzing|x86.ActiveCfg = Release|Win32 + {BB14D603-F44E-4415-8770-BF3E13F4C17F}.PowerShell|Any CPU.ActiveCfg = ReleaseStatic|x64 {BB14D603-F44E-4415-8770-BF3E13F4C17F}.PowerShell|ARM64.ActiveCfg = ReleaseStatic|ARM64 {BB14D603-F44E-4415-8770-BF3E13F4C17F}.PowerShell|ARM64.Build.0 = ReleaseStatic|ARM64 {BB14D603-F44E-4415-8770-BF3E13F4C17F}.PowerShell|x64.ActiveCfg = ReleaseStatic|x64 @@ -1022,12 +1039,11 @@ Global {F3F6E699-BC5D-4950-8A05-E49DD9EB0D51}.Debug|x86.Build.0 = Debug|Win32 {F3F6E699-BC5D-4950-8A05-E49DD9EB0D51}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|x64 {F3F6E699-BC5D-4950-8A05-E49DD9EB0D51}.Fuzzing|ARM64.ActiveCfg = Fuzzing|x64 - {F3F6E699-BC5D-4950-8A05-E49DD9EB0D51}.Fuzzing|ARM64.Build.0 = Fuzzing|x64 {F3F6E699-BC5D-4950-8A05-E49DD9EB0D51}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {F3F6E699-BC5D-4950-8A05-E49DD9EB0D51}.Fuzzing|x64.Build.0 = Fuzzing|x64 {F3F6E699-BC5D-4950-8A05-E49DD9EB0D51}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {F3F6E699-BC5D-4950-8A05-E49DD9EB0D51}.Fuzzing|x86.Build.0 = Fuzzing|Win32 - {F3F6E699-BC5D-4950-8A05-E49DD9EB0D51}.PowerShell|Any CPU.ActiveCfg = Release|x64 + {F3F6E699-BC5D-4950-8A05-E49DD9EB0D51}.PowerShell|Any CPU.ActiveCfg = ReleaseStatic|x64 {F3F6E699-BC5D-4950-8A05-E49DD9EB0D51}.PowerShell|ARM64.ActiveCfg = ReleaseStatic|ARM64 {F3F6E699-BC5D-4950-8A05-E49DD9EB0D51}.PowerShell|ARM64.Build.0 = ReleaseStatic|ARM64 {F3F6E699-BC5D-4950-8A05-E49DD9EB0D51}.PowerShell|x64.ActiveCfg = ReleaseStatic|x64 @@ -1056,11 +1072,10 @@ Global {CA460806-5E41-4E97-9A3D-1D74B433B663}.Debug|x86.ActiveCfg = Debug|Win32 {CA460806-5E41-4E97-9A3D-1D74B433B663}.Debug|x86.Build.0 = Debug|Win32 {CA460806-5E41-4E97-9A3D-1D74B433B663}.Fuzzing|Any CPU.ActiveCfg = Release|x64 - {CA460806-5E41-4E97-9A3D-1D74B433B663}.Fuzzing|ARM64.ActiveCfg = Debug|ARM64 - {CA460806-5E41-4E97-9A3D-1D74B433B663}.Fuzzing|ARM64.Build.0 = Debug|ARM64 - {CA460806-5E41-4E97-9A3D-1D74B433B663}.Fuzzing|x64.ActiveCfg = Debug|x64 - {CA460806-5E41-4E97-9A3D-1D74B433B663}.Fuzzing|x86.ActiveCfg = Debug|Win32 - {CA460806-5E41-4E97-9A3D-1D74B433B663}.PowerShell|Any CPU.ActiveCfg = Release|x64 + {CA460806-5E41-4E97-9A3D-1D74B433B663}.Fuzzing|ARM64.ActiveCfg = Release|ARM64 + {CA460806-5E41-4E97-9A3D-1D74B433B663}.Fuzzing|x64.ActiveCfg = Release|x64 + {CA460806-5E41-4E97-9A3D-1D74B433B663}.Fuzzing|x86.ActiveCfg = Release|Win32 + {CA460806-5E41-4E97-9A3D-1D74B433B663}.PowerShell|Any CPU.ActiveCfg = ReleaseStatic|x64 {CA460806-5E41-4E97-9A3D-1D74B433B663}.PowerShell|ARM64.ActiveCfg = ReleaseStatic|ARM64 {CA460806-5E41-4E97-9A3D-1D74B433B663}.PowerShell|ARM64.Build.0 = ReleaseStatic|ARM64 {CA460806-5E41-4E97-9A3D-1D74B433B663}.PowerShell|x64.ActiveCfg = ReleaseStatic|x64 @@ -1089,11 +1104,10 @@ Global {E8454BF1-2068-4513-A525-ABF55CC8742C}.Debug|x86.ActiveCfg = Debug|Any CPU {E8454BF1-2068-4513-A525-ABF55CC8742C}.Debug|x86.Build.0 = Debug|Any CPU {E8454BF1-2068-4513-A525-ABF55CC8742C}.Fuzzing|Any CPU.ActiveCfg = Release|Any CPU - {E8454BF1-2068-4513-A525-ABF55CC8742C}.Fuzzing|ARM64.ActiveCfg = Debug|Any CPU - {E8454BF1-2068-4513-A525-ABF55CC8742C}.Fuzzing|ARM64.Build.0 = Debug|Any CPU - {E8454BF1-2068-4513-A525-ABF55CC8742C}.Fuzzing|x64.ActiveCfg = Debug|Any CPU - {E8454BF1-2068-4513-A525-ABF55CC8742C}.Fuzzing|x86.ActiveCfg = Debug|Any CPU - {E8454BF1-2068-4513-A525-ABF55CC8742C}.PowerShell|Any CPU.ActiveCfg = Release|Any CPU + {E8454BF1-2068-4513-A525-ABF55CC8742C}.Fuzzing|ARM64.ActiveCfg = Release|Any CPU + {E8454BF1-2068-4513-A525-ABF55CC8742C}.Fuzzing|x64.ActiveCfg = Release|Any CPU + {E8454BF1-2068-4513-A525-ABF55CC8742C}.Fuzzing|x86.ActiveCfg = Release|Any CPU + {E8454BF1-2068-4513-A525-ABF55CC8742C}.PowerShell|Any CPU.ActiveCfg = ReleaseStatic|Any CPU {E8454BF1-2068-4513-A525-ABF55CC8742C}.PowerShell|ARM64.ActiveCfg = ReleaseStatic|Any CPU {E8454BF1-2068-4513-A525-ABF55CC8742C}.PowerShell|ARM64.Build.0 = ReleaseStatic|Any CPU {E8454BF1-2068-4513-A525-ABF55CC8742C}.PowerShell|x64.ActiveCfg = ReleaseStatic|Any CPU @@ -1122,10 +1136,9 @@ Global {EE43C990-7789-4A60-B077-BF0ED3D093A1}.Debug|x86.ActiveCfg = Debug|x86 {EE43C990-7789-4A60-B077-BF0ED3D093A1}.Debug|x86.Build.0 = Debug|x86 {EE43C990-7789-4A60-B077-BF0ED3D093A1}.Fuzzing|Any CPU.ActiveCfg = Release|x64 - {EE43C990-7789-4A60-B077-BF0ED3D093A1}.Fuzzing|ARM64.ActiveCfg = Debug|arm64 - {EE43C990-7789-4A60-B077-BF0ED3D093A1}.Fuzzing|ARM64.Build.0 = Debug|arm64 - {EE43C990-7789-4A60-B077-BF0ED3D093A1}.Fuzzing|x64.ActiveCfg = Debug|x64 - {EE43C990-7789-4A60-B077-BF0ED3D093A1}.Fuzzing|x86.ActiveCfg = Debug|x86 + {EE43C990-7789-4A60-B077-BF0ED3D093A1}.Fuzzing|ARM64.ActiveCfg = Release|arm64 + {EE43C990-7789-4A60-B077-BF0ED3D093A1}.Fuzzing|x64.ActiveCfg = Release|x64 + {EE43C990-7789-4A60-B077-BF0ED3D093A1}.Fuzzing|x86.ActiveCfg = Release|x86 {EE43C990-7789-4A60-B077-BF0ED3D093A1}.PowerShell|Any CPU.ActiveCfg = Release|x64 {EE43C990-7789-4A60-B077-BF0ED3D093A1}.PowerShell|ARM64.ActiveCfg = Release|arm64 {EE43C990-7789-4A60-B077-BF0ED3D093A1}.PowerShell|x64.ActiveCfg = Release|x64 @@ -1149,11 +1162,10 @@ Global {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Debug|x86.ActiveCfg = Debug|Any CPU {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Debug|x86.Build.0 = Debug|Any CPU {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Fuzzing|Any CPU.ActiveCfg = Release|Any CPU - {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Fuzzing|ARM64.ActiveCfg = Debug|Any CPU - {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Fuzzing|ARM64.Build.0 = Debug|Any CPU - {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Fuzzing|x64.ActiveCfg = Debug|Any CPU - {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Fuzzing|x86.ActiveCfg = Debug|Any CPU - {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.PowerShell|Any CPU.ActiveCfg = Release|Any CPU + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Fuzzing|ARM64.ActiveCfg = Release|Any CPU + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Fuzzing|x64.ActiveCfg = Release|Any CPU + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Fuzzing|x86.ActiveCfg = Release|Any CPU + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.PowerShell|Any CPU.ActiveCfg = ReleaseStatic|Any CPU {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.PowerShell|ARM64.ActiveCfg = ReleaseStatic|Any CPU {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.PowerShell|ARM64.Build.0 = ReleaseStatic|Any CPU {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.PowerShell|x64.ActiveCfg = ReleaseStatic|Any CPU @@ -1182,10 +1194,9 @@ Global {6597EB04-D105-49A7-A5A3-D27FE1DF895E}.Debug|x86.ActiveCfg = Debug|x86 {6597EB04-D105-49A7-A5A3-D27FE1DF895E}.Debug|x86.Build.0 = Debug|x86 {6597EB04-D105-49A7-A5A3-D27FE1DF895E}.Fuzzing|Any CPU.ActiveCfg = Release|x64 - {6597EB04-D105-49A7-A5A3-D27FE1DF895E}.Fuzzing|ARM64.ActiveCfg = Debug|arm64 - {6597EB04-D105-49A7-A5A3-D27FE1DF895E}.Fuzzing|ARM64.Build.0 = Debug|arm64 - {6597EB04-D105-49A7-A5A3-D27FE1DF895E}.Fuzzing|x64.ActiveCfg = Debug|x64 - {6597EB04-D105-49A7-A5A3-D27FE1DF895E}.Fuzzing|x86.ActiveCfg = Debug|x86 + {6597EB04-D105-49A7-A5A3-D27FE1DF895E}.Fuzzing|ARM64.ActiveCfg = Release|arm64 + {6597EB04-D105-49A7-A5A3-D27FE1DF895E}.Fuzzing|x64.ActiveCfg = Release|x64 + {6597EB04-D105-49A7-A5A3-D27FE1DF895E}.Fuzzing|x86.ActiveCfg = Release|x86 {6597EB04-D105-49A7-A5A3-D27FE1DF895E}.PowerShell|Any CPU.ActiveCfg = Release|x64 {6597EB04-D105-49A7-A5A3-D27FE1DF895E}.PowerShell|ARM64.ActiveCfg = Release|arm64 {6597EB04-D105-49A7-A5A3-D27FE1DF895E}.PowerShell|x64.ActiveCfg = Release|x64 @@ -1212,11 +1223,10 @@ Global {1F56BECB-D65D-4BBA-8788-6671B251392A}.Debug|x86.ActiveCfg = Debug|Any CPU {1F56BECB-D65D-4BBA-8788-6671B251392A}.Debug|x86.Build.0 = Debug|Any CPU {1F56BECB-D65D-4BBA-8788-6671B251392A}.Fuzzing|Any CPU.ActiveCfg = Release|Any CPU - {1F56BECB-D65D-4BBA-8788-6671B251392A}.Fuzzing|ARM64.ActiveCfg = Debug|Any CPU - {1F56BECB-D65D-4BBA-8788-6671B251392A}.Fuzzing|ARM64.Build.0 = Debug|Any CPU - {1F56BECB-D65D-4BBA-8788-6671B251392A}.Fuzzing|x64.ActiveCfg = Debug|Any CPU - {1F56BECB-D65D-4BBA-8788-6671B251392A}.Fuzzing|x86.ActiveCfg = Debug|Any CPU - {1F56BECB-D65D-4BBA-8788-6671B251392A}.PowerShell|Any CPU.ActiveCfg = Release|Any CPU + {1F56BECB-D65D-4BBA-8788-6671B251392A}.Fuzzing|ARM64.ActiveCfg = Release|Any CPU + {1F56BECB-D65D-4BBA-8788-6671B251392A}.Fuzzing|x64.ActiveCfg = Release|Any CPU + {1F56BECB-D65D-4BBA-8788-6671B251392A}.Fuzzing|x86.ActiveCfg = Release|Any CPU + {1F56BECB-D65D-4BBA-8788-6671B251392A}.PowerShell|Any CPU.ActiveCfg = ReleaseStatic|Any CPU {1F56BECB-D65D-4BBA-8788-6671B251392A}.PowerShell|ARM64.ActiveCfg = Release|Any CPU {1F56BECB-D65D-4BBA-8788-6671B251392A}.PowerShell|x64.ActiveCfg = ReleaseStatic|Any CPU {1F56BECB-D65D-4BBA-8788-6671B251392A}.PowerShell|x64.Build.0 = ReleaseStatic|Any CPU @@ -1240,11 +1250,10 @@ Global {167F634B-A3AD-494E-8E67-B888103E35FF}.Debug|x64.Build.0 = Debug|Any CPU {167F634B-A3AD-494E-8E67-B888103E35FF}.Debug|x86.ActiveCfg = Debug|Any CPU {167F634B-A3AD-494E-8E67-B888103E35FF}.Debug|x86.Build.0 = Debug|Any CPU - {167F634B-A3AD-494E-8E67-B888103E35FF}.Fuzzing|Any CPU.ActiveCfg = ReleaseStatic|Any CPU - {167F634B-A3AD-494E-8E67-B888103E35FF}.Fuzzing|ARM64.ActiveCfg = Debug|Any CPU - {167F634B-A3AD-494E-8E67-B888103E35FF}.Fuzzing|ARM64.Build.0 = Debug|Any CPU - {167F634B-A3AD-494E-8E67-B888103E35FF}.Fuzzing|x64.ActiveCfg = Debug|Any CPU - {167F634B-A3AD-494E-8E67-B888103E35FF}.Fuzzing|x86.ActiveCfg = Debug|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.Fuzzing|Any CPU.ActiveCfg = Release|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.Fuzzing|ARM64.ActiveCfg = Release|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.Fuzzing|x64.ActiveCfg = Release|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.Fuzzing|x86.ActiveCfg = Release|Any CPU {167F634B-A3AD-494E-8E67-B888103E35FF}.PowerShell|Any CPU.ActiveCfg = ReleaseStatic|Any CPU {167F634B-A3AD-494E-8E67-B888103E35FF}.PowerShell|ARM64.ActiveCfg = Release|Any CPU {167F634B-A3AD-494E-8E67-B888103E35FF}.PowerShell|x64.ActiveCfg = ReleaseStatic|Any CPU @@ -1269,11 +1278,10 @@ Global {C54F80ED-B736-49B0-9BD3-662F57024D01}.Debug|x64.Build.0 = Debug|Any CPU {C54F80ED-B736-49B0-9BD3-662F57024D01}.Debug|x86.ActiveCfg = Debug|Any CPU {C54F80ED-B736-49B0-9BD3-662F57024D01}.Debug|x86.Build.0 = Debug|Any CPU - {C54F80ED-B736-49B0-9BD3-662F57024D01}.Fuzzing|Any CPU.ActiveCfg = ReleaseStatic|Any CPU - {C54F80ED-B736-49B0-9BD3-662F57024D01}.Fuzzing|ARM64.ActiveCfg = Debug|Any CPU - {C54F80ED-B736-49B0-9BD3-662F57024D01}.Fuzzing|ARM64.Build.0 = Debug|Any CPU - {C54F80ED-B736-49B0-9BD3-662F57024D01}.Fuzzing|x64.ActiveCfg = Debug|Any CPU - {C54F80ED-B736-49B0-9BD3-662F57024D01}.Fuzzing|x86.ActiveCfg = Debug|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.Fuzzing|Any CPU.ActiveCfg = Release|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.Fuzzing|ARM64.ActiveCfg = Release|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.Fuzzing|x64.ActiveCfg = Release|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.Fuzzing|x86.ActiveCfg = Release|Any CPU {C54F80ED-B736-49B0-9BD3-662F57024D01}.PowerShell|Any CPU.ActiveCfg = ReleaseStatic|Any CPU {C54F80ED-B736-49B0-9BD3-662F57024D01}.PowerShell|ARM64.ActiveCfg = Release|Any CPU {C54F80ED-B736-49B0-9BD3-662F57024D01}.PowerShell|x64.ActiveCfg = ReleaseStatic|Any CPU @@ -1299,11 +1307,10 @@ Global {2268D5AD-7F2A-485A-8C4B-C574497514C9}.Debug|x86.ActiveCfg = Debug|Win32 {2268D5AD-7F2A-485A-8C4B-C574497514C9}.Debug|x86.Build.0 = Debug|Win32 {2268D5AD-7F2A-485A-8C4B-C574497514C9}.Fuzzing|Any CPU.ActiveCfg = Release|x64 - {2268D5AD-7F2A-485A-8C4B-C574497514C9}.Fuzzing|ARM64.ActiveCfg = ReleaseStatic|ARM64 - {2268D5AD-7F2A-485A-8C4B-C574497514C9}.Fuzzing|ARM64.Build.0 = ReleaseStatic|ARM64 - {2268D5AD-7F2A-485A-8C4B-C574497514C9}.Fuzzing|x64.ActiveCfg = ReleaseStatic|x64 - {2268D5AD-7F2A-485A-8C4B-C574497514C9}.Fuzzing|x86.ActiveCfg = Debug|Win32 - {2268D5AD-7F2A-485A-8C4B-C574497514C9}.PowerShell|Any CPU.ActiveCfg = Release|x64 + {2268D5AD-7F2A-485A-8C4B-C574497514C9}.Fuzzing|ARM64.ActiveCfg = Release|ARM64 + {2268D5AD-7F2A-485A-8C4B-C574497514C9}.Fuzzing|x64.ActiveCfg = Release|x64 + {2268D5AD-7F2A-485A-8C4B-C574497514C9}.Fuzzing|x86.ActiveCfg = Release|Win32 + {2268D5AD-7F2A-485A-8C4B-C574497514C9}.PowerShell|Any CPU.ActiveCfg = ReleaseStatic|x64 {2268D5AD-7F2A-485A-8C4B-C574497514C9}.PowerShell|ARM64.ActiveCfg = ReleaseStatic|ARM64 {2268D5AD-7F2A-485A-8C4B-C574497514C9}.PowerShell|ARM64.Build.0 = ReleaseStatic|ARM64 {2268D5AD-7F2A-485A-8C4B-C574497514C9}.PowerShell|x64.ActiveCfg = ReleaseStatic|x64 @@ -1357,17 +1364,17 @@ Global {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.Debug|x64.Build.0 = Debug|Any CPU {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.Debug|x86.ActiveCfg = Debug|Any CPU {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.Debug|x86.Build.0 = Debug|Any CPU - {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.Fuzzing|Any CPU.ActiveCfg = Debug|Any CPU - {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.Fuzzing|ARM64.ActiveCfg = Debug|Any CPU - {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.Fuzzing|x64.ActiveCfg = Debug|Any CPU - {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.Fuzzing|x86.ActiveCfg = Debug|Any CPU - {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|Any CPU.ActiveCfg = Release|Any CPU - {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|ARM64.ActiveCfg = Release|Any CPU - {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|ARM64.Build.0 = Release|Any CPU - {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|x64.ActiveCfg = Release|Any CPU - {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|x64.Build.0 = Release|Any CPU - {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|x86.ActiveCfg = Release|Any CPU - {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|x86.Build.0 = Release|Any CPU + {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.Fuzzing|Any CPU.ActiveCfg = Release|Any CPU + {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.Fuzzing|ARM64.ActiveCfg = Release|Any CPU + {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.Fuzzing|x64.ActiveCfg = Release|Any CPU + {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.Fuzzing|x86.ActiveCfg = Release|Any CPU + {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|Any CPU.ActiveCfg = ReleaseStatic|Any CPU + {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|ARM64.ActiveCfg = ReleaseStatic|Any CPU + {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|ARM64.Build.0 = ReleaseStatic|Any CPU + {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|x64.ActiveCfg = ReleaseStatic|Any CPU + {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|x64.Build.0 = ReleaseStatic|Any CPU + {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|x86.ActiveCfg = ReleaseStatic|Any CPU + {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|x86.Build.0 = ReleaseStatic|Any CPU {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.Release|Any CPU.ActiveCfg = Release|Any CPU {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.Release|ARM64.ActiveCfg = Release|Any CPU {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.Release|ARM64.Build.0 = Release|Any CPU @@ -1379,6 +1386,38 @@ Global {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.TestRelease|ARM64.ActiveCfg = Release|Any CPU {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.TestRelease|x64.ActiveCfg = Release|Any CPU {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.TestRelease|x86.ActiveCfg = Release|Any CPU + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.Debug|Any CPU.ActiveCfg = Debug|x64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.Debug|ARM64.Build.0 = Debug|ARM64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.Debug|x64.ActiveCfg = Debug|x64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.Debug|x64.Build.0 = Debug|x64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.Debug|x86.ActiveCfg = Debug|Win32 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.Debug|x86.Build.0 = Debug|Win32 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.Fuzzing|Any CPU.ActiveCfg = Release|x64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.Fuzzing|ARM64.ActiveCfg = Release|ARM64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.Fuzzing|x64.ActiveCfg = Release|x64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.Fuzzing|x86.ActiveCfg = Release|Win32 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.PowerShell|Any CPU.ActiveCfg = ReleaseStatic|x64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.PowerShell|ARM64.ActiveCfg = ReleaseStatic|ARM64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.PowerShell|ARM64.Build.0 = ReleaseStatic|ARM64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.PowerShell|x64.ActiveCfg = ReleaseStatic|x64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.PowerShell|x64.Build.0 = ReleaseStatic|x64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.PowerShell|x86.ActiveCfg = ReleaseStatic|Win32 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.PowerShell|x86.Build.0 = ReleaseStatic|Win32 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.Release|Any CPU.ActiveCfg = Release|x64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.Release|ARM64.ActiveCfg = Release|ARM64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.Release|ARM64.Build.0 = Release|ARM64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.Release|x64.ActiveCfg = Release|x64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.Release|x64.Build.0 = Release|x64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.Release|x86.ActiveCfg = Release|Win32 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.Release|x86.Build.0 = Release|Win32 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.TestRelease|Any CPU.ActiveCfg = Release|x64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.TestRelease|ARM64.ActiveCfg = Release|ARM64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.TestRelease|ARM64.Build.0 = Release|ARM64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.TestRelease|x64.ActiveCfg = Release|x64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.TestRelease|x64.Build.0 = Release|x64 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.TestRelease|x86.ActiveCfg = Release|Win32 + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}.TestRelease|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1409,6 +1448,8 @@ Global {C54F80ED-B736-49B0-9BD3-662F57024D01} = {7C218A3E-9BC8-48FF-B91B-BCACD828C0C9} {272B2B0E-40D4-4F0F-B187-519A6EF89B10} = {7C218A3E-9BC8-48FF-B91B-BCACD828C0C9} {5A52D9FC-0059-4A4A-8196-427A7AA0D1C5} = {7C218A3E-9BC8-48FF-B91B-BCACD828C0C9} + {1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4} = {60618CAC-2995-4DF9-9914-45C6FC02C995} + {76B26B2C-602A-4AD0-9736-4162D3FCA92A} = {1A5D7A7D-5CB2-47D5-B40D-4E61CAEDC798} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B6FDB70C-A751-422C-ACD1-E35419495857} diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -98,6 +98,44 @@ <LinkIncremental>false</LinkIncremental> <OutDir>$(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\</OutDir> </PropertyGroup> + <PropertyGroup Label="Vcpkg"> + <VcpkgEnableManifest>true</VcpkgEnableManifest> + <VcpkgApplocalDeps>false</VcpkgApplocalDeps> + <VcpkgInstalledDir>$(ProjectDir)..\vcpkg_installed</VcpkgInstalledDir> + <VcpkgUseStatic></VcpkgUseStatic> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'"> + <VcpkgTriplet>arm</VcpkgTriplet> + <VcpkgConfiguration>Debug</VcpkgConfiguration> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'"> + <VcpkgTriplet>arm64</VcpkgTriplet> + <VcpkgConfiguration>Debug</VcpkgConfiguration> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> + <VcpkgTriplet>x86</VcpkgTriplet> + <VcpkgConfiguration>Debug</VcpkgConfiguration> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> + <VcpkgTriplet>x64</VcpkgTriplet> + <VcpkgConfiguration>Debug</VcpkgConfiguration> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Release|ARM'"> + <VcpkgTriplet>arm-release</VcpkgTriplet> + <VcpkgConfiguration>Release</VcpkgConfiguration> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'"> + <VcpkgTriplet>arm64-release</VcpkgTriplet> + <VcpkgConfiguration>Release</VcpkgConfiguration> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> + <VcpkgTriplet>x86-release</VcpkgTriplet> + <VcpkgConfiguration>Release</VcpkgConfiguration> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> + <VcpkgTriplet>x64-release</VcpkgTriplet> + <VcpkgConfiguration>Release</VcpkgConfiguration> + </PropertyGroup> <ItemDefinitionGroup> <ClCompile> <PrecompiledHeader>Use</PrecompiledHeader> @@ -946,6 +984,9 @@ <ProjectReference Include="..\Microsoft.Management.Configuration\Microsoft.Management.Configuration.vcxproj"> <Project>{ca460806-5e41-4e97-9a3d-1d74b433b663}</Project> </ProjectReference> + <ProjectReference Include="..\SfsClient\SfsClient.vcxproj"> + <Project>{1b9077b3-8923-4ecd-8fc9-b3190fcbe4d4}</Project> + </ProjectReference> <ProjectReference Include="..\YamlCppLib\YamlCppLib.vcxproj"> <Project>{8bb94bb8-374f-4294-bca1-c7811514a6b7}</Project> </ProjectReference> @@ -966,4 +1007,4 @@ <Error Condition="!Exists('$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.props'))" /> <Error Condition="!Exists('$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.targets'))" /> </Target> -</Project>- \ No newline at end of file +</Project> diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -247,9 +247,9 @@ <ClCompile> <Optimization>Disabled</Optimization> <PreprocessorDefinitions>_NO_ASYNCRTIMP;_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING;_DEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\PureLib\pure;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\PureLib\pure;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\PureLib\pure;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\PureLib\pure;$(ProjectDir)..\SfsClient\sfs-client\client\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\PureLib\pure;$(ProjectDir)..\SfsClient\sfs-client\client\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\PureLib\pure;$(ProjectDir)..\SfsClient\sfs-client\client\include;%(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> @@ -273,7 +273,7 @@ <ItemDefinitionGroup Condition="'$(Platform)'=='Win32'"> <ClCompile> <PreprocessorDefinitions>_NO_ASYNCRTIMP;_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING;WIN32;%(PreprocessorDefinitions);CLICOREDLLBUILD</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\PureLib\pure;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\PureLib\pure;$(ProjectDir)..\SfsClient\sfs-client\client\include;%(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> @@ -289,10 +289,10 @@ <FunctionLevelLinking>true</FunctionLevelLinking> <IntrinsicFunctions>true</IntrinsicFunctions> <PreprocessorDefinitions>_NO_ASYNCRTIMP;_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING;NDEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\PureLib\pure;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\PureLib\pure;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\PureLib\pure;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\PureLib\pure;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\PureLib\pure;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\SfsClient\sfs-client\client\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\PureLib\pure;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\SfsClient\sfs-client\client\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\PureLib\pure;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\SfsClient\sfs-client\client\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\PureLib\pure;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\SfsClient\sfs-client\client\include;%(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> @@ -326,10 +326,10 @@ <FunctionLevelLinking>true</FunctionLevelLinking> <IntrinsicFunctions>true</IntrinsicFunctions> <PreprocessorDefinitions>_NO_ASYNCRTIMP;_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING;NDEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|ARM'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\PureLib\pure;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|ARM64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\PureLib\pure;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|Win32'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\PureLib\pure;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|x64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\PureLib\pure;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|ARM'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\PureLib\pure;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\SfsClient\sfs-client\client\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|ARM64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\PureLib\pure;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\SfsClient\sfs-client\client\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|Win32'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\PureLib\pure;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\SfsClient\sfs-client\client\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|x64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\AppInstallerSharedLib;$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;$(ProjectDir)..\PureLib\pure;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\SfsClient\sfs-client\client\include;%(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> diff --git a/src/SfsClient/SfsClient.vcxproj b/src/SfsClient/SfsClient.vcxproj @@ -0,0 +1,436 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup Label="Globals"> + <VCProjectVersion>16.0</VCProjectVersion> + <ProjectGuid>{1B9077B3-8923-4ECD-8FC9-B3190FCBE4D4}</ProjectGuid> + <Keyword>Win32Proj</Keyword> + <WindowsSDKDesktopARMSupport>true</WindowsSDKDesktopARMSupport> + <WindowsSDKDesktopARM64Support>true</WindowsSDKDesktopARM64Support> + <WindowsTargetPlatformVersion>10.0.22000.0</WindowsTargetPlatformVersion> + <WindowsTargetPlatformMinVersion>10.0.17763.0</WindowsTargetPlatformMinVersion> + </PropertyGroup> + <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" /> + <ItemGroup Label="ProjectConfigurations"> + <ProjectConfiguration Include="Debug|ARM"> + <Configuration>Debug</Configuration> + <Platform>ARM</Platform> + </ProjectConfiguration> + <ProjectConfiguration Include="Debug|ARM64"> + <Configuration>Debug</Configuration> + <Platform>ARM64</Platform> + </ProjectConfiguration> + <ProjectConfiguration Include="Debug|Win32"> + <Configuration>Debug</Configuration> + <Platform>Win32</Platform> + </ProjectConfiguration> + <ProjectConfiguration Include="ReleaseStatic|ARM"> + <Configuration>ReleaseStatic</Configuration> + <Platform>ARM</Platform> + </ProjectConfiguration> + <ProjectConfiguration Include="ReleaseStatic|ARM64"> + <Configuration>ReleaseStatic</Configuration> + <Platform>ARM64</Platform> + </ProjectConfiguration> + <ProjectConfiguration Include="ReleaseStatic|Win32"> + <Configuration>ReleaseStatic</Configuration> + <Platform>Win32</Platform> + </ProjectConfiguration> + <ProjectConfiguration Include="ReleaseStatic|x64"> + <Configuration>ReleaseStatic</Configuration> + <Platform>x64</Platform> + </ProjectConfiguration> + <ProjectConfiguration Include="Release|ARM"> + <Configuration>Release</Configuration> + <Platform>ARM</Platform> + </ProjectConfiguration> + <ProjectConfiguration Include="Release|ARM64"> + <Configuration>Release</Configuration> + <Platform>ARM64</Platform> + </ProjectConfiguration> + <ProjectConfiguration Include="Release|Win32"> + <Configuration>Release</Configuration> + <Platform>Win32</Platform> + </ProjectConfiguration> + <ProjectConfiguration Include="Debug|x64"> + <Configuration>Debug</Configuration> + <Platform>x64</Platform> + </ProjectConfiguration> + <ProjectConfiguration Include="Release|x64"> + <Configuration>Release</Configuration> + <Platform>x64</Platform> + </ProjectConfiguration> + </ItemGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration"> + <ConfigurationType>StaticLibrary</ConfigurationType> + <UseDebugLibraries>true</UseDebugLibraries> + <PlatformToolset>v142</PlatformToolset> + <PlatformToolset Condition="'$(VisualStudioVersion)' == '17.0'">v143</PlatformToolset> + <CharacterSet>Unicode</CharacterSet> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration"> + <ConfigurationType>StaticLibrary</ConfigurationType> + <UseDebugLibraries>false</UseDebugLibraries> + <PlatformToolset>v142</PlatformToolset> + <PlatformToolset Condition="'$(VisualStudioVersion)' == '17.0'">v143</PlatformToolset> + <CharacterSet>Unicode</CharacterSet> + <SpectreMitigation>Spectre</SpectreMitigation> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|Win32'" Label="Configuration"> + <ConfigurationType>StaticLibrary</ConfigurationType> + <UseDebugLibraries>false</UseDebugLibraries> + <PlatformToolset>v142</PlatformToolset> + <PlatformToolset Condition="'$(VisualStudioVersion)' == '17.0'">v143</PlatformToolset> + <CharacterSet>Unicode</CharacterSet> + <SpectreMitigation>Spectre</SpectreMitigation> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration"> + <ConfigurationType>StaticLibrary</ConfigurationType> + <UseDebugLibraries>true</UseDebugLibraries> + <PlatformToolset>v142</PlatformToolset> + <PlatformToolset Condition="'$(VisualStudioVersion)' == '17.0'">v143</PlatformToolset> + <CharacterSet>Unicode</CharacterSet> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration"> + <ConfigurationType>StaticLibrary</ConfigurationType> + <UseDebugLibraries>false</UseDebugLibraries> + <PlatformToolset>v142</PlatformToolset> + <PlatformToolset Condition="'$(VisualStudioVersion)' == '17.0'">v143</PlatformToolset> + <CharacterSet>Unicode</CharacterSet> + <SpectreMitigation>Spectre</SpectreMitigation> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|x64'" Label="Configuration"> + <ConfigurationType>StaticLibrary</ConfigurationType> + <UseDebugLibraries>false</UseDebugLibraries> + <PlatformToolset>v142</PlatformToolset> + <PlatformToolset Condition="'$(VisualStudioVersion)' == '17.0'">v143</PlatformToolset> + <CharacterSet>Unicode</CharacterSet> + <SpectreMitigation>Spectre</SpectreMitigation> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'" Label="Configuration"> + <ConfigurationType>StaticLibrary</ConfigurationType> + <UseDebugLibraries>true</UseDebugLibraries> + <PlatformToolset>v142</PlatformToolset> + <PlatformToolset Condition="'$(VisualStudioVersion)' == '17.0'">v143</PlatformToolset> + <CharacterSet>Unicode</CharacterSet> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration"> + <ConfigurationType>StaticLibrary</ConfigurationType> + <UseDebugLibraries>true</UseDebugLibraries> + <PlatformToolset>v142</PlatformToolset> + <PlatformToolset Condition="'$(VisualStudioVersion)' == '17.0'">v143</PlatformToolset> + <CharacterSet>Unicode</CharacterSet> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM'" Label="Configuration"> + <ConfigurationType>StaticLibrary</ConfigurationType> + <UseDebugLibraries>false</UseDebugLibraries> + <PlatformToolset>v142</PlatformToolset> + <PlatformToolset Condition="'$(VisualStudioVersion)' == '17.0'">v143</PlatformToolset> + <CharacterSet>Unicode</CharacterSet> + <SpectreMitigation>Spectre</SpectreMitigation> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|ARM'" Label="Configuration"> + <ConfigurationType>StaticLibrary</ConfigurationType> + <UseDebugLibraries>false</UseDebugLibraries> + <PlatformToolset>v142</PlatformToolset> + <PlatformToolset Condition="'$(VisualStudioVersion)' == '17.0'">v143</PlatformToolset> + <CharacterSet>Unicode</CharacterSet> + <SpectreMitigation>Spectre</SpectreMitigation> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration"> + <ConfigurationType>StaticLibrary</ConfigurationType> + <UseDebugLibraries>false</UseDebugLibraries> + <PlatformToolset>v142</PlatformToolset> + <PlatformToolset Condition="'$(VisualStudioVersion)' == '17.0'">v143</PlatformToolset> + <CharacterSet>Unicode</CharacterSet> + <SpectreMitigation>Spectre</SpectreMitigation> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|ARM64'" Label="Configuration"> + <ConfigurationType>StaticLibrary</ConfigurationType> + <UseDebugLibraries>false</UseDebugLibraries> + <PlatformToolset>v142</PlatformToolset> + <PlatformToolset Condition="'$(VisualStudioVersion)' == '17.0'">v143</PlatformToolset> + <CharacterSet>Unicode</CharacterSet> + <SpectreMitigation>Spectre</SpectreMitigation> + </PropertyGroup> + <PropertyGroup Label="Vcpkg"> + <VcpkgEnabled>true</VcpkgEnabled> + <VcpkgEnableManifest>true</VcpkgEnableManifest> + <VcpkgManifestInstall>true</VcpkgManifestInstall> + <VcpkgInstalledDir>$(ProjectDir)..\vcpkg_installed</VcpkgInstalledDir> + </PropertyGroup> + <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" /> + <ImportGroup Label="ExtensionSettings"> + </ImportGroup> + <ImportGroup Label="Shared"> + </ImportGroup> + <PropertyGroup Label="UserMacros" /> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> + <LinkIncremental>true</LinkIncremental> + <OutDir>$(SolutionDir)$(PlatformShortname)\$(Configuration)\$(ProjectName)\</OutDir> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> + <LinkIncremental>true</LinkIncremental> + <OutDir>$(SolutionDir)$(PlatformShortname)\$(Configuration)\$(ProjectName)\</OutDir> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|Win32'"> + <LinkIncremental>true</LinkIncremental> + <OutDir>$(SolutionDir)$(PlatformShortname)\$(Configuration)\$(ProjectName)\</OutDir> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> + <OutDir>$(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\</OutDir> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|x64'"> + <OutDir>$(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\</OutDir> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM'"> + <OutDir>$(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\</OutDir> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|ARM'"> + <OutDir>$(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\</OutDir> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'"> + <OutDir>$(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\</OutDir> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|ARM64'"> + <OutDir>$(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\</OutDir> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'"> + <OutDir>$(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\</OutDir> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'"> + <OutDir>$(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\</OutDir> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> + <OutDir>$(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\</OutDir> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> + <VcpkgTriplet>x64</VcpkgTriplet> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> + <VcpkgTriplet>x64-release</VcpkgTriplet> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|x64'"> + <VcpkgTriplet>x64-release-static</VcpkgTriplet> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|ARM'"> + <VcpkgTriplet>arm-release-static</VcpkgTriplet> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|ARM64'"> + <VcpkgTriplet>arm64-release-static</VcpkgTriplet> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|Win32'"> + <VcpkgTriplet>x86-release-static</VcpkgTriplet> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'"> + <VcpkgTriplet>arm</VcpkgTriplet> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'"> + <VcpkgTriplet>arm64</VcpkgTriplet> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> + <VcpkgTriplet>x86</VcpkgTriplet> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> + <VcpkgTriplet>x86-release</VcpkgTriplet> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'"> + <VcpkgTriplet>arm64-release</VcpkgTriplet> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Release|ARM'"> + <VcpkgTriplet>arm-release</VcpkgTriplet> + </PropertyGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> + <ClCompile> + <PreprocessorDefinitions>CURL_STATICLIB;GUID_WINDOWS;SFS_VERSION="cf18b35";WIN32;_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <WarningLevel>TurnOffAllWarnings</WarningLevel> + <DebugInformationFormat>ProgramDatabase</DebugInformationFormat> + <LanguageStandard>stdcpp17</LanguageStandard> + <Optimization>Disabled</Optimization> + <AdditionalIncludeDirectories>$(ProjectDir)sfs-client\client\include\sfsclient;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <RuntimeTypeInfo>true</RuntimeTypeInfo> + </ClCompile> + <Link> + <TargetMachine>MachineX86</TargetMachine> + <GenerateDebugInformation>true</GenerateDebugInformation> + <SubSystem>Windows</SubSystem> + </Link> + </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> + <ClCompile> + <PreprocessorDefinitions>CURL_STATICLIB;GUID_WINDOWS;SFS_VERSION="cf18b35";WIN32;NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <WarningLevel>Level3</WarningLevel> + <DebugInformationFormat>ProgramDatabase</DebugInformationFormat> + <LanguageStandard>stdcpp17</LanguageStandard> + <AdditionalIncludeDirectories>$(ProjectDir)sfs-client\client\include\sfsclient;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <RuntimeTypeInfo>true</RuntimeTypeInfo> + </ClCompile> + <Link> + <TargetMachine>MachineX86</TargetMachine> + <GenerateDebugInformation>true</GenerateDebugInformation> + <SubSystem>Windows</SubSystem> + <EnableCOMDATFolding>true</EnableCOMDATFolding> + <OptimizeReferences>true</OptimizeReferences> + </Link> + </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|Win32'"> + <ClCompile> + <PreprocessorDefinitions>CURL_STATICLIB;GUID_WINDOWS;SFS_VERSION="cf18b35";WIN32;NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <WarningLevel>Level3</WarningLevel> + <DebugInformationFormat>ProgramDatabase</DebugInformationFormat> + <LanguageStandard>stdcpp17</LanguageStandard> + <AdditionalIncludeDirectories>$(ProjectDir)sfs-client\client\include\sfsclient;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <RuntimeLibrary>MultiThreaded</RuntimeLibrary> + <RuntimeTypeInfo>true</RuntimeTypeInfo> + </ClCompile> + <Link> + <TargetMachine>MachineX86</TargetMachine> + <GenerateDebugInformation>true</GenerateDebugInformation> + <SubSystem>Windows</SubSystem> + <EnableCOMDATFolding>true</EnableCOMDATFolding> + <OptimizeReferences>true</OptimizeReferences> + </Link> + </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> + <ClCompile> + <LanguageStandard>stdcpp17</LanguageStandard> + <PrecompiledHeader>NotUsing</PrecompiledHeader> + <PreprocessorDefinitions>CURL_STATICLIB;GUID_WINDOWS;SFS_VERSION="cf18b35";NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <WarningLevel>Level3</WarningLevel> + <AdditionalIncludeDirectories>$(ProjectDir)sfs-client\client\include\sfsclient;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <RuntimeTypeInfo>true</RuntimeTypeInfo> + </ClCompile> + </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|x64'"> + <ClCompile> + <LanguageStandard>stdcpp17</LanguageStandard> + <PrecompiledHeader>NotUsing</PrecompiledHeader> + <PreprocessorDefinitions>CURL_STATICLIB;GUID_WINDOWS;SFS_VERSION="cf18b35";NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <WarningLevel>Level3</WarningLevel> + <AdditionalIncludeDirectories>$(ProjectDir)sfs-client\client\include\sfsclient;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <RuntimeLibrary>MultiThreaded</RuntimeLibrary> + <RuntimeTypeInfo>true</RuntimeTypeInfo> + </ClCompile> + </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM'"> + <ClCompile> + <LanguageStandard>stdcpp17</LanguageStandard> + <PrecompiledHeader>NotUsing</PrecompiledHeader> + <PreprocessorDefinitions>CURL_STATICLIB;GUID_WINDOWS;SFS_VERSION="cf18b35";NDEBUG;%(PreprocessorDefinitions)%(PreprocessorDefinitions)</PreprocessorDefinitions> + <WarningLevel>Level3</WarningLevel> + <AdditionalIncludeDirectories>$(ProjectDir)sfs-client\client\include\sfsclient;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <RuntimeTypeInfo>true</RuntimeTypeInfo> + </ClCompile> + </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|ARM'"> + <ClCompile> + <LanguageStandard>stdcpp17</LanguageStandard> + <PrecompiledHeader>NotUsing</PrecompiledHeader> + <PreprocessorDefinitions>CURL_STATICLIB;GUID_WINDOWS;SFS_VERSION="cf18b35";NDEBUG;%(PreprocessorDefinitions)%(PreprocessorDefinitions)</PreprocessorDefinitions> + <WarningLevel>Level3</WarningLevel> + <AdditionalIncludeDirectories>$(ProjectDir)sfs-client\client\include\sfsclient;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <RuntimeLibrary>MultiThreaded</RuntimeLibrary> + <RuntimeTypeInfo>true</RuntimeTypeInfo> + </ClCompile> + </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'"> + <ClCompile> + <LanguageStandard>stdcpp17</LanguageStandard> + <PrecompiledHeader>NotUsing</PrecompiledHeader> + <PreprocessorDefinitions>CURL_STATICLIB;GUID_WINDOWS;SFS_VERSION="cf18b35";NDEBUG;%(PreprocessorDefinitions)%(PreprocessorDefinitions)</PreprocessorDefinitions> + <WarningLevel>Level3</WarningLevel> + <AdditionalIncludeDirectories>$(ProjectDir)sfs-client\client\include\sfsclient;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <RuntimeTypeInfo>true</RuntimeTypeInfo> + </ClCompile> + </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|ARM64'"> + <ClCompile> + <LanguageStandard>stdcpp17</LanguageStandard> + <PrecompiledHeader>NotUsing</PrecompiledHeader> + <PreprocessorDefinitions>CURL_STATICLIB;GUID_WINDOWS;SFS_VERSION="cf18b35";NDEBUG;%(PreprocessorDefinitions)%(PreprocessorDefinitions)</PreprocessorDefinitions> + <WarningLevel>Level3</WarningLevel> + <AdditionalIncludeDirectories>$(ProjectDir)sfs-client\client\include\sfsclient;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <RuntimeLibrary>MultiThreaded</RuntimeLibrary> + <RuntimeTypeInfo>true</RuntimeTypeInfo> + </ClCompile> + </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'"> + <ClCompile> + <LanguageStandard>stdcpp17</LanguageStandard> + <PrecompiledHeader>NotUsing</PrecompiledHeader> + <PreprocessorDefinitions>CURL_STATICLIB;GUID_WINDOWS;SFS_VERSION="cf18b35";_DEBUG;%(PreprocessorDefinitions)%(PreprocessorDefinitions)</PreprocessorDefinitions> + <WarningLevel>TurnOffAllWarnings</WarningLevel> + <AdditionalIncludeDirectories>$(ProjectDir)sfs-client\client\include\sfsclient;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <RuntimeTypeInfo>true</RuntimeTypeInfo> + </ClCompile> + </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'"> + <ClCompile> + <LanguageStandard>stdcpp17</LanguageStandard> + <PrecompiledHeader>NotUsing</PrecompiledHeader> + <PreprocessorDefinitions>CURL_STATICLIB;GUID_WINDOWS;SFS_VERSION="cf18b35";_DEBUG;%(PreprocessorDefinitions)%(PreprocessorDefinitions)</PreprocessorDefinitions> + <WarningLevel>TurnOffAllWarnings</WarningLevel> + <AdditionalIncludeDirectories>$(ProjectDir)sfs-client\client\include\sfsclient;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <RuntimeTypeInfo>true</RuntimeTypeInfo> + </ClCompile> + </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> + <ClCompile> + <LanguageStandard>stdcpp17</LanguageStandard> + <PrecompiledHeader>NotUsing</PrecompiledHeader> + <PreprocessorDefinitions>CURL_STATICLIB;GUID_WINDOWS;SFS_VERSION="cf18b35";_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <WarningLevel>TurnOffAllWarnings</WarningLevel> + <AdditionalIncludeDirectories>$(ProjectDir)sfs-client\client\include\sfsclient;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <RuntimeTypeInfo>true</RuntimeTypeInfo> + </ClCompile> + </ItemDefinitionGroup> + <ItemGroup> + <ClInclude Include="sfs-client\client\include\sfsclient\AppContent.h" /> + <ClInclude Include="sfs-client\client\include\sfsclient\AppFile.h" /> + <ClInclude Include="sfs-client\client\include\sfsclient\ApplicabilityDetails.h" /> + <ClInclude Include="sfs-client\client\include\sfsclient\ClientConfig.h" /> + <ClInclude Include="sfs-client\client\include\sfsclient\Content.h" /> + <ClInclude Include="sfs-client\client\include\sfsclient\ContentId.h" /> + <ClInclude Include="sfs-client\client\include\sfsclient\File.h" /> + <ClInclude Include="sfs-client\client\include\sfsclient\Logging.h" /> + <ClInclude Include="sfs-client\client\include\sfsclient\RequestParams.h" /> + <ClInclude Include="sfs-client\client\include\sfsclient\Result.h" /> + <ClInclude Include="sfs-client\client\include\sfsclient\SFSClient.h" /> + </ItemGroup> + <ItemGroup> + <ClCompile Include="sfs-client\client\src\AppContent.cpp" /> + <ClCompile Include="sfs-client\client\src\AppFile.cpp" /> + <ClCompile Include="sfs-client\client\src\ApplicabilityDetails.cpp" /> + <ClCompile Include="sfs-client\client\src\Content.cpp" /> + <ClCompile Include="sfs-client\client\src\ContentId.cpp" /> + <ClCompile Include="sfs-client\client\src\details\connection\mock\MockConnection.cpp" /> + <ClCompile Include="sfs-client\client\src\details\connection\mock\MockConnectionManager.cpp" /> + <ClCompile Include="sfs-client\client\src\details\TestOverride.cpp" /> + <ClCompile Include="sfs-client\client\src\File.cpp" /> + <ClCompile Include="sfs-client\client\src\Logging.cpp" /> + <ClCompile Include="sfs-client\client\src\Result.cpp" /> + <ClCompile Include="sfs-client\client\src\SFSClient.cpp" /> + <ClCompile Include="sfs-client\client\src\details\ContentUtil.cpp" /> + <ClCompile Include="sfs-client\client\src\details\CorrelationVector.cpp" /> + <ClCompile Include="sfs-client\client\src\details\Env.cpp" /> + <ClCompile Include="sfs-client\client\src\details\ErrorHandling.cpp" /> + <ClCompile Include="sfs-client\client\src\details\OSInfo.cpp" /> + <ClCompile Include="sfs-client\client\src\details\ReportingHandler.cpp" /> + <ClCompile Include="sfs-client\client\src\details\SFSClientImpl.cpp" /> + <ClCompile Include="sfs-client\client\src\details\SFSException.cpp" /> + <ClCompile Include="sfs-client\client\src\details\SFSUrlBuilder.cpp" /> + <ClCompile Include="sfs-client\client\src\details\UrlBuilder.cpp" /> + <ClCompile Include="sfs-client\client\src\details\Util.cpp" /> + <ClCompile Include="sfs-client\client\src\details\connection\Connection.cpp" /> + <ClCompile Include="sfs-client\client\src\details\connection\ConnectionConfig.cpp" /> + <ClCompile Include="sfs-client\client\src\details\connection\ConnectionManager.cpp" /> + <ClCompile Include="sfs-client\client\src\details\connection\CurlConnection.cpp" /> + <ClCompile Include="sfs-client\client\src\details\connection\CurlConnectionManager.cpp" /> + <ClCompile Include="sfs-client\client\src\details\connection\HttpHeader.cpp" /> + <ClCompile Include="sfs-client\client\src\details\entity\ContentType.cpp" /> + <ClCompile Include="sfs-client\client\src\details\entity\FileEntity.cpp" /> + <ClCompile Include="sfs-client\client\src\details\entity\VersionEntity.cpp" /> + </ItemGroup> + <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> + <ImportGroup Label="ExtensionTargets"> + </ImportGroup> +</Project> diff --git a/src/SfsClient/SfsClient.vcxproj.filters b/src/SfsClient/SfsClient.vcxproj.filters @@ -0,0 +1,159 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <ItemGroup> + <Filter Include="include"> + <UniqueIdentifier>{7825c1db-0159-4653-b26c-bc674bb9e29f}</UniqueIdentifier> + </Filter> + <Filter Include="include\sfsclient"> + <UniqueIdentifier>{357ffe7c-3ce6-4b0d-9e93-bbda28fb9fc3}</UniqueIdentifier> + </Filter> + <Filter Include="src"> + <UniqueIdentifier>{d7879d14-b049-4837-a336-895bd20ab5d8}</UniqueIdentifier> + </Filter> + <Filter Include="src\details"> + <UniqueIdentifier>{15462934-ddd8-46ad-a0fd-110df7fa2acb}</UniqueIdentifier> + </Filter> + <Filter Include="src\details\connection"> + <UniqueIdentifier>{133cf863-8baa-4ce2-9050-40155b027f04}</UniqueIdentifier> + </Filter> + <Filter Include="src\details\connection\mock"> + <UniqueIdentifier>{93204abc-3d4c-4cf0-9433-db8c4ad667b5}</UniqueIdentifier> + </Filter> + <Filter Include="src\details\entity"> + <UniqueIdentifier>{102d246a-6b50-4171-a909-cb68923ac321}</UniqueIdentifier> + </Filter> + </ItemGroup> + <ItemGroup> + <ClInclude Include="sfs-client\client\include\sfsclient\AppContent.h"> + <Filter>include\sfsclient</Filter> + </ClInclude> + <ClInclude Include="sfs-client\client\include\sfsclient\AppFile.h"> + <Filter>include\sfsclient</Filter> + </ClInclude> + <ClInclude Include="sfs-client\client\include\sfsclient\ApplicabilityDetails.h"> + <Filter>include\sfsclient</Filter> + </ClInclude> + <ClInclude Include="sfs-client\client\include\sfsclient\ClientConfig.h"> + <Filter>include\sfsclient</Filter> + </ClInclude> + <ClInclude Include="sfs-client\client\include\sfsclient\Content.h"> + <Filter>include\sfsclient</Filter> + </ClInclude> + <ClInclude Include="sfs-client\client\include\sfsclient\ContentId.h"> + <Filter>include\sfsclient</Filter> + </ClInclude> + <ClInclude Include="sfs-client\client\include\sfsclient\File.h"> + <Filter>include\sfsclient</Filter> + </ClInclude> + <ClInclude Include="sfs-client\client\include\sfsclient\Logging.h"> + <Filter>include\sfsclient</Filter> + </ClInclude> + <ClInclude Include="sfs-client\client\include\sfsclient\RequestParams.h"> + <Filter>include\sfsclient</Filter> + </ClInclude> + <ClInclude Include="sfs-client\client\include\sfsclient\Result.h"> + <Filter>include\sfsclient</Filter> + </ClInclude> + <ClInclude Include="sfs-client\client\include\sfsclient\SFSClient.h"> + <Filter>include\sfsclient</Filter> + </ClInclude> + </ItemGroup> + <ItemGroup> + <ClCompile Include="sfs-client\client\src\AppContent.cpp"> + <Filter>src</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\AppFile.cpp"> + <Filter>src</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\ApplicabilityDetails.cpp"> + <Filter>src</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\Content.cpp"> + <Filter>src</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\ContentId.cpp"> + <Filter>src</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\File.cpp"> + <Filter>src</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\Logging.cpp"> + <Filter>src</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\Result.cpp"> + <Filter>src</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\SFSClient.cpp"> + <Filter>src</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\ContentUtil.cpp"> + <Filter>src\details</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\CorrelationVector.cpp"> + <Filter>src\details</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\Env.cpp"> + <Filter>src\details</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\ErrorHandling.cpp"> + <Filter>src\details</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\OSInfo.cpp"> + <Filter>src\details</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\ReportingHandler.cpp"> + <Filter>src\details</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\SFSClientImpl.cpp"> + <Filter>src\details</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\SFSException.cpp"> + <Filter>src\details</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\SFSUrlBuilder.cpp"> + <Filter>src\details</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\TestOverride.cpp"> + <Filter>src\details</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\UrlBuilder.cpp"> + <Filter>src\details</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\Util.cpp"> + <Filter>src\details</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\connection\Connection.cpp"> + <Filter>src\details\connection</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\connection\ConnectionConfig.cpp"> + <Filter>src\details\connection</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\connection\ConnectionManager.cpp"> + <Filter>src\details\connection</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\connection\CurlConnection.cpp"> + <Filter>src\details\connection</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\connection\CurlConnectionManager.cpp"> + <Filter>src\details\connection</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\connection\HttpHeader.cpp"> + <Filter>src\details\connection</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\connection\mock\MockConnection.cpp"> + <Filter>src\details\connection\mock</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\connection\mock\MockConnectionManager.cpp"> + <Filter>src\details\connection\mock</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\entity\ContentType.cpp"> + <Filter>src\details\entity</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\entity\FileEntity.cpp"> + <Filter>src\details\entity</Filter> + </ClCompile> + <ClCompile Include="sfs-client\client\src\details\entity\VersionEntity.cpp"> + <Filter>src\details\entity</Filter> + </ClCompile> + </ItemGroup> +</Project> diff --git a/src/SfsClient/readme.md b/src/SfsClient/readme.md @@ -0,0 +1,14 @@ +## SfsClient + +Do not change code under the sfs-client directory; it contains sfs-client source code from commit [cf18b35](https://github.com/microsoft/sfs-client/commits/cf18b35). +It is created using git subtree command: +``` + git subtree add --prefix=src/SfsClient/sfs-client https://github.com/microsoft/sfs-client.git cf18b357f43aa9bbaba7d8b3b3774b39140aa00f --squash +``` + +### Update +To update, run the following command, then update the above commit for reference. 'master' can be replaced with the appropriate commit spec as desired. +``` + git subtree pull -P src/SfsClient/sfs-client https://github.com/microsoft/sfs-client master --squash +``` +**When committing the PR, DO NOT squash it. The two commits are needed as is to allow for future subtree pulls.** diff --git a/src/SfsClient/sfs-client/.clang-format b/src/SfsClient/sfs-client/.clang-format @@ -0,0 +1,29 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# +# -- Usage -- +# Read README.md for initial context, and instructions for VSCode and command line. +# +# See here for the meaning of each item in this file: https://clang.llvm.org/docs/ClangFormatStyleOptions.html +# To get all the options that are part of a standard style such as Microsoft, do this: +# > clang-format --style=Microsoft -dump-config > clang-format-microsoft.txt +# +# -- Philosophy -- +# We will try to adhere to the standard Microsoft style as much as possible. +# The overrides below are deviations from the standard. Keep it short. +--- +Language: Cpp +BasedOnStyle: Microsoft + +AllowAllArgumentsOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterCaseLabel: true +BreakConstructorInitializers: BeforeComma +BreakStringLiterals: false +InsertNewlineAtEOF: true +PackConstructorInitializers: CurrentLine +PointerAlignment: Left diff --git a/src/SfsClient/sfs-client/.cmake-format.json b/src/SfsClient/sfs-client/.cmake-format.json @@ -0,0 +1,7 @@ +{ + "first_comment_is_literal": "true", + "keyword_case": "upper", + "line_ending": "windows", + "max_pargs_hwrap": 3, + "tab_size": 4 +}+ \ No newline at end of file diff --git a/src/SfsClient/sfs-client/.github/CODEOWNERS b/src/SfsClient/sfs-client/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# The SFS Client Reviewers team will be requested for review when someone opens a pull request. +* @microsoft/sfs-client-reviewers+ \ No newline at end of file diff --git a/src/SfsClient/sfs-client/.github/ISSUE_TEMPLATE/bug_report.md b/src/SfsClient/sfs-client/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Additional context** +Add any other context about the problem here. diff --git a/src/SfsClient/sfs-client/.github/ISSUE_TEMPLATE/feature_request.md b/src/SfsClient/sfs-client/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/src/SfsClient/sfs-client/.github/dependabot.yml b/src/SfsClient/sfs-client/.github/dependabot.yml @@ -0,0 +1,19 @@ +# Dependabot helps maintain dependencies updated for different package ecosystems +# +# Here is the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + + # Maintain dependencies for pip + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" diff --git a/src/SfsClient/sfs-client/.github/pull_request_template.md b/src/SfsClient/sfs-client/.github/pull_request_template.md @@ -0,0 +1,15 @@ +#### Related Issues + +- Closes #<!--Related issue--> <!--When this PR is merged, the related issue will be closed--> +- Helps #<!--Related issue--> <!-- When this PR is merged, the related issue will remain open, but there will be a mention saved in the issue--> + +#### Why is this change being made? + +#### What is being changed? + +#### How was the change tested? + +<!-- Uncomment the relevant line below --> +<!-- - Current tests test this behavior: <testnames> --> +<!-- - New tests are being added: <testnames> --> +<!-- - Not a functional change. Ex: modifying documentation, auxiliary files, etc --> diff --git a/src/SfsClient/sfs-client/.github/workflows/install-winget/action.yml b/src/SfsClient/sfs-client/.github/workflows/install-winget/action.yml @@ -0,0 +1,56 @@ +name: Install WinGet + +description: Installs WinGet from sources, caching the installation files if possible + +inputs: + winget-installation-cache: + description: 'Where the installation files will be cached' + required: false + default: 'winget-installation-cache' + +runs: + using: "composite" + + steps: + - name: Cache WinGet installation + id: cache-winget-installation + uses: actions/cache@v4 + with: + path: winget-installation-cache + key: winget-installation + + # Winget is not available in the windows-latest image as it is Windows Server 2022 and winget is not yet available officially for it. + # We can still install it in "experimental" mode manually though. + - name: Install WinGet + shell: pwsh + run: | + # Check it doesn't exist to avoid installing it without need + if (!(Get-Command 'winget.exe' -CommandType Application -ErrorAction SilentlyContinue)) + { + New-Item -ItemType Directory ${{ inputs.winget-installation-cache }} -Force | Out-Null + Set-Location ${{ inputs.winget-installation-cache }} + $ProgressPreference = "silentlyContinue" + $WingetInstaller = "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle" + if (!(Test-Path $WingetInstaller)) + { + Write-Host "Downloading Winget" + Invoke-WebRequest -Uri https://github.com/microsoft/winget-cli/releases/download/v1.7.10582/$WingetInstaller -OutFile $WingetInstaller + } + $VCLibsInstaller = "Microsoft.VCLibs.x64.14.00.Desktop.appx" + if (!(Test-Path $VCLibsInstaller)) + { + Write-Host "Downloading Winget dependency VCLibs" + Invoke-WebRequest -Uri https://aka.ms/$VCLibsInstaller -OutFile $VCLibsInstaller + } + $XamlInstaller = "Microsoft.UI.Xaml.2.8.x64.appx" + if (!(Test-Path $XamlInstaller)) + { + Write-Host "Downloading Winget dependency Xaml" + Invoke-WebRequest -Uri https://github.com/microsoft/microsoft-ui-xaml/releases/download/v2.8.6/$XamlInstaller -OutFile $XamlInstaller + } + + Import-Module -Name Appx -UseWindowsPowershell | Out-Null + Add-AppxPackage $VCLibsInstaller + Add-AppxPackage $XamlInstaller + Add-AppxPackage $WingetInstaller + } diff --git a/src/SfsClient/sfs-client/.github/workflows/main-build-ubuntu.yml b/src/SfsClient/sfs-client/.github/workflows/main-build-ubuntu.yml @@ -0,0 +1,38 @@ +name: Main Build (Ubuntu) + +on: + push: + branches: [ "main" ] + +# Permissions and environment values to be able to update the dependency graph with vcpkg information +permissions: + contents: write + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VCPKG_FEATURE_FLAGS: dependencygraph + +jobs: + build-ubuntu: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup + run: source ./scripts/setup.sh + + - name: Build and Test (no test overrides) + run: | + ./scripts/build.sh + ./scripts/test.sh --output-on-failure + + - name: Build and Test (with test overrides) + run: | + ./scripts/build.sh --enable-test-overrides + ./scripts/test.sh --output-on-failure + + - name: Build and Test (Release) + run: | + ./scripts/build.sh --build-type Release + ./scripts/test.sh --output-on-failure diff --git a/src/SfsClient/sfs-client/.github/workflows/main-build-windows.yml b/src/SfsClient/sfs-client/.github/workflows/main-build-windows.yml @@ -0,0 +1,45 @@ +name: Main Build (Windows) + +on: + push: + branches: [ "main" ] + +# Permissions and environment values to be able to update the dependency graph with vcpkg information +permissions: + contents: write + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VCPKG_FEATURE_FLAGS: dependencygraph + +jobs: + build-windows: + runs-on: 'windows-latest' + + steps: + - uses: actions/checkout@v4 + + - name: Install Winget + uses: ./.github/workflows/install-winget + + - name: Setup + shell: pwsh + run: .\scripts\Setup.ps1 -NoBuildTools + + - name: Build and Test (no test overrides) + shell: pwsh + run: | + .\scripts\Build.ps1 + .\scripts\Test.ps1 -OutputOnFailure + + - name: Build and Test (with test overrides) + shell: pwsh + run: | + .\scripts\Build.ps1 -EnableTestOverrides + .\scripts\Test.ps1 -OutputOnFailure + + - name: Build and Test (Release) + shell: pwsh + run: | + .\scripts\Build.ps1 -BuildType Release + .\scripts\Test.ps1 -OutputOnFailure diff --git a/src/SfsClient/sfs-client/.github/workflows/pr.yml b/src/SfsClient/sfs-client/.github/workflows/pr.yml @@ -0,0 +1,75 @@ +name: PR Build + +env: + # Set up vcpkg to read from the GitHub cache (https://learn.microsoft.com/en-us/vcpkg/consume/binary-caching-github-actions-cache) + VCPKG_BINARY_SOURCES: 'clear;x-gha,readwrite' + +on: + pull_request: + branches: [ "main" ] + +jobs: + build-windows: + runs-on: 'windows-latest' + + steps: + - uses: actions/checkout@v4 + + - name: Set up required environment variables for vcpkg cache + uses: actions/github-script@v7 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Install Winget + uses: ./.github/workflows/install-winget + + - name: Setup + shell: pwsh + run: .\scripts\Setup.ps1 -NoBuildTools + + - name: Check formatting + shell: pwsh + run: python .\scripts\check-format.py + + - name: Build and Test (no test overrides) + shell: pwsh + run: | + .\scripts\Build.ps1 + .\scripts\Test.ps1 -OutputOnFailure + + - name: Build and Test (with test overrides) + shell: pwsh + run: | + .\scripts\Build.ps1 -EnableTestOverrides + .\scripts\Test.ps1 -OutputOnFailure + + build-ubuntu: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up required environment variables for vcpkg cache + uses: actions/github-script@v7 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Setup + run: source ./scripts/setup.sh + + - name: Check formatting + run: python ./scripts/check-format.py + + - name: Build and Test (no test overrides) + run: | + ./scripts/build.sh + ./scripts/test.sh --output-on-failure + + - name: Build and Test (with test overrides) + run: | + ./scripts/build.sh --enable-test-overrides + ./scripts/test.sh --output-on-failure diff --git a/src/SfsClient/sfs-client/.gitignore b/src/SfsClient/sfs-client/.gitignore @@ -0,0 +1,45 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# Build +/build + +# VSCode specific folder +/.vscode + +# Vcpkg +/vcpkg +/vcpkg_installed + +# Formatting temporary file +/.tmp_formatted diff --git a/src/SfsClient/sfs-client/API.md b/src/SfsClient/sfs-client/API.md @@ -0,0 +1,59 @@ +## SFSClient + +To start using the SFSClient library, use `SFSClient::Make()` to create an `SFSClient` instance, which allows you to use the SFS APIs. +The first argument to the factory is a `ClientConfig` struct. Configuring this struct allows you to customize the behavior of the client. +Refer to the documentation of the `ClientConfig` struct in [ClientConfig.h](client/include/sfsclient/ClientConfig.h) to see the available options. + +## Logging Callback + +To retrieve logging information from the API, set a logging callback in `ClientConfig::logCallbackFn` when constructing an SFSClient instance with `SFSClient::Make()`. + +The logging callback function has the signature: + +```cpp +void callback(const SFS::LogData&); +``` + +An example to log the data directly to the standard output using `std::cout`: + +```cpp +void LoggingCallback(const SFS::LogData& logData) +{ + std::cout << "Log: [" << ToString(logData.severity) << "]" << " " << logData.file << ":" + << logData.line << " " << logData.message << std::endl; +} +``` + +Notes: +- The callback itself is processed in the main thread. Do not use a blocking callback. If heavy processing has to be done, consider capturing the data and processing another thread. +- The LogData contents only exist within the callback call. If the processing will be done later, you should copy the data elsewhere. +- The callback should not do any re-entrant calls (e.g. call `SFSClient` methods). + +## Class instances + +It is recommended to only create a single `SFSClient` instance, even if multiple threads will be used. +Each `GetLatestDownloadInfo()` call will create its own connection and should not interfere with other calls. + +### Thread safety + +All API calls are thread-safe. + +If a logging callback is set in a multi-threaded environment, and the same `SFSClient()` is reused across different threads, the same callback will be called by all usages of the class. So, make sure the callback itself is also thread-safe. + +## Content types + +A few data types are provided which abstract contents that can be sent by the SFS Service, such as `Content`, `ContentId`, `File`. +These data types provide `noexcept` methods to interact with member data. + +## Retry Behavior + +The API follows a certain set of rules to retry upon reaching specific HTTP Status Codes. The behavior is configurable through the `retryOnError` member of `RequestParams`. + +By default, the client will retry up to 3 times when reaching the following HTTP Status Codes: +- 429: Too Many Requests +- 500: Internal Server Error +- 502: Bad Gateway +- 503: Server Busy +- 504: Gateway Timeout + +Between each retry the Client will wait an interval that follows either the `Retry-After` response header, or an exponential backoff calculation with a factor of 2 starting from 15s. diff --git a/src/SfsClient/sfs-client/CMakeLists.txt b/src/SfsClient/sfs-client/CMakeLists.txt @@ -0,0 +1,79 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cmake_minimum_required(VERSION 3.19) + +# Tell CMake to look in cmake for our custom CMake module files. +set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +include(SFSOptions) + +if(SFS_BUILD_TESTS) + list(APPEND VCPKG_MANIFEST_FEATURES "tests") +endif() + +set(CMAKE_TOOLCHAIN_FILE "vcpkg/scripts/buildsystems/vcpkg.cmake") + +# By default using x64 static custom triplet for Windows. Can be overridden by +# setting VCPKG_TARGET_TRIPLET +if(SFS_WINDOWS_STATIC_ONLY + AND WIN32 + AND NOT DEFINED VCPKG_TARGET_TRIPLET) + set(VCPKG_TARGET_TRIPLET "x64-windows-static-custom") +endif() + +# Semver versioning. Given MAJOR.MINOR.PATCH, increment the: +# +# 1. MAJOR version when you make incompatible API changes +# 2. MINOR version when you add functionality in a backward compatible manner +# 3. PATCH version when you make backward compatible bug fixes +set(SFS_LIBRARY_VERSION "0.1.0") + +project( + sfsclient + VERSION ${SFS_LIBRARY_VERSION} + LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED True) + +# Use this function to set warning level and warnings as errors for a given +# target +function(set_compile_options_for_target target) + if(MSVC) + # Enable some MSVC warnings that are off by default: + # + # * 4062: all enums are handled in switch statements without a default + # label + # * 4191: unsafe conversion from 'type of expression' to 'type required' + # * 4242: possible loss of data (identifier) + # * 4254: possible loss of data (operator) + # * 4287: 'operator': unsigned/negative constant mismatch + # * 4296: 'operator': expression is always true (or false) + # * 4388: signed/unsigned mismatch + # * 4800: implicit conversion to bool; possible information loss + # * 4946: reinterpret_cast used between related classes + + # cmake-format: off + target_compile_options(${target} PRIVATE /W4 /WX /we4062 /we4191 /we4242 /we4254 /we4287 /we4296 /we4388 /we4800 /we4946) + # cmake-format: on + else() + target_compile_options( + ${target} + PRIVATE -Wall + -Wextra + -Wpedantic + -Werror) + endif() +endfunction() + +if(SFS_WINDOWS_STATIC_ONLY AND MSVC) + # For MSVC, use a multi-threaded statically-linked runtime library + set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>") +endif() + +add_subdirectory(client) + +if(SFS_BUILD_SAMPLES) + add_subdirectory(samples) +endif() diff --git a/src/SfsClient/sfs-client/CODE_OF_CONDUCT.md b/src/SfsClient/sfs-client/CODE_OF_CONDUCT.md @@ -0,0 +1,9 @@ +# Microsoft Open Source Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +Resources: + +- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) +- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns diff --git a/src/SfsClient/sfs-client/DEVELOPMENT.md b/src/SfsClient/sfs-client/DEVELOPMENT.md @@ -0,0 +1,6 @@ +# cgmanifest.json + +Keep this file updated as you update `vcpkg.json`. It must indicate the dependencies we use for proper automatic scanning of dependencies done in the Azure DevOps backend. +The folder `build\vcpkg_installed\vcpkg\info` is a good source of truth of which vcpkg packages are installed. It does however accumulate temporary packages you may have added in the past. Before using it, try cleaning the build folder and re-building. + +Once you find the GitHub repo of the dependency, use the version on the tags page to find the commit hash that corresponds to the installed version. diff --git a/src/SfsClient/sfs-client/LICENSE b/src/SfsClient/sfs-client/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/src/SfsClient/sfs-client/NOTICE.md b/src/SfsClient/sfs-client/NOTICE.md @@ -0,0 +1,441 @@ +# NOTICES + +This repository incorporates material as listed below or described in the code. + +--------------------------------------------------------- + +**Component:** + +CorrelationVector-Cpp + +**Open Source License/Copyright Notice:** + +MIT License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE + +--------------------------------------------------------- + +**Component:** + +curl + +**Open Source License/Copyright Notice:** + +COPYRIGHT AND PERMISSION NOTICE + +Copyright (c) 1996 - 2024, Daniel Stenberg, <daniel@haxx.se>, and many +contributors, see the THANKS file. + +All rights reserved. + +Permission to use, copy, modify, and distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright +notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN +NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall not +be used in advertising or otherwise to promote the sale, use or other dealings +in this Software without prior written authorization of the copyright holder. + +--------------------------------------------------------- + +**Component:** + +c-ares + +**Open Source License/Copyright Notice:** + +MIT License + +Copyright (c) 1998 Massachusetts Institute of Technology Copyright (c) 2007 - 2023 Daniel Stenberg with many contributors, see AUTHORS file. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +**Additional Attribution:** + +c-ares is based on ares, and these are the people that have worked on it since +the fork was made: + +Albert Chin +Alex Loukissas +Alexander Klauer +Alexander Lazic +Alexey Simak +Andreas Rieke +Andrew Andkjar +Andrew Ayer +Andrew C. Morrow +Ashish Sharma +Ben Greear +Ben Noordhuis +BogDan Vatra +Brad House +Brad Spencer +Bram Matthys +Chris Araman +Dan Fandrich +Daniel Johnson +Daniel Stenberg +David Drysdale +David Stuart +Denis Bilenko +Dima Tisnek +Dirk Manske +Dominick Meglio +Doug Goldstein +Doug Kwan +Duncan Wilcox +Eino Tuominen +Erik Kline +Fedor Indutny +Frederic Germain +Geert Uytterhoeven +George Neill +Gisle Vanem +Google LLC +Gregor Jasny +Guenter Knauf +Guilherme Balena Versiani +Gunter Knauf +Henrik Stoerner +Jakub Hrozek +James Bursa +Jérémy Lal +John Schember +Keith Shaw +Lei Shi +Marko Kreen +Michael Wallner +Mike Crowe +Nick Alcock +Nick Mathewson +Nicolas "Pixel" Noble +Ning Dong +Oleg Pudeyev +Patrick Valsecchi +Patrik Thunstrom +Paul Saab +Peter Pentchev +Phil Blundell +Poul Thomas Lomholt +Ravi Pratap +Robin Cornelius +Saúl Ibarra Corretgé +Sebastian at basti79.de +Shmulik Regev +Stefan Bühler +Steinar H. Gunderson +Svante Karlsson +Tofu Linden +Tom Hughes +Tor Arntsen +Viktor Szakats +Vlad Dinulescu +William Ahern +Yang Tse +hpopescu at ixiacom.com +liren at vivisimo.com +nordsturm +saghul + +--------------------------------------------------------- + +**Component:** + +openssl + +**Open Source License/Copyright Notice:** + +<pre> + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS +</pre> + +**Additional Attribution:** + +<pre> + +Authors +======= + +This is the list of OpenSSL authors for copyright purposes. +It does not necessarily list everyone who has contributed code, +since in some cases, their employer may be the copyright holder. +To see the full list of contributors, see the revision history in +source control. + +Groups +------ + + * OpenSSL Software Services, Inc. + * OpenSSL Software Foundation, Inc. + +Individuals +----------- + + * Andy Polyakov + * Ben Laurie + * Ben Kaduk + * Bernd Edlinger + * Bodo Möller + * David Benjamin + * David von Oheimb + * Dmitry Belyavskiy (Дмитрий Белявский) + * Emilia Käsper + * Eric Young + * Geoff Thorpe + * Holger Reif + * Kurt Roeckx + * Lutz Jänicke + * Mark J. Cox + * Matt Caswell + * Matthias St. Pierre + * Nicola Tuveri + * Nils Larsch + * Patrick Steuer + * Paul Dale + * Paul C. Sutton + * Paul Yang + * Ralf S. Engelschall + * Rich Salz + * Richard Levitte + * Shane Lontis + * Stephen Henson + * Steve Marquess + * Tim Hudson + * Tomáš Mráz + * Ulf Möller + * Viktor Dukhovni +</pre> + +--------------------------------------------------------- + +**Component:** nlohmann/json + +**Open Source License/Copyright Notice:** + +MIT License + +Copyright (c) 2013-2022 Niels Lohmann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/SfsClient/sfs-client/README.md b/src/SfsClient/sfs-client/README.md @@ -0,0 +1,195 @@ +# SFS Client + +[![Windows Latest](https://github.com/microsoft/sfs-client/actions/workflows/main-build-windows.yml/badge.svg?branch=main&event=push)](https://github.com/microsoft/sfs-client/actions/workflows/main-build-windows.yml) [![Ubuntu Latest](https://github.com/microsoft/sfs-client/actions/workflows/main-build-ubuntu.yml/badge.svg?branch=main&event=push)](https://github.com/microsoft/sfs-client/actions/workflows/main-build-ubuntu.yml) + +## Introduction + +This repository holds the Simple File Solution (SFS) Client, a C++ library that simplifies the interface with the SFS service. +Read below to get started on developing and using the library. + +## Usage + +Follow the [API](API.md) document for tips on how to use the API. + +## Getting Started + +### Prerequisites + +#### Setup script + +There are a few dependencies required to work on this project. +To set them up, use the Setup script. + +Windows: +```powershell +.\scripts\Setup.ps1 +``` + +Linux: +```bash +source ./scripts/setup.sh +``` + +The script can be run multiple times as it does not replace what has been installed, and updates dependencies. +It also sets up useful command-line aliases that can be used while developing. + +## Consuming the library + +This library is distributed as Source Code and meant for consumption in this format. Below we outline how to easily consume us through the vcpkg tool, but feel free to use other methods to incorporate the source. + +### vcpkg + +The [vcpkg](https://vcpkg.io/) tool is a Microsoft dependency manager for C/C++. It works "for all platforms, buildsystems, and workflows". +In general the dependencies are registered in the central vcpkg registry, where the "portfile" recipes are hosted. These files indicate the way the dependencies should be acquired and built. + +One of the features it provides is also a way to find dependencies listed in the local filesystem. See the [overlay-ports](https://learn.microsoft.com/en-us/vcpkg/concepts/overlay-ports) feature to see that. +We are not hosted in the central vcpkg registry, but we provide a template overlay-port for easy consumption of the library. See the [sfs-client-vcpkg-port](./sfs-client-vcpkg-port) folder for the files you need to have in your local repository in order to consume us. A few placeholders have to be filled in on those files. + +## Formatting + +This project is currently using the clang-format tool to format its source code according to predefined rules. +It is also using cmake-format to format the CMakeLists.txt files. +Both are installed automatically with the Setup script. + +### Automatic usage + +The project is configured to automatically run formatting upon committing so nobody introduces +unformatted changes to the codebase. This is done through a pre-commit hook. +If you must avoid the hook, you can use `git commit -n` to bypass it. + +### Running on command line + +Use -h to see the binary help: +``` +clang-format -h +cmake-format -h +``` + +Use -i to edit a file inplace: +``` +clang-format -i ./interface.cpp +cmake-format -i ./CMakeLists.txt +``` + +Wildcards are accepted in clang-format: +``` +clang-format -i ./*.h +``` + +## Building + +To build, use the `build` command. It simplifies the CMake build commands and re-generates CMake configurations if needed. + +Available build options: + +| Switch (PowerShell) | Switch (Bash) | Description | +|----------------------|---------------------------|------------------------------------------------------------------------------------------| +| -Clean | --clean | Use this to clean the build folder before building. | +| -BuildType | --build-type | Use this to define the build type between "Debug" and "Release". The default is "Debug". | +| -EnableTestOverrides | --enable-test-overrides | Use this to enable test overrides. See [TEST](TEST.md) for more. | +| -BuildTests <bool> | --build-tests {ON, OFF} | Use this to build tests alongside the library. On by default. | +| -BuildSamples <bool> | --build-samples {ON, OFF} | Use this to build samples alongside the library. On by default. | + +See [below](#building-with-cmake-vscode-extension) for building within VSCode. + +## VSCode + +[Visual Studio Code](https://code.visualstudio.com) is the recommended editor to work with this project. +But you're free to use other editors and command-line tools. + +### Configuring VSCode includes with CMake + +VSCode has a great integration with CMake through the CMake Tools extension (ms-vscode.cmake-tools). +It allows you to configure and build the project through the UI. + +Open the command pane on VSCode and search for "C/C++: Edit Configurations (JSON)" to create a c_cpp_properties.json file under a .vscode folder in repo root. +The folder is ignored in git by default. +Add the following line to the "configurations" element to make VSCode start using the includes defined in the CMakeLists.txt files. + +```json +"configurations": [ + { + "configurationProvider": "ms-vscode.cmake-tools" + } +] +``` + +### Formatting C++ Sources with VSCode + +Install the extension https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools and Shift+Alt+F inside a file. +You can also access "Format" options by right clicking on an open file or over a line selection. + +If you want it, you can also make VSCode format on each Save operation by adding this to your User JSON settings: +`"editor.formatOnSave": true` + +### vcpkg integration + +From [the vcpkg docs](https://github.com/Microsoft/vcpkg/#visual-studio-code-with-cmake-tools): + +Adding the following to your workspace settings.json will make CMake Tools automatically use vcpkg for libraries: + +```json +{ + "cmake.configureSettings": { + "CMAKE_TOOLCHAIN_FILE": "[vcpkg root]/scripts/buildsystems/vcpkg.cmake" + } +} +``` + +### Building with CMake VSCode extension + +If you're using the CMake Tools extension on VSCode, you can set the build options through the VSCode settings. Add something like below to either your user or workspace JSON settings to get a default value for an option. You can also later use the command "Edit CMake Cache (UI)" for visual editing. + +```json +"cmake.configureArgs": [ + "-DSFS_ENABLE_TEST_OVERRIDES=ON" +] +``` + +See [SFSOptions.cmake](cmake/SFSOptions.cmake) for the CMake options available for the library. + +## Testing + +Tests are compiled alongside the library by default, and live in the client/tests subdirectory. +To run the tests, you can use the `test` command. It will run all tests directly and output the result to the console. + +If you want to customize the test run, you can make use of the `ctest` tool. + +``` +ctest --test-dir ./build/client +``` + +To run specific tests, you can filter the chosen tests through the switch `-R` or `--tests-regex`. +For more test selection switches, use `ctest --help`. + +The tests are built using the Catch2 framework. For a more verbose run you can run the executable directly, with -s. + +Windows: +``` +.\build\tests\bin\<ReleaseConfiguration>\SFSClientTests.exe -s +``` + +Linux: +``` +./build/tests/bin/SFSClientTests -s +``` + +Follow the [TEST](TEST.md) document for more information regarding testing. + +## Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. + +When you submit a pull request, a CLA bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Trademarks + +This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow Microsoft’s Trademark & Brand Guidelines. Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party’s policies. diff --git a/src/SfsClient/sfs-client/SECURITY.md b/src/SfsClient/sfs-client/SECURITY.md @@ -0,0 +1,41 @@ +<!-- BEGIN MICROSOFT SECURITY.MD V0.0.9 BLOCK --> + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). + +<!-- END MICROSOFT SECURITY.MD BLOCK --> diff --git a/src/SfsClient/sfs-client/SUPPORT.md b/src/SfsClient/sfs-client/SUPPORT.md @@ -0,0 +1,11 @@ +# Support + +## How to file issues and get help + +This project uses GitHub Issues to track bugs and feature requests. Please search the existing +issues before filing new issues to avoid duplicates. For new issues, file your bug or +feature request as a new Issue. + +## Microsoft Support Policy + +Support for this **PROJECT or PRODUCT** is limited to the resources listed above. diff --git a/src/SfsClient/sfs-client/TEST.md b/src/SfsClient/sfs-client/TEST.md @@ -0,0 +1,12 @@ +## Testing overrides + +Some test behaviors are controlled by test overrides set via environment variables. +They only work when the CMake Option `SFS_ENABLE_TEST_OVERRIDES` is set to `ON`. +To enable that during build, you must use the switch `-EnableTestOverrides` (`--enable-test-overrides` on Linux) with the build scripts. Click [here](README.md#building) for more. See also [here](README.md#building-with-cmake-vscode-extension) for how to set when using VSCode. + +When the test overrides are enabled, a few environment variables can be used to adjust the behavior of the tool: + +| Environment Variable | Description | +|-----------------------------------------------|---------------------------------------------------------------------------------------------| +| SFS_TEST_BASE_RETRY_DELAY_MS | Set this to override the base retry delay of 15s. | +| SFS_TEST_OVERRIDE_BASE_URL | Set this to any string value which will be used as the SFS URL rather than the default one. | diff --git a/src/SfsClient/sfs-client/cgmanifest.json b/src/SfsClient/sfs-client/cgmanifest.json @@ -0,0 +1,104 @@ +{ + "$schema": "https://json.schemastore.org/component-detection-manifest.json", + "version": 1, + "registrations": [ + { + "component": { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/catchorg/Catch2", + "commitHash": "6e79e682b726f524310d55dec8ddac4e9c52fb5f" + } + }, + "developmentDependency": true + }, + { + "component": { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/microsoft/CorrelationVector-Cpp", + "commitHash": "cf38d2b44baaf352509ad9980786bc49554c32e4" + } + }, + "developmentDependency": false + }, + { + "component": { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/yhirose/cpp-httplib", + "commitHash": "5c00bbf36ba8ff47b4fb97712fc38cb2884e5b98" + } + }, + "developmentDependency": true + }, + { + "component": { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/curl/curl", + "commitHash": "d755a5f7c009dd63a61b2c745180d8ba937cbfeb" + } + }, + "developmentDependency": false + }, + { + "component": { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/c-ares/c-ares", + "commitHash": "fddf01938d3789e06cc1c3774e4cd0c7d2a89976" + } + }, + "developmentDependency": false, + "dependencyRoots": [ + { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/curl/curl", + "commitHash": "d755a5f7c009dd63a61b2c745180d8ba937cbfeb" + } + } + ] + }, + { + "component": { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/openssl/openssl", + "commitHash": "01d5e2318405362b4de5e670c90d9b40a351d053" + } + }, + "developmentDependency": false, + "dependencyRoots": [ + { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/curl/curl", + "commitHash": "d755a5f7c009dd63a61b2c745180d8ba937cbfeb" + } + } + ] + }, + { + "component": { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/microsoft/do-client", + "commitHash": "d71ade6f692dd8bc319ec3228c956517e9b29292" + } + }, + "developmentDependency": true + }, + { + "component": { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/nlohmann/json", + "commitHash": "9cca280a4d0ccf0c08f47a99aa71d1b0e52f8d03" + } + }, + "developmentDependency": false + } + ] +}+ \ No newline at end of file diff --git a/src/SfsClient/sfs-client/client/CMakeLists.txt b/src/SfsClient/sfs-client/client/CMakeLists.txt @@ -0,0 +1,147 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# HTTP stack library +find_package(CURL REQUIRED) + +# JSON library +find_package(nlohmann_json CONFIG REQUIRED) + +# CorrelationVector Library from Microsoft +find_package(correlation_vector CONFIG REQUIRED) + +add_library(${PROJECT_NAME} STATIC) +add_library(Microsoft::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) + +target_sources( + ${PROJECT_NAME} + PRIVATE src/AppContent.cpp + src/AppFile.cpp + src/ApplicabilityDetails.cpp + src/Content.cpp + src/ContentId.cpp + src/details/connection/Connection.cpp + src/details/connection/ConnectionConfig.cpp + src/details/connection/ConnectionManager.cpp + src/details/connection/CurlConnection.cpp + src/details/connection/CurlConnectionManager.cpp + src/details/connection/HttpHeader.cpp + src/details/connection/mock/MockConnection.cpp + src/details/connection/mock/MockConnectionManager.cpp + src/details/ContentUtil.cpp + src/details/CorrelationVector.cpp + src/details/entity/ContentType.cpp + src/details/entity/FileEntity.cpp + src/details/entity/VersionEntity.cpp + src/details/Env.cpp + src/details/ErrorHandling.cpp + src/details/OSInfo.cpp + src/details/ReportingHandler.cpp + src/details/SFSClientImpl.cpp + src/details/SFSException.cpp + src/details/SFSUrlBuilder.cpp + src/details/TestOverride.cpp + src/details/UrlBuilder.cpp + src/details/Util.cpp + src/File.cpp + src/Logging.cpp + src/Result.cpp + src/SFSClient.cpp) + +# Include dir for the library depends on whether the library is being built or +# if it was installed +target_include_directories( + ${PROJECT_NAME} + PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/sfsclient> + $<INSTALL_INTERFACE:include> # <prefix>/include +) + +target_link_libraries(${PROJECT_NAME} PRIVATE CURL::libcurl) +target_link_libraries(${PROJECT_NAME} PRIVATE nlohmann_json::nlohmann_json) +target_link_libraries(${PROJECT_NAME} PRIVATE microsoft::correlation_vector) + +# Pick up git revision during configuration to add to logging +include(FindGit) +if(GIT_FOUND) + execute_process( + COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + OUTPUT_VARIABLE SFS_GIT_HEAD_NAME + OUTPUT_STRIP_TRAILING_WHITESPACE) + execute_process( + COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + OUTPUT_VARIABLE SFS_GIT_HEAD_REVISION + OUTPUT_STRIP_TRAILING_WHITESPACE) + + target_compile_definitions( + ${PROJECT_NAME} + PRIVATE SFS_GIT_INFO="${SFS_GIT_HEAD_NAME}:${SFS_GIT_HEAD_REVISION}") +endif() + +set_compile_options_for_target(${PROJECT_NAME}) + +target_compile_definitions(${PROJECT_NAME} + PRIVATE SFS_VERSION="${SFS_LIBRARY_VERSION}") + +if(SFS_ENABLE_TEST_OVERRIDES) + target_compile_definitions(${PROJECT_NAME} + PRIVATE SFS_ENABLE_TEST_OVERRIDES=1) +endif() + +if(SFS_BUILD_TESTS) + # Enables one to run tests through "ctest --test-dir .\build\client" + enable_testing() + + add_subdirectory(tests) +endif() + +# +# Install section +# + +set(_INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/sfsclient") +set(_TARGET_CONFIG_CMAKE_FILEPATH + "${CMAKE_CURRENT_BINARY_DIR}/sfsclient-config.cmake") + +# Install headers +install( + FILES include/sfsclient/AppContent.h + include/sfsclient/AppFile.h + include/sfsclient/ApplicabilityDetails.h + include/sfsclient/ClientConfig.h + include/sfsclient/Content.h + include/sfsclient/ContentId.h + include/sfsclient/File.h + include/sfsclient/Logging.h + include/sfsclient/RequestParams.h + include/sfsclient/Result.h + include/sfsclient/SFSClient.h + DESTINATION include/sfsclient) + +# Export targets for this library to a local file +install( + TARGETS ${PROJECT_NAME} + EXPORT sfsclient-targets + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) + +# And then install the exported targets file to the destination +install( + EXPORT sfsclient-targets + FILE sfsclient-targets.cmake + NAMESPACE microsoft:: + DESTINATION ${_INSTALL_DESTINATION}) + +# Add configure_package_config_file for below +include(CMakePackageConfigHelpers) + +# Create the sfsclient-config.cmake file, which will be used by find_package() +configure_package_config_file( + ../cmake/sfsclient-config.cmake.in ${_TARGET_CONFIG_CMAKE_FILEPATH} + INSTALL_DESTINATION ${_INSTALL_DESTINATION}) + +# Then install the generated file to the destination +install(FILES ${_TARGET_CONFIG_CMAKE_FILEPATH} + DESTINATION ${_INSTALL_DESTINATION}) diff --git a/src/SfsClient/sfs-client/client/include/sfsclient/AppContent.h b/src/SfsClient/sfs-client/client/include/sfsclient/AppContent.h @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "AppFile.h" +#include "Content.h" + +#include <memory> +#include <string> +#include <vector> + +namespace SFS +{ +class AppPrerequisiteContent +{ + public: + [[nodiscard]] static Result Make(std::unique_ptr<ContentId>&& contentId, + std::vector<AppFile>&& files, + std::unique_ptr<AppPrerequisiteContent>& out) noexcept; + + AppPrerequisiteContent(AppPrerequisiteContent&&) noexcept; + + AppPrerequisiteContent(const AppPrerequisiteContent&) = delete; + AppPrerequisiteContent& operator=(const AppPrerequisiteContent&) = delete; + + /** + * @return Unique content identifier + */ + const ContentId& GetContentId() const noexcept; + + /** + * @return Files belonging to this Prequisite + */ + const std::vector<AppFile>& GetFiles() const noexcept; + + private: + AppPrerequisiteContent() = default; + + std::unique_ptr<ContentId> m_contentId; + std::vector<AppFile> m_files; +}; + +class AppContent +{ + public: + [[nodiscard]] static Result Make(std::unique_ptr<ContentId>&& contentId, + std::string updateId, + std::vector<AppPrerequisiteContent>&& prerequisites, + std::vector<AppFile>&& files, + std::unique_ptr<AppContent>& out) noexcept; + + AppContent(AppContent&&) noexcept; + + AppContent(const AppContent&) = delete; + AppContent& operator=(const AppContent&) = delete; + + /** + * @return Unique content identifier + */ + const ContentId& GetContentId() const noexcept; + + /** + * @return Unique Update Id + */ + const std::string& GetUpdateId() const noexcept; + + /** + * @return Files belonging to this App + */ + const std::vector<AppFile>& GetFiles() const noexcept; + + /** + * @return List of Prerequisite content needed for this App. Prerequisites don't have further dependencies. + */ + const std::vector<AppPrerequisiteContent>& GetPrerequisites() const noexcept; + + private: + AppContent() = default; + + std::unique_ptr<ContentId> m_contentId; + std::vector<AppFile> m_files; + + std::string m_updateId; + std::vector<AppPrerequisiteContent> m_prerequisites; +}; +} // namespace SFS diff --git a/src/SfsClient/sfs-client/client/include/sfsclient/AppFile.h b/src/SfsClient/sfs-client/client/include/sfsclient/AppFile.h @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "ApplicabilityDetails.h" +#include "File.h" + +#include <memory> +#include <string> +#include <unordered_map> +#include <vector> + +namespace SFS +{ +class AppFile : private File +{ + public: + [[nodiscard]] static Result Make(std::string fileId, + std::string url, + uint64_t sizeInBytes, + std::unordered_map<HashType, std::string> hashes, + std::vector<Architecture> architectures, + std::vector<std::string> platformApplicabilityForPackage, + std::string fileMoniker, + std::unique_ptr<AppFile>& out) noexcept; + + AppFile(AppFile&&) noexcept; + + AppFile(const AppFile&) = delete; + AppFile& operator=(const AppFile&) = delete; + + /// Getter methods from File class + using File::GetFileId; + using File::GetHashes; + using File::GetSizeInBytes; + using File::GetUrl; + + /** + * @return Set of details related to applicability of the file + */ + const ApplicabilityDetails& GetApplicabilityDetails() const noexcept; + + /** + * @return Package Moniker of the file + */ + const std::string& GetFileMoniker() const noexcept; + + private: + AppFile(std::string&& fileId, + std::string&& url, + uint64_t sizeInBytes, + std::unordered_map<HashType, std::string>&& hashes, + std::string&& fileMoniker); + + std::unique_ptr<ApplicabilityDetails> m_applicabilityDetails; + std::string m_fileMoniker; +}; +} // namespace SFS diff --git a/src/SfsClient/sfs-client/client/include/sfsclient/ApplicabilityDetails.h b/src/SfsClient/sfs-client/client/include/sfsclient/ApplicabilityDetails.h @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "Result.h" + +#include <memory> +#include <string> +#include <vector> + +namespace SFS +{ +enum class Architecture +{ + None, + Amd64, + Arm, + Arm64, + x86, +}; + +class ApplicabilityDetails +{ + public: + [[nodiscard]] static Result Make(std::vector<Architecture> architectures, + std::vector<std::string> platformApplicabilityForPackage, + std::unique_ptr<ApplicabilityDetails>& out) noexcept; + + ApplicabilityDetails(const ApplicabilityDetails&) = delete; + ApplicabilityDetails& operator=(const ApplicabilityDetails&) = delete; + + const std::vector<Architecture>& GetArchitectures() const noexcept; + const std::vector<std::string>& GetPlatformApplicabilityForPackage() const noexcept; + + private: + ApplicabilityDetails() = default; + + std::vector<Architecture> m_architectures; + std::vector<std::string> m_platformApplicabilityForPackage; +}; +} // namespace SFS diff --git a/src/SfsClient/sfs-client/client/include/sfsclient/ClientConfig.h b/src/SfsClient/sfs-client/client/include/sfsclient/ClientConfig.h @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "Logging.h" + +#include <optional> +#include <string> + +namespace SFS +{ +/// @brief Configurations to create an SFSClient instance +struct ClientConfig +{ + /// @brief The account ID of the SFS service is used to identify the caller (required) + std::string accountId; + + /// @brief The instance ID of the SFS service + std::optional<std::string> instanceId; + + /// @brief The namespace of the SFS service + std::optional<std::string> nameSpace; + + /** + * @brief A logging callback function that is called when the SFSClient logs a message + * @details This function returns logging information from the SFSClient. The caller is responsible for incoporating + * the received data into their logging system. The callback will be called in the same thread as the + * main flow, so make sure the callback does not block for too long so it doesn't delay operations. The + * LogData does not exist after the callback returns, so caller has to copy it if the data will be stored. + */ + std::optional<LoggingCallbackFn> logCallbackFn; +}; +} // namespace SFS diff --git a/src/SfsClient/sfs-client/client/include/sfsclient/Content.h b/src/SfsClient/sfs-client/client/include/sfsclient/Content.h @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "ContentId.h" +#include "File.h" +#include "Result.h" + +#include <memory> +#include <string> +#include <vector> + +namespace SFS +{ +class Content +{ + public: + /** + * @brief This Make() method should be used when the caller wants the @param files to be cloned + */ + [[nodiscard]] static Result Make(std::string contentNameSpace, + std::string contentName, + std::string contentVersion, + const std::vector<File>& files, + std::unique_ptr<Content>& out) noexcept; + + /** + * @brief This Make() method should be used when the caller wants the @param files to be moved + */ + [[nodiscard]] static Result Make(std::string contentNameSpace, + std::string contentName, + std::string contentVersion, + std::vector<File>&& files, + std::unique_ptr<Content>& out) noexcept; + + /** + * @brief This Make() method should be used when the caller wants the @param contentId and @param files to be moved + */ + [[nodiscard]] static Result Make(std::unique_ptr<ContentId>&& contentId, + std::vector<File>&& files, + std::unique_ptr<Content>& out) noexcept; + + Content(Content&&) noexcept; + + Content(const Content&) = delete; + Content& operator=(const Content&) = delete; + + /** + * @return Unique content identifier + */ + const ContentId& GetContentId() const noexcept; + + const std::vector<File>& GetFiles() const noexcept; + + private: + Content() = default; + + std::unique_ptr<ContentId> m_contentId; + std::vector<File> m_files; +}; +} // namespace SFS diff --git a/src/SfsClient/sfs-client/client/include/sfsclient/ContentId.h b/src/SfsClient/sfs-client/client/include/sfsclient/ContentId.h @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "Result.h" + +#include <memory> +#include <string> + +namespace SFS +{ +class ContentId +{ + public: + [[nodiscard]] static Result Make(std::string nameSpace, + std::string name, + std::string version, + std::unique_ptr<ContentId>& out) noexcept; + + ContentId(ContentId&&) noexcept; + + ContentId(const ContentId&) = delete; + ContentId& operator=(const ContentId&) = delete; + + /** + * @return Content namespace + */ + const std::string& GetNameSpace() const noexcept; + + /** + * @return Content name + */ + const std::string& GetName() const noexcept; + + /** + * @return 4-part integer version. Each part can range from 0-65535 + */ + const std::string& GetVersion() const noexcept; + + private: + ContentId() = default; + + std::string m_nameSpace; + std::string m_name; + std::string m_version; +}; +} // namespace SFS diff --git a/src/SfsClient/sfs-client/client/include/sfsclient/File.h b/src/SfsClient/sfs-client/client/include/sfsclient/File.h @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "Result.h" + +#include <cstdint> +#include <memory> +#include <string> +#include <unordered_map> + +namespace SFS +{ +enum class HashType +{ + Sha1, + Sha256 +}; + +class File +{ + public: + [[nodiscard]] static Result Make(std::string fileId, + std::string url, + uint64_t sizeInBytes, + std::unordered_map<HashType, std::string> hashes, + std::unique_ptr<File>& out) noexcept; + + File(File&&) noexcept; + + File(const File&) = delete; + File& operator=(const File&) = delete; + + /** + * @return Unique file identifier within a content version + */ + const std::string& GetFileId() const noexcept; + + /** + * @return Download URL + */ + const std::string& GetUrl() const noexcept; + + /** + * @return File size in number of bytes + */ + uint64_t GetSizeInBytes() const noexcept; + + /** + * @return Dictionary of algorithm type to base64 encoded file hash string + */ + const std::unordered_map<HashType, std::string>& GetHashes() const noexcept; + + protected: + File(std::string&& fileId, + std::string&& url, + uint64_t sizeInBytes, + std::unordered_map<HashType, std::string>&& hashes); + + [[nodiscard]] Result Clone(std::unique_ptr<File>& out) const noexcept; + + friend class Content; + + std::string m_fileId; + std::string m_url; + uint64_t m_sizeInBytes; + std::unordered_map<HashType, std::string> m_hashes; +}; +} // namespace SFS diff --git a/src/SfsClient/sfs-client/client/include/sfsclient/Logging.h b/src/SfsClient/sfs-client/client/include/sfsclient/Logging.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include <chrono> +#include <functional> +#include <string> + +namespace SFS +{ +enum class LogSeverity +{ + Info, + Warning, + Error, + Verbose, +}; + +struct LogData +{ + LogSeverity severity; + const char* message; + const char* file; + unsigned line; + const char* function; + std::chrono::time_point<std::chrono::system_clock> time; +}; + +using LoggingCallbackFn = std::function<void(const LogData&)>; + +std::string_view ToString(LogSeverity severity) noexcept; +}; // namespace SFS diff --git a/src/SfsClient/sfs-client/client/include/sfsclient/RequestParams.h b/src/SfsClient/sfs-client/client/include/sfsclient/RequestParams.h @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include <optional> +#include <string> +#include <unordered_map> +#include <vector> + +namespace SFS +{ +using TargetingAttributes = std::unordered_map<std::string, std::string>; + +constexpr unsigned c_maxRetries = 3; + +struct ProductRequest +{ + /// @brief The name or GUID that uniquely represents the product in the service (required) + std::string product; + + /// @brief Key-value pair to filter the data retrieved from the service. Known from publishing (optional) + TargetingAttributes attributes; +}; + +/// @brief Configurations to perform a request to the SFS service +struct RequestParams +{ + /// @brief List of products to be retrieved from the server (required) + /// @note At the moment only a single product request is supported. Using a vector for future implementation of + /// batch requests + std::vector<ProductRequest> productRequests; + + /// @brief Base CorrelationVector to be used in the request for service telemetry stitching (optional) + /// @note If not provided, a new CorrelationVector will be generated + std::optional<std::string> baseCV; + + /// @brief Retry for a web request after a failed attempt. If true, client will retry up to c_maxRetries times + bool retryOnError{true}; +}; +} // namespace SFS diff --git a/src/SfsClient/sfs-client/client/include/sfsclient/Result.h b/src/SfsClient/sfs-client/client/include/sfsclient/Result.h @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include <cstdint> +#include <string> +#include <string_view> + +namespace SFS +{ +class Result +{ + public: + enum Code : uint32_t + { + Success = 0x00000000, + + // + // Failure codes + // + + // Generic errors start at 0x8000'0000 + InvalidArg = 0x8000'0001, + NotImpl = 0x8000'0002, + NotSet = 0x8000'0003, + OutOfMemory = 0x8000'0004, + Unexpected = 0x8000'0005, + + // Connection errors start at 0x8000'1000 + ConnectionSetupFailed = 0x8000'1000, + ConnectionUnexpectedError = 0x8000'1001, + ConnectionUrlSetupFailed = 0x8000'1002, + + // Http Errors start at 0x8000'2000 + // Generic Http errors + HttpTimeout = 0x8000'2000, + HttpUnexpected = 0x8000'2001, + + // Last 3 digits mapped to Http status codes + HttpBadRequest = 0x8000'2400, + HttpNotFound = 0x8000'2404, + HttpMethodNotAllowed = 0x8000'2405, + HttpTooManyRequests = 0x8000'2429, + HttpServiceNotAvailable = 0x8000'2503, + + // Service errors start at 0x8000'3000 + ServiceInvalidResponse = 0x8000'3000, + ServiceUnexpectedContentType = 0x8000'3001, + }; + + Result(Code code) noexcept; + Result(Code code, std::string message) noexcept; + + Code GetCode() const noexcept; + + /// @brief Returns the message associated with the result code. + /// @note "GetMsg" is used instead of "GetMessage" to avoid conflicts with Windows API. + const std::string& GetMsg() const noexcept; + + bool IsSuccess() const noexcept; + bool IsFailure() const noexcept; + + /** + * @brief Returns true if the result code represents a successful operation. + */ + operator bool() const noexcept; + + bool operator==(Code resultCode) const noexcept; + bool operator!=(Code resultCode) const noexcept; + + // Results should not be compared directly + bool operator==(Result) const = delete; + bool operator!=(Result) const = delete; + bool operator>(Result) const = delete; + bool operator>=(Result) const = delete; + bool operator<(Result) const = delete; + bool operator<=(Result) const = delete; + + private: + Code m_code; + std::string m_message; +}; + +std::string_view ToString(Result::Code code) noexcept; +} // namespace SFS + +std::ostream& operator<<(std::ostream& os, const SFS::Result& value); +std::ostream& operator<<(std::ostream& os, const SFS::Result::Code& value); diff --git a/src/SfsClient/sfs-client/client/include/sfsclient/SFSClient.h b/src/SfsClient/sfs-client/client/include/sfsclient/SFSClient.h @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "AppContent.h" +#include "ClientConfig.h" +#include "Content.h" +#include "Logging.h" +#include "RequestParams.h" +#include "Result.h" + +#include <memory> +#include <optional> +#include <string> +#include <vector> + +namespace SFS +{ +namespace details +{ +class SFSClientInterface; +} + +class SFSClient +{ + public: + ~SFSClient() noexcept; + + SFSClient(const SFSClient&) = delete; + SFSClient& operator=(const SFSClient&) = delete; + + /** + * @brief Make a new SFSClient object + * @details An SFSClient object is used to make calls to the SFS service. The SFSClient object is initialized with + * a few parameters that are used to build the URL for the SFS service. The URL is built as follows: + * https://{accountId}.api.cdp.microsoft.com/api/v2/contents/{instanceId}/namespaces/{nameSpace} + * The instanceId and nameSpace are optionally set in @param config and have a default value if not provided. + * The accountId is required and must be set to a non-empty value. + * + * @param config Describes a set of startup configurations for the SFSClient + */ + [[nodiscard]] static Result Make(ClientConfig config, std::unique_ptr<SFSClient>& out) noexcept; + + // + // API to retrieve download information from the SFS Service + // + + /** + * @brief Retrieve combined metadata & download URLs from the latest version of specified products + * @note At the moment only a single product request is supported + * @param requestParams Parameters that define this request + * @param contents A vector of Content that is populated with the result + */ + [[nodiscard]] Result GetLatestDownloadInfo(const RequestParams& requestParams, + std::vector<Content>& contents) const noexcept; + + /** + * @brief Retrieve combined metadata & download URLs from the latest version of specified apps + * @note At the moment only a single product request is supported + * @param requestParams Parameters that define this request + * @param contents A vector of AppContent that is populated with the result + */ + [[nodiscard]] Result GetLatestAppDownloadInfo(const RequestParams& requestParams, + std::vector<AppContent>& contents) const noexcept; + + /** + * @return The version of the SFSClient library + */ + static const char* GetVersion() noexcept; + + private: + /** + * @brief Construct a new SFSClient object + */ + SFSClient() noexcept; + + std::unique_ptr<details::SFSClientInterface> m_impl; +}; +} // namespace SFS diff --git a/src/SfsClient/sfs-client/client/src/AppContent.cpp b/src/SfsClient/sfs-client/client/src/AppContent.cpp @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "AppContent.h" + +#include "details/ErrorHandling.h" + +using namespace SFS; + +Result AppPrerequisiteContent::Make(std::unique_ptr<ContentId>&& contentId, + std::vector<AppFile>&& files, + std::unique_ptr<AppPrerequisiteContent>& out) noexcept +try +{ + out.reset(); + + std::unique_ptr<AppPrerequisiteContent> tmp(new AppPrerequisiteContent()); + tmp->m_contentId = std::move(contentId); + tmp->m_files = std::move(files); + + out = std::move(tmp); + + return Result::Success; +} +SFS_CATCH_RETURN() + +AppPrerequisiteContent::AppPrerequisiteContent(AppPrerequisiteContent&& other) noexcept +{ + m_contentId = std::move(other.m_contentId); + m_files = std::move(other.m_files); +} + +const ContentId& AppPrerequisiteContent::GetContentId() const noexcept +{ + return *m_contentId; +} + +const std::vector<AppFile>& AppPrerequisiteContent::GetFiles() const noexcept +{ + return m_files; +} + +Result AppContent::Make(std::unique_ptr<ContentId>&& contentId, + std::string updateId, + std::vector<AppPrerequisiteContent>&& prerequisites, + std::vector<AppFile>&& files, + std::unique_ptr<AppContent>& out) noexcept +try +{ + out.reset(); + + std::unique_ptr<AppContent> tmp(new AppContent()); + tmp->m_contentId = std::move(contentId); + tmp->m_updateId = std::move(updateId); + tmp->m_prerequisites = std::move(prerequisites); + tmp->m_files = std::move(files); + + out = std::move(tmp); + + return Result::Success; +} +SFS_CATCH_RETURN() + +AppContent::AppContent(AppContent&& other) noexcept +{ + m_contentId = std::move(other.m_contentId); + m_updateId = std::move(other.m_updateId); + m_prerequisites = std::move(other.m_prerequisites); + m_files = std::move(other.m_files); +} + +const ContentId& AppContent::GetContentId() const noexcept +{ + return *m_contentId; +} + +const std::string& AppContent::GetUpdateId() const noexcept +{ + return m_updateId; +} + +const std::vector<AppFile>& AppContent::GetFiles() const noexcept +{ + return m_files; +} + +const std::vector<AppPrerequisiteContent>& AppContent::GetPrerequisites() const noexcept +{ + return m_prerequisites; +} diff --git a/src/SfsClient/sfs-client/client/src/AppFile.cpp b/src/SfsClient/sfs-client/client/src/AppFile.cpp @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "AppFile.h" + +#include "details/ErrorHandling.h" + +using namespace SFS; + +AppFile::AppFile(std::string&& fileId, + std::string&& url, + uint64_t sizeInBytes, + std::unordered_map<HashType, std::string>&& hashes, + std::string&& fileMoniker) + : File(std::move(fileId), std::move(url), sizeInBytes, std::move(hashes)) + , m_fileMoniker(std::move(fileMoniker)) +{ +} + +Result AppFile::Make(std::string fileId, + std::string url, + uint64_t sizeInBytes, + std::unordered_map<HashType, std::string> hashes, + std::vector<Architecture> architectures, + std::vector<std::string> platformApplicabilityForPackage, + std::string fileMoniker, + std::unique_ptr<AppFile>& out) noexcept +try +{ + out.reset(); + + std::unique_ptr<ApplicabilityDetails> details; + RETURN_IF_FAILED( + ApplicabilityDetails::Make(std::move(architectures), std::move(platformApplicabilityForPackage), details)); + + std::unique_ptr<AppFile> tmp( + new AppFile(std::move(fileId), std::move(url), sizeInBytes, std::move(hashes), std::move(fileMoniker))); + tmp->m_applicabilityDetails = std::move(details); + + out = std::move(tmp); + + return Result::Success; +} +SFS_CATCH_RETURN() + +AppFile::AppFile(AppFile&& other) noexcept : File(std::move(other)) +{ + m_applicabilityDetails = std::move(other.m_applicabilityDetails); + m_fileMoniker = std::move(other.m_fileMoniker); +} + +const ApplicabilityDetails& AppFile::GetApplicabilityDetails() const noexcept +{ + return *m_applicabilityDetails; +} + +const std::string& AppFile::GetFileMoniker() const noexcept +{ + return m_fileMoniker; +} diff --git a/src/SfsClient/sfs-client/client/src/ApplicabilityDetails.cpp b/src/SfsClient/sfs-client/client/src/ApplicabilityDetails.cpp @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "ApplicabilityDetails.h" + +#include "details/ErrorHandling.h" + +using namespace SFS; + +Result ApplicabilityDetails::Make(std::vector<Architecture> architectures, + std::vector<std::string> platformApplicabilityForPackage, + std::unique_ptr<ApplicabilityDetails>& out) noexcept +try +{ + out.reset(); + + std::unique_ptr<ApplicabilityDetails> tmp(new ApplicabilityDetails()); + tmp->m_architectures = std::move(architectures); + tmp->m_platformApplicabilityForPackage = std::move(platformApplicabilityForPackage); + + out = std::move(tmp); + + return Result::Success; +} +SFS_CATCH_RETURN() + +const std::vector<Architecture>& ApplicabilityDetails::GetArchitectures() const noexcept +{ + return m_architectures; +} + +const std::vector<std::string>& ApplicabilityDetails::GetPlatformApplicabilityForPackage() const noexcept +{ + return m_platformApplicabilityForPackage; +} diff --git a/src/SfsClient/sfs-client/client/src/Content.cpp b/src/SfsClient/sfs-client/client/src/Content.cpp @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "Content.h" + +#include "details/ErrorHandling.h" + +using namespace SFS; + +Result Content::Make(std::string contentNameSpace, + std::string contentName, + std::string contentVersion, + const std::vector<File>& files, + std::unique_ptr<Content>& out) noexcept +try +{ + out.reset(); + + std::unique_ptr<Content> tmp(new Content()); + RETURN_IF_FAILED(ContentId::Make(std::move(contentNameSpace), + std::move(contentName), + std::move(contentVersion), + tmp->m_contentId)); + + for (const auto& file : files) + { + std::unique_ptr<File> clone; + RETURN_IF_FAILED(file.Clone(clone)); + tmp->m_files.push_back(std::move(*clone)); + } + + out = std::move(tmp); + + return Result::Success; +} +SFS_CATCH_RETURN() + +Result Content::Make(std::string contentNameSpace, + std::string contentName, + std::string contentVersion, + std::vector<File>&& files, + std::unique_ptr<Content>& out) noexcept +try +{ + out.reset(); + + std::unique_ptr<Content> tmp(new Content()); + RETURN_IF_FAILED(ContentId::Make(std::move(contentNameSpace), + std::move(contentName), + std::move(contentVersion), + tmp->m_contentId)); + tmp->m_files = std::move(files); + + out = std::move(tmp); + + return Result::Success; +} +SFS_CATCH_RETURN() + +Result Content::Make(std::unique_ptr<ContentId>&& contentId, + std::vector<File>&& files, + std::unique_ptr<Content>& out) noexcept +try +{ + out.reset(); + + std::unique_ptr<Content> tmp(new Content()); + tmp->m_contentId = std::move(contentId); + tmp->m_files = std::move(files); + + out = std::move(tmp); + + return Result::Success; +} +SFS_CATCH_RETURN() + +Content::Content(Content&& other) noexcept +{ + m_contentId = std::move(other.m_contentId); + m_files = std::move(other.m_files); +} + +const ContentId& Content::GetContentId() const noexcept +{ + return *m_contentId; +} + +const std::vector<File>& Content::GetFiles() const noexcept +{ + return m_files; +} diff --git a/src/SfsClient/sfs-client/client/src/ContentId.cpp b/src/SfsClient/sfs-client/client/src/ContentId.cpp @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "ContentId.h" + +#include "details/ErrorHandling.h" + +using namespace SFS; + +Result ContentId::Make(std::string nameSpace, + std::string name, + std::string version, + std::unique_ptr<ContentId>& out) noexcept +try +{ + out.reset(); + + std::unique_ptr<ContentId> tmp(new ContentId()); + tmp->m_nameSpace = std::move(nameSpace); + tmp->m_name = std::move(name); + tmp->m_version = std::move(version); + + out = std::move(tmp); + + return Result::Success; +} +SFS_CATCH_RETURN() + +ContentId::ContentId(ContentId&& other) noexcept +{ + m_nameSpace = std::move(other.m_nameSpace); + m_name = std::move(other.m_name); + m_version = std::move(other.m_version); +} + +const std::string& ContentId::GetNameSpace() const noexcept +{ + return m_nameSpace; +} + +const std::string& ContentId::GetName() const noexcept +{ + return m_name; +} + +const std::string& ContentId::GetVersion() const noexcept +{ + return m_version; +} diff --git a/src/SfsClient/sfs-client/client/src/File.cpp b/src/SfsClient/sfs-client/client/src/File.cpp @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "File.h" + +#include "details/ErrorHandling.h" + +using namespace SFS; + +File::File(std::string&& fileId, + std::string&& url, + uint64_t sizeInBytes, + std::unordered_map<HashType, std::string>&& hashes) + : m_fileId(std::move(fileId)) + , m_url(std::move(url)) + , m_sizeInBytes(sizeInBytes) + , m_hashes(std::move(hashes)) +{ +} + +Result File::Make(std::string fileId, + std::string url, + uint64_t sizeInBytes, + std::unordered_map<HashType, std::string> hashes, + std::unique_ptr<File>& out) noexcept +try +{ + out.reset(); + + std::unique_ptr<File> tmp(new File(std::move(fileId), std::move(url), sizeInBytes, std::move(hashes))); + out = std::move(tmp); + + return Result::Success; +} +SFS_CATCH_RETURN() + +Result File::Clone(std::unique_ptr<File>& out) const noexcept +{ + return Make(m_fileId, m_url, m_sizeInBytes, m_hashes, out); +} + +File::File(File&& other) noexcept +{ + m_fileId = std::move(other.m_fileId); + m_url = std::move(other.m_url); + m_sizeInBytes = other.m_sizeInBytes; + m_hashes = std::move(other.m_hashes); +} + +const std::string& File::GetFileId() const noexcept +{ + return m_fileId; +} + +const std::string& File::GetUrl() const noexcept +{ + return m_url; +} + +uint64_t File::GetSizeInBytes() const noexcept +{ + return m_sizeInBytes; +} + +const std::unordered_map<HashType, std::string>& File::GetHashes() const noexcept +{ + return m_hashes; +} diff --git a/src/SfsClient/sfs-client/client/src/Logging.cpp b/src/SfsClient/sfs-client/client/src/Logging.cpp @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "Logging.h" + +using SFS::LogSeverity; + +std::string_view SFS::ToString(LogSeverity severity) noexcept +{ + switch (severity) + { + case LogSeverity::Info: + return "Info"; + case LogSeverity::Warning: + return "Warning"; + case LogSeverity::Error: + return "Error"; + case LogSeverity::Verbose: + return "Verbose"; + } + return ""; +} diff --git a/src/SfsClient/sfs-client/client/src/Result.cpp b/src/SfsClient/sfs-client/client/src/Result.cpp @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "Result.h" + +#include <ostream> + +using namespace SFS; + +Result::Result(Code code) noexcept : m_code(code) +{ +} + +Result::Result(Code code, std::string message) noexcept : Result(code) +{ + try + { + m_message = std::move(message); + } + catch (...) + { + // Ignore exception, leave message empty + } +} + +Result::Code Result::GetCode() const noexcept +{ + return m_code; +} + +const std::string& Result::GetMsg() const noexcept +{ + return m_message; +} + +bool Result::IsSuccess() const noexcept +{ + return GetCode() == Success; +} + +bool Result::IsFailure() const noexcept +{ + return !IsSuccess(); +} + +Result::operator bool() const noexcept +{ + return IsSuccess(); +} + +bool Result::operator==(Code resultCode) const noexcept +{ + return GetCode() == resultCode; +} + +bool Result::operator!=(Code resultCode) const noexcept +{ + return GetCode() != resultCode; +} + +std::string_view SFS::ToString(Result::Code code) noexcept +{ + switch (code) + { + case Result::Success: + return "Success"; + + // + // Failure codes + // + + // Generic errors + case Result::InvalidArg: + return "InvalidArg"; + case Result::NotImpl: + return "NotImpl"; + case Result::NotSet: + return "NotSet"; + case Result::OutOfMemory: + return "OutOfMemory"; + case Result::Unexpected: + return "Unexpected"; + + // Connection errors + case Result::ConnectionSetupFailed: + return "ConnectionSetupFailed"; + case Result::ConnectionUnexpectedError: + return "ConnectionUnexpectedError"; + case Result::ConnectionUrlSetupFailed: + return "ConnectionUrlSetupFailed"; + + // Http Errors + case Result::HttpTimeout: + return "HttpTimeout"; + case Result::HttpUnexpected: + return "HttpUnexpected"; + case Result::HttpBadRequest: + return "HttpBadRequest"; + case Result::HttpNotFound: + return "HttpNotFound"; + case Result::HttpMethodNotAllowed: + return "HttpMethodNotAllowed"; + case Result::HttpTooManyRequests: + return "HttpTooManyRequests"; + case Result::HttpServiceNotAvailable: + return "HttpServiceNotAvailable"; + + // Service errors + case Result::ServiceInvalidResponse: + return "ServiceInvalidResponse"; + case Result::ServiceUnexpectedContentType: + return "ServiceUnexpectedContentType"; + } + return ""; +} + +std::ostream& operator<<(std::ostream& os, const Result& result) +{ + os << ToString(result.GetCode()); + return os; +} + +std::ostream& operator<<(std::ostream& os, const Result::Code& code) +{ + os << ToString(code); + return os; +} diff --git a/src/SfsClient/sfs-client/client/src/SFSClient.cpp b/src/SfsClient/sfs-client/client/src/SFSClient.cpp @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "SFSClient.h" + +#include "details/ErrorHandling.h" +#include "details/ReportingHandler.h" +#include "details/SFSClientImpl.h" +#include "details/connection/CurlConnectionManager.h" + +using namespace SFS; +using namespace SFS::details; + +// Defining the constructor and destructor here allows us to use a unique_ptr to SFSClientImpl in the header file +SFSClient::SFSClient() noexcept = default; +SFSClient::~SFSClient() noexcept = default; + +Result SFSClient::Make(ClientConfig config, std::unique_ptr<SFSClient>& out) noexcept +try +{ + out.reset(); + std::unique_ptr<SFSClient> tmp(new SFSClient()); + tmp->m_impl = std::make_unique<details::SFSClientImpl<CurlConnectionManager>>(std::move(config)); + out = std::move(tmp); + + LOG_INFO(out->m_impl->GetReportingHandler(), "SFSClient instance created successfully. Version: %s", GetVersion()); + + return Result::Success; +} +SFS_CATCH_RETURN() + +Result SFSClient::GetLatestDownloadInfo(const RequestParams& requestParams, + std::vector<Content>& contents) const noexcept +try +{ + contents = m_impl->GetLatestDownloadInfo(requestParams); + return Result::Success; +} +SFS_CATCH_RETURN() + +Result SFSClient::GetLatestAppDownloadInfo(const RequestParams& requestParams, + std::vector<AppContent>& contents) const noexcept +try +{ + contents = m_impl->GetLatestAppDownloadInfo(requestParams); + return Result::Success; +} +SFS_CATCH_RETURN() + +const char* SFSClient::GetVersion() noexcept +{ +#ifdef SFS_GIT_INFO + return SFS_VERSION " (" SFS_GIT_INFO ")"; +#else + return SFS_VERSION; +#endif +} diff --git a/src/SfsClient/sfs-client/client/src/details/ContentUtil.cpp b/src/SfsClient/sfs-client/client/src/details/ContentUtil.cpp @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "ContentUtil.h" + +#include <algorithm> + +using namespace SFS; +using namespace SFS::details; + +bool contentutil::operator==(const ContentId& lhs, const ContentId& rhs) +{ + // String characters can be UTF-8 encoded, so we need to compare them in a case-sensitive manner. + return lhs.GetNameSpace() == rhs.GetNameSpace() && lhs.GetName() == rhs.GetName() && + lhs.GetVersion() == rhs.GetVersion(); +} + +bool contentutil::operator!=(const ContentId& lhs, const ContentId& rhs) +{ + return !(lhs == rhs); +} + +bool contentutil::operator==(const File& lhs, const File& rhs) +{ + // String characters can be UTF-8 encoded, so we need to compare them in a case-sensitive manner. + return lhs.GetFileId() == rhs.GetFileId() && lhs.GetUrl() == rhs.GetUrl() && + lhs.GetSizeInBytes() == rhs.GetSizeInBytes() && lhs.GetHashes() == rhs.GetHashes(); +} + +bool contentutil::operator!=(const File& lhs, const File& rhs) +{ + return !(lhs == rhs); +} + +bool contentutil::operator==(const ApplicabilityDetails& lhs, const ApplicabilityDetails& rhs) +{ + // String characters can be UTF-8 encoded, so we need to compare them in a case-sensitive manner. + return lhs.GetArchitectures() == rhs.GetArchitectures() && + lhs.GetPlatformApplicabilityForPackage() == rhs.GetPlatformApplicabilityForPackage(); +} + +bool contentutil::operator!=(const ApplicabilityDetails& lhs, const ApplicabilityDetails& rhs) +{ + return !(lhs == rhs); +} + +bool contentutil::operator==(const AppFile& lhs, const AppFile& rhs) +{ + // String characters can be UTF-8 encoded, so we need to compare them in a case-sensitive manner. + return lhs.GetFileId() == rhs.GetFileId() && lhs.GetUrl() == rhs.GetUrl() && + lhs.GetSizeInBytes() == rhs.GetSizeInBytes() && lhs.GetHashes() == rhs.GetHashes() && + lhs.GetApplicabilityDetails() == rhs.GetApplicabilityDetails() && + lhs.GetFileMoniker() == rhs.GetFileMoniker(); +} + +bool contentutil::operator!=(const AppFile& lhs, const AppFile& rhs) +{ + return !(lhs == rhs); +} + +bool contentutil::operator==(const Content& lhs, const Content& rhs) +{ + return lhs.GetContentId() == rhs.GetContentId() && + (std::is_permutation(lhs.GetFiles().begin(), + lhs.GetFiles().end(), + rhs.GetFiles().begin(), + rhs.GetFiles().end(), + [](const File& flhs, const File& frhs) { return flhs == frhs; })); +} + +bool contentutil::operator!=(const Content& lhs, const Content& rhs) +{ + return !(lhs == rhs); +} + +bool contentutil::operator==(const AppPrerequisiteContent& lhs, const AppPrerequisiteContent& rhs) +{ + auto areFilesEqual = [&lhs, &rhs]() { + return std::is_permutation(lhs.GetFiles().begin(), + lhs.GetFiles().end(), + rhs.GetFiles().begin(), + rhs.GetFiles().end(), + [](const AppFile& flhs, const AppFile& frhs) { return flhs == frhs; }); + }; + return lhs.GetContentId() == rhs.GetContentId() && areFilesEqual(); +} + +bool contentutil::operator!=(const AppPrerequisiteContent& lhs, const AppPrerequisiteContent& rhs) +{ + return !(lhs == rhs); +} + +bool contentutil::operator==(const AppContent& lhs, const AppContent& rhs) +{ + auto arePrerequisitesEqual = [&lhs, &rhs]() { + return std::equal( + lhs.GetPrerequisites().begin(), + lhs.GetPrerequisites().end(), + rhs.GetPrerequisites().begin(), + rhs.GetPrerequisites().end(), + [](const AppPrerequisiteContent& clhs, const AppPrerequisiteContent& crhs) { return clhs == crhs; }); + }; + auto areFilesEqual = [&lhs, &rhs]() { + return std::is_permutation(lhs.GetFiles().begin(), + lhs.GetFiles().end(), + rhs.GetFiles().begin(), + rhs.GetFiles().end(), + [](const AppFile& flhs, const AppFile& frhs) { return flhs == frhs; }); + }; + return lhs.GetContentId() == rhs.GetContentId() && lhs.GetUpdateId() == rhs.GetUpdateId() && + arePrerequisitesEqual() && areFilesEqual(); +} + +bool contentutil::operator!=(const AppContent& lhs, const AppContent& rhs) +{ + return !(lhs == rhs); +} diff --git a/src/SfsClient/sfs-client/client/src/details/ContentUtil.h b/src/SfsClient/sfs-client/client/src/details/ContentUtil.h @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "AppContent.h" +#include "Content.h" + +namespace SFS::details +{ +class ReportingHandler; + +namespace contentutil +{ +// +// Comparison operators +// + +/// @brief Compares two ContentId objects for equality. The values of members are strictly compared. +bool operator==(const ContentId& lhs, const ContentId& rhs); + +/// @brief Compares two ContentId objects for inequality. The values of members are strictly compared. +bool operator!=(const ContentId& lhs, const ContentId& rhs); + +/// @brief Compares two File objects for equality. The values of members are strictly compared. +bool operator==(const File& lhs, const File& rhs); + +/// @brief Compares two File objects for inequality. The values of members are strictly compared. +bool operator!=(const File& lhs, const File& rhs); + +/// @brief Compares two ApplicabilityDetails objects for equality. The values of members are strictly compared. +bool operator==(const ApplicabilityDetails& lhs, const ApplicabilityDetails& rhs); + +/// @brief Compares two ApplicabilityDetails objects for inequality. The values of members are strictly compared. +bool operator!=(const ApplicabilityDetails& lhs, const ApplicabilityDetails& rhs); + +/// @brief Compares two AppFile objects for equality. The values of members are strictly compared. +bool operator==(const AppFile& lhs, const AppFile& rhs); + +/// @brief Compares two AppFile objects for inequality. The values of members are strictly compared. +bool operator!=(const AppFile& lhs, const AppFile& rhs); + +/// @brief Compares two Content objects for equality. The values of members are strictly compared. +bool operator==(const Content& lhs, const Content& rhs); + +/// @brief Compares two Content objects for inequality. The values of members are strictly compared. +bool operator!=(const Content& lhs, const Content& rhs); + +/// @brief Compares two AppPrerequisiteContent objects for equality. The values of members are strictly compared. +bool operator==(const AppPrerequisiteContent& lhs, const AppPrerequisiteContent& rhs); + +/// @brief Compares two AppPrerequisiteContent objects for inequality. The values of members are strictly compared. +bool operator!=(const AppPrerequisiteContent& lhs, const AppPrerequisiteContent& rhs); + +/// @brief Compares two AppContent objects for equality. The values of members are strictly compared. +bool operator==(const AppContent& lhs, const AppContent& rhs); + +/// @brief Compares two AppContent objects for inequality. The values of members are strictly compared. +bool operator!=(const AppContent& lhs, const AppContent& rhs); +} // namespace contentutil +} // namespace SFS::details diff --git a/src/SfsClient/sfs-client/client/src/details/CorrelationVector.cpp b/src/SfsClient/sfs-client/client/src/details/CorrelationVector.cpp @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "CorrelationVector.h" +#include "ErrorHandling.h" + +#include <correlation_vector/correlation_vector.h> + +#include <stdexcept> + +using namespace SFS::details; +using namespace microsoft; + +CorrelationVector::CorrelationVector() : m_cv(std::make_unique<correlation_vector>()) +{ +} + +CorrelationVector::CorrelationVector(const std::string& cv, const ReportingHandler& handler) +{ + THROW_CODE_IF_LOG(InvalidArg, cv.empty(), handler, "cv must not be empty"); + + try + { + m_cv = std::make_unique<correlation_vector>(correlation_vector::extend(cv)); + } + catch (std::invalid_argument& e) + { + THROW_LOG(Result(Result::InvalidArg, "baseCV is not a valid correlation vector: " + std::string(e.what())), + handler); + } +} + +CorrelationVector::~CorrelationVector() = default; +CorrelationVector::CorrelationVector(CorrelationVector&&) = default; +CorrelationVector& CorrelationVector::operator=(CorrelationVector&&) = default; + +std::string CorrelationVector::IncrementAndGet() +{ + // Only increment after it's used at least once + if (m_isFirstUse) + { + m_isFirstUse = false; + } + else + { + Increment(); + } + + return m_cv->to_string(); +} + +void CorrelationVector::Increment() +{ + m_cv->increment(); +} diff --git a/src/SfsClient/sfs-client/client/src/details/CorrelationVector.h b/src/SfsClient/sfs-client/client/src/details/CorrelationVector.h @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include <memory> +#include <string> + +namespace microsoft +{ +class correlation_vector; +} + +namespace SFS::details +{ +class ReportingHandler; + +class CorrelationVector +{ + public: + CorrelationVector(); + CorrelationVector(const std::string& cv, const ReportingHandler& handler); + + ~CorrelationVector(); + + CorrelationVector(CorrelationVector&&); + CorrelationVector& operator=(CorrelationVector&&); + + /** + * @brief Returns the current correlation vector and increments the internal state + */ + std::string IncrementAndGet(); + + private: + void Increment(); + + std::unique_ptr<microsoft::correlation_vector> m_cv; + bool m_isFirstUse = true; +}; +} // namespace SFS::details diff --git a/src/SfsClient/sfs-client/client/src/details/Env.cpp b/src/SfsClient/sfs-client/client/src/details/Env.cpp @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "Env.h" + +#include "ErrorHandling.h" + +#include <cstdlib> + +using namespace SFS::details; +using SFS::details::env::ScopedEnv; + +std::optional<std::string> env::GetEnv(const std::string& varName) +{ + if (varName.empty()) + { + return std::nullopt; + } +#ifdef _WIN32 + size_t len; + char* buf; + if (_dupenv_s(&buf, &len, varName.c_str()) == 0 && buf != nullptr) + { + std::string result{buf}; + free(buf); + return result; + } +#else + if (const char* envValue = std::getenv(varName.c_str())) + { + return std::string(envValue); + } +#endif + // Return std::nullopt if the environment variable is not set or in case of failure + return std::nullopt; +} + +bool env::SetEnv(const std::string& varName, const std::string& value) +{ + if (varName.empty() || value.empty()) + { + return false; + } +#ifdef _WIN32 + return _putenv_s(varName.c_str(), value.c_str()) == 0; +#else + return setenv(varName.c_str(), value.c_str(), 1 /*overwrite*/) == 0; +#endif +} + +bool env::UnsetEnv(const std::string& varName) +{ + if (varName.empty()) + { + return false; + } +#ifdef _WIN32 + // On Windows, setting the value to an empty string is equivalent to unsetting the variable + return _putenv_s(varName.c_str(), "") == 0; +#else + return unsetenv(varName.c_str()) == 0; +#endif +} + +ScopedEnv::ScopedEnv(std::string varName, const std::string& value) : m_varName(std::move(varName)) +{ + m_oldValue = GetEnv(m_varName); + if (!SetEnv(m_varName, value)) + { + throw SFSException(Result::Unexpected, "Failed to set environment variable"); + } +} + +ScopedEnv::~ScopedEnv() +{ + if (m_oldValue) + { + SetEnv(m_varName, *m_oldValue); + } + else + { + UnsetEnv(m_varName); + } +} diff --git a/src/SfsClient/sfs-client/client/src/details/Env.h b/src/SfsClient/sfs-client/client/src/details/Env.h @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include <optional> +#include <string> + +namespace SFS::details::env +{ +/** + * @brief Get the value of an environment variable. + * @details std::nullopt is returned if the environment variable is not set or in case of failure. + * The returned string is encoded in ASCII in Windows, not UTF-8. + */ +std::optional<std::string> GetEnv(const std::string& varName); + +/** + * @brief Set the value of an environment variable. + * @return false in case of failure. + */ +bool SetEnv(const std::string& varName, const std::string& value); + +/** + * @brief Unset the value of an environment variable. + * @return false in case of failure. If the environment variable didn't exist, it still returns true. + */ +bool UnsetEnv(const std::string& varName); + +class ScopedEnv +{ + public: + ScopedEnv(std::string varName, const std::string& value); + ~ScopedEnv(); + + private: + std::string m_varName; + std::optional<std::string> m_oldValue; +}; +} // namespace SFS::details::env diff --git a/src/SfsClient/sfs-client/client/src/details/ErrorHandling.cpp b/src/SfsClient/sfs-client/client/src/details/ErrorHandling.cpp @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "ErrorHandling.h" + +#include "ReportingHandler.h" + +using namespace SFS::details; + +void SFS::details::LogFailedResult(const SFS::Result& result, + const ReportingHandler& handler, + const char* file, + unsigned line) +{ + if (result.IsFailure()) + { + LOG_ERROR(handler, + "FAILED [%s] %s%s(%s:%u)", + std::string(ToString(result.GetCode())).c_str(), + result.GetMsg().c_str(), + result.GetMsg().empty() ? "" : " ", + file, + line); + } +} + +void SFS::details::LogIfFailed(const Result& result, const ReportingHandler& handler, const char* file, unsigned line) +{ + if (result.IsFailure()) + { + LogFailedResult(result, handler, file, line); + } +} + +void SFS::details::ThrowLog(Result result, const ReportingHandler& handler, const char* file, unsigned line) +{ + assert(result.IsFailure()); + LogFailedResult(result, handler, file, line); + throw SFSException(std::move(result)); +} + +void SFS::details::ThrowIfFailedLog(Result result, const ReportingHandler& handler, const char* file, unsigned line) +{ + if (result.IsFailure()) + { + LogFailedResult(result, handler, file, line); + throw SFSException(std::move(result)); + } +} + +void SFS::details::ThrowCodeIf(Result::Code code, bool condition, std::string message) +{ + if (condition) + { + Result result(code, std::move(message)); + assert(result.IsFailure()); + throw SFSException(std::move(result)); + } +} + +void SFS::details::ThrowCodeIfLog(Result::Code code, + bool condition, + const ReportingHandler& handler, + const char* file, + unsigned line, + std::string message) +{ + if (condition) + { + Result result(code, std::move(message)); + assert(result.IsFailure()); + LogFailedResult(result, handler, file, line); + throw SFSException(std::move(result)); + } +} diff --git a/src/SfsClient/sfs-client/client/src/details/ErrorHandling.h b/src/SfsClient/sfs-client/client/src/details/ErrorHandling.h @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "Result.h" +#include "SFSException.h" + +#include <cassert> + +#define SFS_CATCH_RETURN() \ + catch (const std::bad_alloc&) \ + { \ + return Result::OutOfMemory; \ + } \ + catch (const SFS::details::SFSException& e) \ + { \ + return e.GetResult(); \ + } \ + catch (const std::exception&) \ + { \ + return Result::Unexpected; \ + } \ + catch (...) \ + { \ + return Result::Unexpected; \ + } + +#define SFS_CATCH_LOG_RETHROW(handler) \ + catch (const SFS::details::SFSException& e) \ + { \ + SFS::details::LogFailedResult(e.GetResult(), handler, __FILE__, __LINE__); \ + throw; \ + } + +#define RETURN_IF_FAILED(result) \ + do \ + { \ + auto __result = (result); /* Assigning to a variable ensures a code block gets called only once */ \ + if (__result.IsFailure()) \ + { \ + return __result; \ + } \ + } while ((void)0, 0) + +#define RETURN_IF_FAILED_LOG(result, handler) \ + do \ + { \ + auto __result = (result); /* Assigning to a variable ensures a code block gets called only once */ \ + if (__result.IsFailure()) \ + { \ + SFS::details::LogFailedResult(__result, handler, __FILE__, __LINE__); \ + return __result; \ + } \ + } while ((void)0, 0) + +#define LOG_IF_FAILED(result, handler) LogIfFailed(result, handler, __FILE__, __LINE__) + +#define THROW_LOG(result, handler) ThrowLog(result, handler, __FILE__, __LINE__) + +#define THROW_IF_FAILED_LOG(result, handler) ThrowIfFailedLog(result, handler, __FILE__, __LINE__) + +#define THROW_CODE_IF(code, condition, ...) ThrowCodeIf(SFS::Result::code, condition, ##__VA_ARGS__) + +#define THROW_CODE_IF_LOG(code, condition, handler, ...) \ + ThrowCodeIfLog(SFS::Result::code, condition, handler, __FILE__, __LINE__, ##__VA_ARGS__) + +#define THROW_CODE_IF_NOT_LOG(code, condition, handler, ...) \ + ThrowCodeIfLog(SFS::Result::code, !(condition), handler, __FILE__, __LINE__, ##__VA_ARGS__) + +namespace SFS::details +{ +class ReportingHandler; + +void LogFailedResult(const Result& result, const ReportingHandler& handler, const char* file, unsigned line); +void LogIfFailed(const Result& result, const ReportingHandler& handler, const char* file, unsigned line); + +void ThrowLog(Result result, const ReportingHandler& handler, const char* file, unsigned line); +void ThrowIfFailedLog(Result result, const ReportingHandler& handler, const char* file, unsigned line); +void ThrowCodeIf(Result::Code code, bool condition, std::string message = {}); +void ThrowCodeIfLog(Result::Code code, + bool condition, + const ReportingHandler& handler, + const char* file, + unsigned line, + std::string message = {}); +} // namespace SFS::details diff --git a/src/SfsClient/sfs-client/client/src/details/OSInfo.cpp b/src/SfsClient/sfs-client/client/src/details/OSInfo.cpp @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "OSInfo.h" + +#ifdef _WIN32 + +#include <windows.h> + +#include <sysinfoapi.h> + +#elif __linux__ + +#include <sys/utsname.h> + +#endif + +using namespace SFS::details; + +namespace +{ +#ifdef _WIN32 +std::string GetPlatform() +{ + return "Windows"; +} + +std::string GetOSMachineInfo() +{ + SYSTEM_INFO systemInfo; + GetNativeSystemInfo(&systemInfo); + + switch (systemInfo.wProcessorArchitecture) + { + case PROCESSOR_ARCHITECTURE_AMD64: + return "x64"; + case PROCESSOR_ARCHITECTURE_ARM: + return "ARM"; + case PROCESSOR_ARCHITECTURE_ARM64: + return "ARM64"; + case PROCESSOR_ARCHITECTURE_INTEL: + return "x86"; + default: + return "Unknown"; + } +} + +#elif __linux__ + +std::string GetPlatform() +{ + // Return kernel name, usually "Linux" + utsname buf; + if (uname(&buf) >= 0) + { + return buf.sysname; + } + return "Unknown"; +} + +std::string GetOSMachineInfo() +{ + utsname buf; + if (uname(&buf) >= 0) + { + return buf.machine; + } + return "Unknown"; +} + +#else +#error "Unsupported platform" +#endif +} // namespace + +std::string osinfo::GetPlatform() +{ + return ::GetPlatform(); +} + +std::string osinfo::GetOSMachineInfo() +{ + return ::GetOSMachineInfo(); +} diff --git a/src/SfsClient/sfs-client/client/src/details/OSInfo.h b/src/SfsClient/sfs-client/client/src/details/OSInfo.h @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include <string> + +namespace SFS::details::osinfo +{ +std::string GetPlatform(); +std::string GetOSMachineInfo(); +} // namespace SFS::details::osinfo diff --git a/src/SfsClient/sfs-client/client/src/details/ReportingHandler.cpp b/src/SfsClient/sfs-client/client/src/details/ReportingHandler.cpp @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "ReportingHandler.h" + +#include <chrono> + +using namespace SFS; +using namespace SFS::details; + +void ReportingHandler::SetLoggingCallback(LoggingCallbackFn&& callback) +{ + std::lock_guard guard(m_loggingCallbackFnMutex); + m_loggingCallbackFn = std::move(callback); +} + +void ReportingHandler::CallLoggingCallback(LogSeverity severity, + const char* message, + const char* file, + unsigned line, + const char* function) const +{ + std::lock_guard guard(m_loggingCallbackFnMutex); + if (m_loggingCallbackFn) + { + m_loggingCallbackFn({severity, message, file, line, function, std::chrono::system_clock::now()}); + } +} diff --git a/src/SfsClient/sfs-client/client/src/details/ReportingHandler.h b/src/SfsClient/sfs-client/client/src/details/ReportingHandler.h @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "Logging.h" + +#include <mutex> +#include <stdio.h> + +#define MAX_LOG_MESSAGE_SIZE 1024 + +#define LOG_INFO(handler, format, ...) \ + handler.LogWithSeverity(SFS::LogSeverity::Info, __FILE__, __LINE__, __FUNCTION__, format, ##__VA_ARGS__) +#define LOG_WARNING(handler, format, ...) \ + handler.LogWithSeverity(SFS::LogSeverity::Warning, __FILE__, __LINE__, __FUNCTION__, format, ##__VA_ARGS__) +#define LOG_ERROR(handler, format, ...) \ + handler.LogWithSeverity(SFS::LogSeverity::Error, __FILE__, __LINE__, __FUNCTION__, format, ##__VA_ARGS__) +#define LOG_VERBOSE(handler, format, ...) \ + handler.LogWithSeverity(SFS::LogSeverity::Verbose, __FILE__, __LINE__, __FUNCTION__, format, ##__VA_ARGS__) + +namespace SFS::details +{ +/** + * @brief This class enables thread-safe access to the externally set logging callback function. + * @details Each SFSClient instance will have one ReportingHandler instance, and access to the logging callback function + * is controlled by a mutex that makes sure that only one thread can access the logging callback function at a time. + */ +class ReportingHandler +{ + public: + ReportingHandler() = default; + + ReportingHandler(const ReportingHandler&) = delete; + ReportingHandler& operator=(const ReportingHandler&) = delete; + + /** + * @brief Sets the logging callback function. + * @details This function is thread-safe. + * @param callback The logging callback function. To reset, pass a nullptr. + */ + void SetLoggingCallback(LoggingCallbackFn&& callback); + + /** + * @brief Logs a message with the given severity. + * @details Prefer calling it with macros LOG_INFO, LOG_WARNING, LOG_ERROR, LOG_VERBOSE so file, line and function + * are automatically populated and the message can be formatted. + */ + template <typename... Args> + void LogWithSeverity(LogSeverity severity, + const char* file, + unsigned line, + const char* function, + const char* format, + const Args&... args) const + { + constexpr std::size_t n = sizeof...(Args); + if constexpr (n == 0) + { + CallLoggingCallback(severity, format, file, line, function); + } + else + { + char message[MAX_LOG_MESSAGE_SIZE]; + snprintf(message, MAX_LOG_MESSAGE_SIZE, format, args...); + CallLoggingCallback(severity, message, file, line, function); + } + } + + private: + void CallLoggingCallback(LogSeverity severity, + const char* message, + const char* file, + unsigned line, + const char* function) const; + + LoggingCallbackFn m_loggingCallbackFn; + mutable std::mutex m_loggingCallbackFnMutex; +}; +} // namespace SFS::details diff --git a/src/SfsClient/sfs-client/client/src/details/SFSClientImpl.cpp b/src/SfsClient/sfs-client/client/src/details/SFSClientImpl.cpp @@ -0,0 +1,415 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "SFSClientImpl.h" + +#include "AppContent.h" +#include "Content.h" +#include "ErrorHandling.h" +#include "Logging.h" +#include "TestOverride.h" +#include "Util.h" +#include "connection/Connection.h" +#include "connection/ConnectionManager.h" +#include "connection/CurlConnectionManager.h" +#include "connection/mock/MockConnectionManager.h" + +#include <nlohmann/json.hpp> + +#include <unordered_set> + +using namespace SFS; +using namespace SFS::details; +using namespace SFS::details::util; +using json = nlohmann::json; + +constexpr const char* c_defaultInstanceId = "default"; +constexpr const char* c_defaultNameSpace = "default"; + +namespace +{ +void ValidateClientConfig(const ClientConfig& config, const ReportingHandler& handler) +{ + THROW_CODE_IF_LOG(InvalidArg, config.accountId.empty(), handler, "ClientConfig::accountId must not be empty"); + + if (config.instanceId) + { + THROW_CODE_IF_LOG(InvalidArg, + config.instanceId->empty(), + handler, + "ClientConfig::instanceId must not be empty"); + } + + if (config.nameSpace) + { + THROW_CODE_IF_LOG(InvalidArg, config.nameSpace->empty(), handler, "ClientConfig::nameSpace must not be empty"); + } +} + +void LogIfTestOverridesAllowed(const ReportingHandler& handler) +{ + if (test::AreTestOverridesAllowed()) + { + LOG_INFO(handler, "Test overrides are allowed"); + } +} + +void ThrowInvalidResponseIfFalse(bool condition, const std::string& message, const ReportingHandler& handler) +{ + THROW_CODE_IF_LOG(ServiceInvalidResponse, !condition, handler, message); +} + +json ParseServerMethodStringToJson(const std::string& data, const std::string& method, const ReportingHandler& handler) +{ + try + { + return json::parse(data); + } + catch (json::parse_error& ex) + { + THROW_LOG( + Result(Result::ServiceInvalidResponse, "(" + method + ") JSON Parsing error: " + std::string(ex.what())), + handler); + return json(); // Unreachable code, but the compiler doesn't know that. + } +} + +VersionEntities ConvertLatestVersionBatchResponseToVersionEntities(const json& data, const ReportingHandler& handler) +{ + // Expected format: + // [ + // { + // "ContentId": { + // "Namespace": <ns>, + // "Name": <name>, + // "Version": <version> + // } + // }, + // ... + // ] + // + + ThrowInvalidResponseIfFalse(data.is_array(), "Response is not a JSON array", handler); + ThrowInvalidResponseIfFalse(data.size() > 0, "Response does not have the expected size", handler); + + VersionEntities entities; + for (const auto& obj : data) + { + entities.push_back(std::move(VersionEntity::FromJson(obj, handler))); + } + + return entities; +} + +bool VerifyVersionResponseMatchesProduct(const ContentIdEntity& contentId, + std::string_view nameSpace, + std::string_view name) +{ + return contentId.nameSpace == nameSpace && contentId.name == name; +} + +void ValidateVersionEntity(const VersionEntity& versionEntity, + const std::string& nameSpace, + const std::string& product, + const ReportingHandler& handler) +{ + THROW_CODE_IF_NOT_LOG(ServiceInvalidResponse, + VerifyVersionResponseMatchesProduct(versionEntity.contentId, nameSpace, product), + handler, + "Response does not match the requested product"); +} + +void ValidateBatchVersionEntity(const VersionEntities& versionEntities, + const std::string& nameSpace, + const std::unordered_set<std::string>& requestedProducts, + const ReportingHandler& handler) +{ + for (const auto& entity : versionEntities) + { + THROW_CODE_IF_LOG(ServiceInvalidResponse, + requestedProducts.count(entity->contentId.name) == 0, + handler, + "Received product [" + entity->contentId.name + + "] which is not one of the requested products"); + THROW_CODE_IF_LOG(ServiceInvalidResponse, + AreNotEqualI(entity->contentId.nameSpace, nameSpace), + handler, + "Received product [" + entity->contentId.name + "] with a namespace [" + + entity->contentId.nameSpace + "] that does not match the requested namespace"); + + LOG_INFO(handler, + "Received a response for product [%s] with version %s", + entity->contentId.name.c_str(), + entity->contentId.version.c_str()); + } +} + +void ValidateRequestParams(const RequestParams& requestParams, const ReportingHandler& handler) +{ + THROW_CODE_IF_LOG(InvalidArg, requestParams.productRequests.empty(), handler, "productRequests cannot be empty"); + + // TODO #78: Add support for multiple product requests + THROW_CODE_IF_LOG(NotImpl, + requestParams.productRequests.size() > 1, + handler, + "There cannot be more than 1 productRequest at the moment"); + + for (const auto& [product, _] : requestParams.productRequests) + { + THROW_CODE_IF_LOG(InvalidArg, product.empty(), handler, "product must not be empty"); + } +} +} // namespace + +template <typename ConnectionManagerT> +SFSClientImpl<ConnectionManagerT>::SFSClientImpl(ClientConfig&& config) +{ + if (config.logCallbackFn) + { + m_reportingHandler.SetLoggingCallback(std::move(*config.logCallbackFn)); + } + + ValidateClientConfig(config, m_reportingHandler); + + m_accountId = std::move(config.accountId); + m_instanceId = + (config.instanceId && !config.instanceId->empty()) ? std::move(*config.instanceId) : c_defaultInstanceId; + m_nameSpace = (config.nameSpace && !config.nameSpace->empty()) ? std::move(*config.nameSpace) : c_defaultNameSpace; + + static_assert(std::is_base_of<ConnectionManager, ConnectionManagerT>::value, + "ConnectionManagerT not derived from ConnectionManager"); + m_connectionManager = std::make_unique<ConnectionManagerT>(m_reportingHandler); + + LogIfTestOverridesAllowed(m_reportingHandler); +} + +template <typename ConnectionManagerT> +std::unique_ptr<VersionEntity> SFSClientImpl<ConnectionManagerT>::GetLatestVersion(const ProductRequest& productRequest, + Connection& connection) const +try +{ + const auto& [product, attributes] = productRequest; + const std::string url{MakeUrlBuilder().GetLatestVersionUrl(product)}; + + LOG_INFO(m_reportingHandler, "Requesting latest version of [%s] from URL [%s]", product.c_str(), url.c_str()); + + const json body = {{"TargetingAttributes", attributes}}; + LOG_VERBOSE(m_reportingHandler, "Request body [%s]", body.dump().c_str()); + + const std::string postResponse{connection.Post(url, body.dump())}; + const json versionResponse = ParseServerMethodStringToJson(postResponse, "GetLatestVersion", m_reportingHandler); + + auto versionEntity = VersionEntity::FromJson(versionResponse, m_reportingHandler); + ValidateVersionEntity(*versionEntity, m_nameSpace, product, m_reportingHandler); + + LOG_INFO(m_reportingHandler, "Received a response with version %s", versionEntity->contentId.version.c_str()); + + return versionEntity; +} +SFS_CATCH_LOG_RETHROW(m_reportingHandler) + +template <typename ConnectionManagerT> +VersionEntities SFSClientImpl<ConnectionManagerT>::GetLatestVersionBatch( + const std::vector<ProductRequest>& productRequests, + Connection& connection) const +try +{ + const std::string url{MakeUrlBuilder().GetLatestVersionBatchUrl()}; + + LOG_INFO(m_reportingHandler, "Requesting latest version of multiple products from URL [%s]", url.c_str()); + + // Creating request body + std::unordered_set<std::string> requestedProducts; + json body = json::array(); + for (const auto& [product, attributes] : productRequests) + { + LOG_INFO(m_reportingHandler, "Product #%zu: [%s]", body.size() + size_t{1}, product.c_str()); + requestedProducts.insert(product); + + body.push_back({{"TargetingAttributes", attributes}, {"Product", product}}); + } + + LOG_VERBOSE(m_reportingHandler, "Request body [%s]", body.dump().c_str()); + + const std::string postResponse{connection.Post(url, body.dump())}; + + const json versionResponse = + ParseServerMethodStringToJson(postResponse, "GetLatestVersionBatch", m_reportingHandler); + + auto entities = ConvertLatestVersionBatchResponseToVersionEntities(versionResponse, m_reportingHandler); + ValidateBatchVersionEntity(entities, m_nameSpace, requestedProducts, m_reportingHandler); + + return entities; +} +SFS_CATCH_LOG_RETHROW(m_reportingHandler) + +template <typename ConnectionManagerT> +std::unique_ptr<VersionEntity> SFSClientImpl<ConnectionManagerT>::GetSpecificVersion(const std::string& product, + const std::string& version, + Connection& connection) const +try +{ + const std::string url{MakeUrlBuilder().GetSpecificVersionUrl(product, version)}; + + LOG_INFO(m_reportingHandler, + "Requesting version [%s] of [%s] from URL [%s]", + version.c_str(), + product.c_str(), + url.c_str()); + + const std::string getResponse{connection.Get(url)}; + + const json versionResponse = ParseServerMethodStringToJson(getResponse, "GetSpecificVersion", m_reportingHandler); + + auto versionEntity = VersionEntity::FromJson(versionResponse, m_reportingHandler); + ValidateVersionEntity(*versionEntity, m_nameSpace, product, m_reportingHandler); + + LOG_INFO(m_reportingHandler, + "Received the expected response with version %s", + versionEntity->contentId.version.c_str()); + + return versionEntity; +} +SFS_CATCH_LOG_RETHROW(m_reportingHandler) + +template <typename ConnectionManagerT> +FileEntities SFSClientImpl<ConnectionManagerT>::GetDownloadInfo(const std::string& product, + const std::string& version, + Connection& connection) const +try +{ + const std::string url{MakeUrlBuilder().GetDownloadInfoUrl(product, version)}; + + LOG_INFO(m_reportingHandler, + "Requesting download info of version [%s] of [%s] from URL [%s]", + version.c_str(), + product.c_str(), + url.c_str()); + + const std::string postResponse{connection.Post(url)}; + + const json downloadInfoResponse = + ParseServerMethodStringToJson(postResponse, "GetDownloadInfo", m_reportingHandler); + + auto files = FileEntity::DownloadInfoResponseToFileEntities(downloadInfoResponse, m_reportingHandler); + + LOG_INFO(m_reportingHandler, "Received a response with %zu files", files.size()); + + return files; +} +SFS_CATCH_LOG_RETHROW(m_reportingHandler) + +template <typename ConnectionManagerT> +std::vector<Content> SFSClientImpl<ConnectionManagerT>::GetLatestDownloadInfo(const RequestParams& requestParams) const +try +{ + ValidateRequestParams(requestParams, m_reportingHandler); + + const auto connection = MakeConnection(ConnectionConfig(requestParams)); + + auto versionEntity = GetLatestVersion(requestParams.productRequests[0], *connection); + auto contentId = VersionEntity::ToContentId(std::move(*versionEntity), m_reportingHandler); + + const auto& product = requestParams.productRequests[0].product; + auto fileEntities = GetDownloadInfo(product, contentId->GetVersion(), *connection); + auto files = GenericFileEntity::FileEntitiesToFileVector(std::move(fileEntities), m_reportingHandler); + + std::unique_ptr<Content> content; + THROW_IF_FAILED_LOG(Content::Make(std::move(contentId), std::move(files), content), m_reportingHandler); + + std::vector<Content> contents; + contents.push_back(std::move(*content)); + + return contents; +} +SFS_CATCH_LOG_RETHROW(m_reportingHandler) + +template <typename ConnectionManagerT> +std::vector<AppContent> SFSClientImpl<ConnectionManagerT>::GetLatestAppDownloadInfo( + const RequestParams& requestParams) const +try +{ + ValidateRequestParams(requestParams, m_reportingHandler); + + // TODO #150: For now apps are only coming from the "storeapps" instanceId and the service has requested + // we double check for it. In the future we should remove this check and allow the user to specify any instanceId + THROW_CODE_IF_LOG(Unexpected, + AreNotEqualI(m_instanceId, "storeapps"), + m_reportingHandler, + "At this moment only the \"storeapps\" instanceId can send app requests"); + + const auto connection = MakeConnection(ConnectionConfig(requestParams)); + + auto versionEntity = GetLatestVersion(requestParams.productRequests[0], *connection); + + auto appVersionEntity = AppVersionEntity::GetAppVersionEntityPtr(versionEntity, m_reportingHandler); + auto contentId = AppVersionEntity::ToContentId(std::move(*appVersionEntity), m_reportingHandler); + + LOG_INFO(m_reportingHandler, "Getting download info for main app content"); + const auto& product = requestParams.productRequests[0].product; + auto fileEntities = GetDownloadInfo(product, contentId->GetVersion(), *connection); + auto files = AppFileEntity::FileEntitiesToAppFileVector(std::move(fileEntities), m_reportingHandler); + + std::vector<AppPrerequisiteContent> prerequisites; + for (auto& prereq : appVersionEntity->prerequisites) + { + LOG_INFO(m_reportingHandler, "Getting download info for prerequisite [%s]", prereq.contentId.name.c_str()); + auto prereqContentId = GenericVersionEntity::ToContentId(std::move(prereq), m_reportingHandler); + + auto prereqFileEntities = + GetDownloadInfo(prereqContentId->GetName(), prereqContentId->GetVersion(), *connection); + auto prereqFiles = + AppFileEntity::FileEntitiesToAppFileVector(std::move(prereqFileEntities), m_reportingHandler); + + std::unique_ptr<AppPrerequisiteContent> prereqContent; + THROW_IF_FAILED_LOG( + AppPrerequisiteContent::Make(std::move(prereqContentId), std::move(prereqFiles), prereqContent), + m_reportingHandler); + + prerequisites.push_back(std::move(*prereqContent)); + } + + std::unique_ptr<AppContent> content; + THROW_IF_FAILED_LOG(AppContent::Make(std::move(contentId), + std::move(appVersionEntity->updateId), + std::move(prerequisites), + std::move(files), + content), + m_reportingHandler); + + std::vector<AppContent> contents; + contents.push_back(std::move(*content)); + + return contents; +} +SFS_CATCH_LOG_RETHROW(m_reportingHandler) + +template <typename ConnectionManagerT> +std::unique_ptr<Connection> SFSClientImpl<ConnectionManagerT>::MakeConnection(const ConnectionConfig& config) const +{ + return m_connectionManager->MakeConnection(config); +} + +template <typename ConnectionManagerT> +void SFSClientImpl<ConnectionManagerT>::SetCustomBaseUrl(std::string customBaseUrl) +{ + m_customBaseUrl = std::move(customBaseUrl); +} + +template <typename ConnectionManagerT> +SFSUrlBuilder SFSClientImpl<ConnectionManagerT>::MakeUrlBuilder() const +{ + if (auto envVar = test::GetTestOverride(test::TestOverride::BaseUrl)) + { + return SFSUrlBuilder(SFSCustomUrl(*envVar), m_instanceId, m_nameSpace, m_reportingHandler); + } + if (m_customBaseUrl) + { + return SFSUrlBuilder(SFSCustomUrl(*m_customBaseUrl), m_instanceId, m_nameSpace, m_reportingHandler); + } + + return SFSUrlBuilder(m_accountId, m_instanceId, m_nameSpace, m_reportingHandler); +} + +template class SFS::details::SFSClientImpl<CurlConnectionManager>; +template class SFS::details::SFSClientImpl<MockConnectionManager>; diff --git a/src/SfsClient/sfs-client/client/src/details/SFSClientImpl.h b/src/SfsClient/sfs-client/client/src/details/SFSClientImpl.h @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "SFSClientInterface.h" + +#include "ClientConfig.h" +#include "Content.h" +#include "Logging.h" +#include "Result.h" +#include "SFSUrlBuilder.h" + +#include <memory> +#include <optional> +#include <string> + +namespace SFS::details +{ +template <typename ConnectionManagerT> +class SFSClientImpl : public SFSClientInterface +{ + public: + SFSClientImpl(ClientConfig&& config); + ~SFSClientImpl() override = default; + + // + // Combined API calls for retrieval of metadata & download URLs + // + + /** + * @brief Retrieve combined metadata & download URLs from the latest version of specified products + * @note At the moment only a single product request is supported + * @param requestParams Parameters that define this request + */ + std::vector<Content> GetLatestDownloadInfo(const RequestParams& requestParams) const override; + + /** + * @brief Retrieve combined metadata & download URLs from the latest version of specified apps + * @note At the moment only a single product request is supported + * @param requestParams Parameters that define this request + */ + std::vector<AppContent> GetLatestAppDownloadInfo(const RequestParams& requestParams) const override; + + // + // Individual APIs 1:1 with service endpoints (SFSClientInterface) + // + + /** + * @brief Gets the metadata for the latest available version for the specified product request + * @return Entity that describes the latest version of the product + * @throws SFSException if the request fails + */ + std::unique_ptr<VersionEntity> GetLatestVersion(const ProductRequest& productRequest, + Connection& connection) const override; + + /** + * @brief Gets the metadata for the latest available version for the specified product requests + * @return Vector of entities that describe the latest version of the products + * @throws SFSException if the request fails + */ + VersionEntities GetLatestVersionBatch(const std::vector<ProductRequest>& productRequests, + Connection& connection) const override; + + /** + * @brief Gets the metadata for a specific version of the specified product + * @return Entity that describes the latest version of the product + * @throws SFSException if the request fails + */ + std::unique_ptr<VersionEntity> GetSpecificVersion(const std::string& product, + const std::string& version, + Connection& connection) const override; + + /** + * @brief Gets the files metadata for a specific version of the specified product + * @return Vector of File entities for the specific version of the product + * @throws SFSException if the request fails + */ + FileEntities GetDownloadInfo(const std::string& product, + const std::string& version, + Connection& connection) const override; + + /** + * @brief Returns a new Connection to be used by the SFSClient to make requests + * @param config Configurations for the connection object + */ + std::unique_ptr<Connection> MakeConnection(const ConnectionConfig& config) const override; + + // + // Configuration methods + // + + /** + * @brief Allows one to override the base URL used to make calls to the SFS service + * @details Not exposed to the user. Used for testing purposes only + * @param customBaseUrl The custom base URL to use + */ + void SetCustomBaseUrl(std::string customBaseUrl); + + /** + * @return A SFSUrlBuilder object that can be used to build URLs for the SFS service + */ + SFSUrlBuilder MakeUrlBuilder() const; + + private: + std::string m_accountId; + std::string m_instanceId; + std::string m_nameSpace; + + std::unique_ptr<ConnectionManagerT> m_connectionManager; + + std::optional<std::string> m_customBaseUrl; +}; +} // namespace SFS::details diff --git a/src/SfsClient/sfs-client/client/src/details/SFSClientInterface.h b/src/SfsClient/sfs-client/client/src/details/SFSClientInterface.h @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "Logging.h" +#include "ReportingHandler.h" +#include "RequestParams.h" +#include "entity/FileEntity.h" +#include "entity/VersionEntity.h" + +#include <memory> +#include <string> + +namespace SFS +{ +class AppContent; +class Content; + +namespace details +{ +class Connection; +class ConnectionManager; +struct ConnectionConfig; + +class SFSClientInterface +{ + public: + virtual ~SFSClientInterface() + { + } + + // + // Combined API calls for retrieval of metadata & download URLs + // + + /** + * @brief Retrieve combined metadata & download URLs from the latest version of specified products + * @note At the moment only a single product request is supported + * @param requestParams Parameters that define this request + */ + virtual std::vector<Content> GetLatestDownloadInfo(const RequestParams& requestParams) const = 0; + + /** + * @brief Retrieve combined metadata & download URLs from the latest version of specified apps + * @note At the moment only a single product request is supported + * @param requestParams Parameters that define this request + */ + virtual std::vector<AppContent> GetLatestAppDownloadInfo(const RequestParams& requestParams) const = 0; + + // + // Individual APIs 1:1 with service endpoints + // + + /** + * @brief Gets the metadata for the latest available version for the specified product request + * @return Entity that describes the latest version of the product + * @throws SFSException if the request fails + */ + virtual std::unique_ptr<VersionEntity> GetLatestVersion(const ProductRequest& productRequest, + Connection& connection) const = 0; + + /** + * @brief Gets the metadata for the latest available version for the specified product requests + * @return Vector of entities that describe the latest version of the products + * @throws SFSException if the request fails + */ + virtual VersionEntities GetLatestVersionBatch(const std::vector<ProductRequest>& productRequests, + Connection& connection) const = 0; + + /** + * @brief Gets the metadata for a specific version of the specified product + * @return Entity that describes the latest version of the product + * @throws SFSException if the request fails + */ + virtual std::unique_ptr<VersionEntity> GetSpecificVersion(const std::string& product, + const std::string& version, + Connection& connection) const = 0; + + /** + * @brief Gets the files metadata for a specific version of the specified product + * @return Vector of File entities for the specific version of the product + * @throws SFSException if the request fails + */ + virtual FileEntities GetDownloadInfo(const std::string& product, + const std::string& version, + Connection& connection) const = 0; + + /** + * @brief Returns a new Connection to be used by the SFSClient to make requests + * @param config Configurations for the connection object + */ + virtual std::unique_ptr<Connection> MakeConnection(const ConnectionConfig& config) const = 0; + + const ReportingHandler& GetReportingHandler() const + { + return m_reportingHandler; + } + + protected: + ReportingHandler m_reportingHandler; +}; +} // namespace details +} // namespace SFS diff --git a/src/SfsClient/sfs-client/client/src/details/SFSException.cpp b/src/SfsClient/sfs-client/client/src/details/SFSException.cpp @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "SFSException.h" + +using namespace SFS::details; + +SFSException::SFSException(SFS::Result result) : m_result(std::move(result)) +{ +} + +SFSException::SFSException(SFS::Result::Code code) : m_result(Result(code)) +{ +} + +SFSException::SFSException(SFS::Result::Code code, std::string message) : m_result(Result(code, std::move(message))) +{ +} + +const SFS::Result& SFSException::GetResult() const noexcept +{ + return m_result; +} + +const char* SFSException::what() const noexcept +{ + return m_result.GetMsg().c_str(); +} diff --git a/src/SfsClient/sfs-client/client/src/details/SFSException.h b/src/SfsClient/sfs-client/client/src/details/SFSException.h @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "Result.h" + +#include <exception> +#include <string> + +namespace SFS::details +{ +class SFSException : public std::exception +{ + public: + SFSException() = default; + explicit SFSException(SFS::Result result); + + explicit SFSException(SFS::Result::Code code); + SFSException(SFS::Result::Code code, std::string message); + + const SFS::Result& GetResult() const noexcept; + const char* what() const noexcept override; + + private: + SFS::Result m_result; +}; +} // namespace SFS::details diff --git a/src/SfsClient/sfs-client/client/src/details/SFSUrlBuilder.cpp b/src/SfsClient/sfs-client/client/src/details/SFSUrlBuilder.cpp @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "SFSUrlBuilder.h" + +using namespace SFS::details; + +constexpr const char* c_apiVersion = "v2"; +constexpr const char* c_apiDomain = "api.cdp.microsoft.com"; + +SFSUrlBuilder::SFSUrlBuilder(const std::string& accountId, + std::string instanceId, + std::string nameSpace, + const ReportingHandler& handler) + : UrlBuilder(handler) + , m_instanceId(std::move(instanceId)) + , m_nameSpace(std::move(nameSpace)) +{ + SetScheme(Scheme::Https); + SetHost(accountId + "." + std::string(c_apiDomain)); +} + +SFSUrlBuilder::SFSUrlBuilder(const SFSCustomUrl& customUrl, + std::string instanceId, + std::string nameSpace, + const ReportingHandler& handler) + : UrlBuilder(handler) + , m_instanceId(std::move(instanceId)) + , m_nameSpace(std::move(nameSpace)) +{ + SetUrl(customUrl.url); +} + +std::string SFSUrlBuilder::GetLatestVersionUrl(const std::string& product) +{ + SetVersionsUrlPath(product); + AppendPath("latest"); + SetQuery("action", "select"); + return GetUrl(); +} + +std::string SFSUrlBuilder::GetLatestVersionBatchUrl() +{ + SetNamesUrlPath(); + SetQuery("action", "BatchUpdates"); + return GetUrl(); +} + +std::string SFSUrlBuilder::GetSpecificVersionUrl(const std::string& product, const std::string& version) +{ + SetVersionsUrlPath(product); + AppendPathEncoded(version); + return GetUrl(); +} + +std::string SFSUrlBuilder::GetDownloadInfoUrl(const std::string& product, const std::string& version) +{ + SetVersionsUrlPath(product); + AppendPathEncoded(version); + AppendPath("files"); + SetQuery("action", "GenerateDownloadInfo"); + return GetUrl(); +} + +SFSUrlBuilder& SFSUrlBuilder::SetNamesUrlPath() +{ + ResetPath().ResetQuery(); + AppendPath("api").AppendPath(c_apiVersion); + AppendPath("contents").AppendPathEncoded(m_instanceId); + AppendPath("namespaces").AppendPathEncoded(m_nameSpace); + AppendPath("names"); + return *this; +} + +SFSUrlBuilder& SFSUrlBuilder::SetVersionsUrlPath(const std::string& product) +{ + SetNamesUrlPath(); + AppendPathEncoded(product); + AppendPath("versions"); + return *this; +} diff --git a/src/SfsClient/sfs-client/client/src/details/SFSUrlBuilder.h b/src/SfsClient/sfs-client/client/src/details/SFSUrlBuilder.h @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "UrlBuilder.h" + +namespace SFS::details +{ +// Wrapper struct that only exists to allow the SFSUrlBuilder to have a constructor that takes a custom URL +struct SFSCustomUrl +{ + explicit SFSCustomUrl(std::string url) : url(std::move(url)) + { + } + + std::string url; +}; + +class SFSUrlBuilder : private UrlBuilder +{ + public: + SFSUrlBuilder(const std::string& accountId, + std::string instanceId, + std::string nameSpace, + const ReportingHandler& handler); + + SFSUrlBuilder(const SFSCustomUrl& customUrl, + std::string instanceId, + std::string nameSpace, + const ReportingHandler& handler); + + std::string GetLatestVersionUrl(const std::string& product); + std::string GetLatestVersionBatchUrl(); + std::string GetSpecificVersionUrl(const std::string& product, const std::string& version); + std::string GetDownloadInfoUrl(const std::string& product, const std::string& version); + + using UrlBuilder::GetUrl; + + private: + SFSUrlBuilder& SetNamesUrlPath(); + SFSUrlBuilder& SetVersionsUrlPath(const std::string& product); + + std::string m_instanceId; + std::string m_nameSpace; +}; +} // namespace SFS::details diff --git a/src/SfsClient/sfs-client/client/src/details/TestOverride.cpp b/src/SfsClient/sfs-client/client/src/details/TestOverride.cpp @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "TestOverride.h" + +using namespace SFS; +using SFS::test::ScopedTestOverride; +using SFS::test::TestOverride; + +bool test::AreTestOverridesAllowed() +{ +#ifdef SFS_ENABLE_TEST_OVERRIDES + return true; +#else + return false; +#endif +} + +std::string test::GetEnvVarNameFromOverride(TestOverride override) +{ + switch (override) + { + case TestOverride::BaseRetryDelayMs: + return "SFS_TEST_BASE_RETRY_DELAY_MS"; + case TestOverride::BaseUrl: + return "SFS_TEST_OVERRIDE_BASE_URL"; + } + return ""; +} + +std::optional<std::string> test::GetTestOverride(TestOverride override) +{ + if (!AreTestOverridesAllowed()) + { + return std::nullopt; + } + + return details::env::GetEnv(GetEnvVarNameFromOverride(override)); +} + +std::optional<int> test::GetTestOverrideAsInt(TestOverride override) +{ + auto str = GetTestOverride(override); + if (str) + { + return std::stoi(*str); + } + return std::nullopt; +} + +ScopedTestOverride::ScopedTestOverride(TestOverride override, const std::string& value) + : m_scopedEnv(GetEnvVarNameFromOverride(override), value) +{ +} + +ScopedTestOverride::ScopedTestOverride(TestOverride override, int value) + : m_scopedEnv(GetEnvVarNameFromOverride(override), std::to_string(value)) +{ +} diff --git a/src/SfsClient/sfs-client/client/src/details/TestOverride.h b/src/SfsClient/sfs-client/client/src/details/TestOverride.h @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "Env.h" + +#include <optional> +#include <string> + +namespace SFS::test +{ +/** + * @brief Check if test overrides are allowed and logs if so. + * @details Test overrides are allowed if the SFS_ENABLE_TEST_OVERRIDES macro is defined. + */ +bool AreTestOverridesAllowed(); + +enum class TestOverride +{ + BaseRetryDelayMs, // Integer. Allows one to override the base retry delay. + BaseUrl, // String. Allows one to override the base URL used for all requests +}; + +/** + * @brief Get the environment variable name for a given test override + */ +std::string GetEnvVarNameFromOverride(TestOverride override); + +/** + * @brief Get the value of a test override + * @details std::nullopt is returned if the environment variable is not set or in case of failure. + * The returned string may be different in Win32 due to the encoding of the environment variables. + */ +std::optional<std::string> GetTestOverride(TestOverride override); + +/** + * @brief Get the value of a test override as int + * @details std::nullopt is returned if the environment variable is not set or in case of failure. + */ +std::optional<int> GetTestOverrideAsInt(TestOverride override); + +class ScopedTestOverride +{ + public: + ScopedTestOverride(TestOverride override, const std::string& value); + ScopedTestOverride(TestOverride override, int value); + + private: + SFS::details::env::ScopedEnv m_scopedEnv; +}; +} // namespace SFS::test diff --git a/src/SfsClient/sfs-client/client/src/details/UrlBuilder.cpp b/src/SfsClient/sfs-client/client/src/details/UrlBuilder.cpp @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "UrlBuilder.h" + +#include "ErrorHandling.h" +#include "ReportingHandler.h" + +#include <curl/curl.h> + +#include <memory> + +#define THROW_IF_CURL_URL_ERROR(curlCall, error) \ + do \ + { \ + auto __curlUrlCode = (curlCall); \ + THROW_CODE_IF_NOT_LOG(error, __curlUrlCode == CURLUE_OK, m_handler, GetCurlUrlStrError(__curlUrlCode)); \ + } while ((void)0, 0) + +#define THROW_IF_CURL_URL_SETUP_ERROR(curlCall) THROW_IF_CURL_URL_ERROR(curlCall, ConnectionUrlSetupFailed) + +using namespace SFS::details; + +namespace +{ +struct CurlCharDeleter +{ + void operator()(char* val) + { + if (val) + { + curl_free(val); + } + } +}; + +using CurlCharPtr = std::unique_ptr<char, CurlCharDeleter>; + +std::string GetCurlUrlStrError(CURLUcode code) +{ + return "Curl URL error: " + std::string(curl_url_strerror(code)); +} +} // namespace + +UrlBuilder::UrlBuilder(const ReportingHandler& handler) : m_handler(handler) +{ + m_handle = curl_url(); + THROW_CODE_IF_NOT_LOG(ConnectionUrlSetupFailed, m_handle, m_handler, "Curl URL error: Failed to create URL"); +} + +UrlBuilder::UrlBuilder(const std::string& url, const ReportingHandler& handler) : UrlBuilder(handler) +{ + SetUrl(url); +} + +UrlBuilder::~UrlBuilder() +{ + curl_url_cleanup(m_handle); +} + +std::string UrlBuilder::GetUrl() const +{ + CurlCharPtr url; + char* urlPtr = url.get(); + THROW_IF_CURL_URL_SETUP_ERROR(curl_url_get(m_handle, CURLUPART_URL, &urlPtr, 0 /*flags*/)); + return urlPtr; +} + +UrlBuilder& UrlBuilder::SetScheme(Scheme scheme) +{ + switch (scheme) + { + case Scheme::Https: + THROW_IF_CURL_URL_SETUP_ERROR(curl_url_set(m_handle, CURLUPART_SCHEME, "https", 0 /*flags*/)); + break; + } + return *this; +} + +UrlBuilder& UrlBuilder::SetHost(const std::string& host) +{ + THROW_CODE_IF_LOG(InvalidArg, host.empty(), m_handler, "Host must not empty"); + THROW_IF_CURL_URL_SETUP_ERROR(curl_url_set(m_handle, CURLUPART_HOST, host.c_str(), 0 /*flags*/)); + return *this; +} + +UrlBuilder& UrlBuilder::SetPath(const std::string& path) +{ + THROW_CODE_IF_LOG(InvalidArg, path.empty(), m_handler, "Path must not empty"); + m_path = path; + THROW_IF_CURL_URL_SETUP_ERROR(curl_url_set(m_handle, CURLUPART_PATH, m_path.c_str(), 0 /*flags*/)); + return *this; +} + +UrlBuilder& UrlBuilder::AppendPath(const std::string& path) +{ + return AppendPath(path, false /*encode*/); +} + +UrlBuilder& UrlBuilder::AppendPathEncoded(const std::string& path) +{ + return AppendPath(path, true /*encode*/); +} + +UrlBuilder& UrlBuilder::AppendPath(const std::string& path, bool encode) +{ + THROW_CODE_IF_LOG(InvalidArg, path.empty(), m_handler, "Path must not empty"); + if (!m_path.empty() && m_path.back() != '/') + { + m_path += '/'; + } + + if (encode) + { + m_path += URLEncode(path); + } + else + { + m_path += path; + } + + THROW_IF_CURL_URL_SETUP_ERROR(curl_url_set(m_handle, CURLUPART_PATH, m_path.c_str(), 0 /*flags*/)); + return *this; +} + +UrlBuilder& UrlBuilder::ResetPath() +{ + m_path.clear(); + THROW_IF_CURL_URL_SETUP_ERROR(curl_url_set(m_handle, CURLUPART_PATH, "", 0 /*flags*/)); + return *this; +} + +UrlBuilder& UrlBuilder::SetQuery(const std::string& key, const std::string& value) +{ + THROW_CODE_IF_LOG(InvalidArg, key.empty() || value.empty(), m_handler, "Query key and value must not empty"); + const std::string query = URLEncode(key) + "=" + URLEncode(value); + THROW_IF_CURL_URL_SETUP_ERROR(curl_url_set(m_handle, CURLUPART_QUERY, query.c_str(), 0 /*flags*/)); + return *this; +} + +UrlBuilder& UrlBuilder::AppendQuery(const std::string& key, const std::string& value) +{ + THROW_CODE_IF_LOG(InvalidArg, key.empty() || value.empty(), m_handler, "Query key and value must not empty"); + const std::string query = URLEncode(key) + "=" + URLEncode(value); + THROW_IF_CURL_URL_SETUP_ERROR(curl_url_set(m_handle, CURLUPART_QUERY, query.c_str(), CURLU_APPENDQUERY)); + return *this; +} + +UrlBuilder& UrlBuilder::ResetQuery() +{ + THROW_IF_CURL_URL_SETUP_ERROR(curl_url_set(m_handle, CURLUPART_QUERY, "", 0 /*flags*/)); + return *this; +} + +UrlBuilder& UrlBuilder::SetUrl(const std::string& url) +{ + THROW_CODE_IF_LOG(InvalidArg, url.empty(), m_handler, "Url must not empty"); + THROW_IF_CURL_URL_SETUP_ERROR(curl_url_set(m_handle, CURLUPART_URL, url.c_str(), 0 /*flags*/)); + return *this; +} + +std::string UrlBuilder::URLEncode(const std::string& str) const +{ + CurlCharPtr encodedStr{curl_easy_escape(nullptr /*ignored*/, str.c_str(), static_cast<int>(str.length()))}; + THROW_CODE_IF_NOT_LOG(ConnectionUrlSetupFailed, encodedStr, m_handler, "Failed to URL-encode string"); + return encodedStr.get(); +} diff --git a/src/SfsClient/sfs-client/client/src/details/UrlBuilder.h b/src/SfsClient/sfs-client/client/src/details/UrlBuilder.h @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include <string> + +// Forward declaration +struct Curl_URL; +typedef struct Curl_URL CURLU; + +namespace SFS::details +{ +class ReportingHandler; + +enum class Scheme +{ + Https, +}; + +class UrlBuilder +{ + public: + /** + * @brief Construct a new Url Builder object with an empty URL + */ + explicit UrlBuilder(const ReportingHandler& handler); + + /** + * @brief Construct a new Url Builder object with an existing URL + * @param url The URL to set + */ + UrlBuilder(const std::string& url, const ReportingHandler& handler); + + ~UrlBuilder(); + + UrlBuilder(const UrlBuilder&) = delete; + UrlBuilder& operator=(const UrlBuilder&) = delete; + + std::string GetUrl() const; + + /** + * @brief Set the scheme for the URL + * @param scheme The scheme to set for the URL Ex: Https + * @return The reference to the current object + */ + UrlBuilder& SetScheme(Scheme scheme); + + /** + * @brief Set the host for the URL + * @param host The host to set for the URL. Ex: www.example.com + * @throws SFSException if the string is invalid + * @return The reference to the current object + */ + UrlBuilder& SetHost(const std::string& host); + + /** + * @brief Set a path to the URL + * @param path The path to set for the URL. Ex: index.html + * @throws SFSException if the string is invalid + * @return The reference to the current object + */ + UrlBuilder& SetPath(const std::string& path); + + /** + * @brief Append a path to the URL + * @param path The path to be appended for the URL. Ex: index.html + * @throws SFSException if the string is invalid + * @return The reference to the current object + */ + UrlBuilder& AppendPath(const std::string& path); + + /** + * @brief Append a path to the URL. The new path element will be URL encoded, including forward slashes + * @param path The path to be appended for the URL. Ex: index.html + * @throws SFSException if the string is invalid + * @return The reference to the current object + */ + UrlBuilder& AppendPathEncoded(const std::string& path); + + /** + * @brief Reset the path of the URL + * @return The reference to the current object + */ + UrlBuilder& ResetPath(); + + /** + * @brief Set a query to the URL (?key=value) + * @param key The key of the query string. Ex: value + * @param value The value of the query string. Ex: value + * @note Both key and value will be URL encoded + * @throws SFSException if the string is invalid + * @return The reference to the current object + */ + UrlBuilder& SetQuery(const std::string& key, const std::string& value); + + /** + * @brief Append a query to the URL (&key=value) + * @param key The key of the query string. Ex: value + * @param value The value of the query string. Ex: value + * @note Both key and value will be URL encoded + * @throws SFSException if the string is invalid + * @return The reference to the current object + */ + UrlBuilder& AppendQuery(const std::string& key, const std::string& value); + + /** + * @brief Reset the query of the URL + * @return The reference to the current object + */ + UrlBuilder& ResetQuery(); + + /** + * @brief Set the URL through a string. Other methods can still be called later to modify the URl + * @param url The string to set as URL. Ex: http://www.example.com/index.html + * @throws SFSException if the string is invalid + * @return The reference to the current object + */ + UrlBuilder& SetUrl(const std::string& url); + + private: + /** + * @brief Append a path to the URL + * @param path The path to be appended for the URL. Ex: index.html + * @param encode If true, the new path element will be URL encoded, including forward slashes + * @throws SFSException if the string is invalid + * @return The reference to the current object + */ + UrlBuilder& AppendPath(const std::string& path, bool encode); + + /** + * @brief URL-encode (or percent-encode) a given string + */ + std::string URLEncode(const std::string& str) const; + + const ReportingHandler& m_handler; + + CURLU* m_handle = nullptr; + std::string m_path; +}; +} // namespace SFS::details diff --git a/src/SfsClient/sfs-client/client/src/details/Util.cpp b/src/SfsClient/sfs-client/client/src/details/Util.cpp @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "Util.h" + +#include <algorithm> +#include <string> + +using namespace SFS::details; + +bool util::AreEqualI(std::string_view a, std::string_view b) +{ + return std::equal(a.begin(), a.end(), b.begin(), b.end(), [](char a, char b) { + return std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b)); + }); +} + +bool util::AreNotEqualI(std::string_view a, std::string_view b) +{ + return !AreEqualI(a, b); +} diff --git a/src/SfsClient/sfs-client/client/src/details/Util.h b/src/SfsClient/sfs-client/client/src/details/Util.h @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include <string_view> + +namespace SFS::details::util +{ +bool AreEqualI(std::string_view a, std::string_view b); +bool AreNotEqualI(std::string_view a, std::string_view b); +} // namespace SFS::details::util diff --git a/src/SfsClient/sfs-client/client/src/details/connection/Connection.cpp b/src/SfsClient/sfs-client/client/src/details/connection/Connection.cpp @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "Connection.h" + +using namespace SFS; +using namespace SFS::details; + +Connection::Connection(const ConnectionConfig& config, const ReportingHandler& handler) : m_handler(handler) +{ + if (config.baseCV) + { + m_cv = std::move(CorrelationVector(*config.baseCV, m_handler)); + } + m_maxRetries = config.maxRetries; +} + +std::string Connection::Post(const std::string& url) +{ + return Post(url, {}); +} diff --git a/src/SfsClient/sfs-client/client/src/details/connection/Connection.h b/src/SfsClient/sfs-client/client/src/details/connection/Connection.h @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "../CorrelationVector.h" +#include "ConnectionConfig.h" + +#include <string> + +namespace SFS::details +{ +class ReportingHandler; + +class Connection +{ + public: + Connection(const ConnectionConfig& config, const ReportingHandler& handler); + + virtual ~Connection() + { + } + + Connection(const Connection&) = delete; + Connection& operator=(const Connection&) = delete; + + /** + * @brief Perform a GET request to the given @param url + * @return The response body + * @throws SFSException if the request fails + */ + virtual std::string Get(const std::string& url) = 0; + + /** + * @brief Perform a POST request to the given @param url with @param data as the request body + * @return The response body + * @throws SFSException if the request fails + */ + virtual std::string Post(const std::string& url, const std::string& data) = 0; + + /** + * @brief Perform a POST request to the given @param url + * @return The response body + * @throws SFSException if the request fails + */ + std::string Post(const std::string& url); + + protected: + const ReportingHandler& m_handler; + + /// @brief The correlation vector to use for requests + CorrelationVector m_cv; + + /// @brief Expected number of retries for a web request after a failed attempt + unsigned m_maxRetries{3}; +}; +} // namespace SFS::details diff --git a/src/SfsClient/sfs-client/client/src/details/connection/ConnectionConfig.cpp b/src/SfsClient/sfs-client/client/src/details/connection/ConnectionConfig.cpp @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "ConnectionConfig.h" + +#include "RequestParams.h" + +using namespace SFS; +using namespace SFS::details; + +ConnectionConfig::ConnectionConfig(const SFS::RequestParams& requestParams) + : maxRetries(requestParams.retryOnError ? c_maxRetries : 0) + , baseCV(requestParams.baseCV) +{ +} diff --git a/src/SfsClient/sfs-client/client/src/details/connection/ConnectionConfig.h b/src/SfsClient/sfs-client/client/src/details/connection/ConnectionConfig.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include <optional> +#include <string> + +namespace SFS +{ +struct RequestParams; + +namespace details +{ +struct ConnectionConfig +{ + ConnectionConfig() = default; + explicit ConnectionConfig(const RequestParams& requestParams); + + /// @brief Expected number of retries for a web request after a failed attempt + unsigned maxRetries{3}; + + /// @brief The correlation vector to use for requests + std::optional<std::string> baseCV; +}; +} // namespace details +} // namespace SFS diff --git a/src/SfsClient/sfs-client/client/src/details/connection/ConnectionManager.cpp b/src/SfsClient/sfs-client/client/src/details/connection/ConnectionManager.cpp @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "ConnectionManager.h" + +#include "../ReportingHandler.h" + +using namespace SFS::details; + +ConnectionManager::ConnectionManager(const ReportingHandler& handler) : m_handler(handler) +{ +} + +ConnectionManager::~ConnectionManager() +{ +} diff --git a/src/SfsClient/sfs-client/client/src/details/connection/ConnectionManager.h b/src/SfsClient/sfs-client/client/src/details/connection/ConnectionManager.h @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include <memory> + +namespace SFS::details +{ +class Connection; +class ReportingHandler; +struct ConnectionConfig; + +class ConnectionManager +{ + public: + ConnectionManager(const ReportingHandler& handler); + virtual ~ConnectionManager(); + + ConnectionManager(const ConnectionManager&) = delete; + ConnectionManager& operator=(const ConnectionManager&) = delete; + + virtual std::unique_ptr<Connection> MakeConnection(const ConnectionConfig& config) = 0; + + protected: + const ReportingHandler& m_handler; +}; +} // namespace SFS::details diff --git a/src/SfsClient/sfs-client/client/src/details/connection/CurlConnection.cpp b/src/SfsClient/sfs-client/client/src/details/connection/CurlConnection.cpp @@ -0,0 +1,427 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "CurlConnection.h" + +#include "../ErrorHandling.h" +#include "../ReportingHandler.h" +#include "../TestOverride.h" +#include "HttpHeader.h" + +#include <curl/curl.h> + +#include <chrono> +#include <cstring> +#include <optional> +#include <thread> + +#define THROW_IF_CURL_ERROR(curlCall, error) \ + do \ + { \ + auto __curlCode = (curlCall); \ + std::string __message = "Curl error: " + std::string(curl_easy_strerror(__curlCode)); \ + THROW_CODE_IF_NOT_LOG(error, __curlCode == CURLE_OK, m_handler, std::move(__message)); \ + } while ((void)0, 0) + +#define THROW_IF_CURL_SETUP_ERROR(curlCall) THROW_IF_CURL_ERROR(curlCall, ConnectionSetupFailed) +#define THROW_IF_CURL_UNEXPECTED_ERROR(curlCall) THROW_IF_CURL_ERROR(curlCall, ConnectionUnexpectedError) + +// Setting a hard limit of 100k characters for the response to avoid rogue servers sending huge amounts of data +#define MAX_RESPONSE_CHARACTERS 100000 + +using namespace SFS; +using namespace SFS::details; +using namespace std::chrono_literals; + +namespace +{ +// Curl callback for writing data to a std::string. Must return the number of bytes written. +// This callback may be called multiple times for a single request, and will keep appending +// to userData until the request is complete. The data received is not null-terminated. +// For SFS, this data will likely be a JSON string. +size_t WriteCallback(char* contents, size_t sizeInBytes, size_t numElements, void* userData) +{ + auto readBufferPtr = static_cast<std::string*>(userData); + if (readBufferPtr) + { + size_t totalSize = sizeInBytes * numElements; + + // Checking final response size to avoid unexpected amounts of data + if ((readBufferPtr->length() + totalSize) > MAX_RESPONSE_CHARACTERS) + { + return CURL_WRITEFUNC_ERROR; + } + + readBufferPtr->append(contents, totalSize); + return totalSize; + } + return CURL_WRITEFUNC_ERROR; +} + +struct CurlErrorBuffer +{ + public: + CurlErrorBuffer(CURL* handle, const ReportingHandler& reportingHandler) + : m_handle(handle) + , m_reportingHandler(reportingHandler) + { + m_errorBuffer[0] = '\0'; + SetBuffer(); + } + + ~CurlErrorBuffer() + { + LOG_IF_FAILED(UnsetBuffer(), m_reportingHandler); + } + + void SetBuffer() + { + THROW_CODE_IF_NOT_LOG(ConnectionSetupFailed, + curl_easy_setopt(m_handle, CURLOPT_ERRORBUFFER, m_errorBuffer) == CURLE_OK, + m_reportingHandler, + "Failed to set up error buffer for curl"); + } + + Result UnsetBuffer() + { + return curl_easy_setopt(m_handle, CURLOPT_ERRORBUFFER, nullptr) == CURLE_OK + ? Result::Success + : Result(Result::ConnectionSetupFailed, "Failed to unset curl error buffer"); + } + + char* Get() + { + return m_errorBuffer; + } + + private: + CURL* m_handle; + const ReportingHandler& m_reportingHandler; + + char m_errorBuffer[CURL_ERROR_SIZE]; +}; + +Result CurlCodeToResult(CURLcode curlCode, char* errorBuffer) +{ + Result::Code code; + switch (curlCode) + { + case CURLE_OPERATION_TIMEDOUT: + code = Result::HttpTimeout; + break; + default: + code = Result::ConnectionUnexpectedError; + break; + } + + const bool isErrorStringRegistered = errorBuffer && errorBuffer[0] != '\0'; + std::string message = isErrorStringRegistered ? errorBuffer : "Curl error"; + + return Result(code, std::move(message)); +} + +bool IsSuccessfulSFSHttpCode(long httpCode) +{ + return httpCode == 200; +} + +Result HttpCodeToResult(long httpCode) +{ + if (IsSuccessfulSFSHttpCode(httpCode)) + { + return Result::Success; + } + + switch (httpCode) + { + case 400: + { + return Result(Result::HttpBadRequest, "400 Bad Request"); + } + case 404: + { + return Result(Result::HttpNotFound, "404 Not Found"); + } + case 405: + { + return Result(Result::HttpMethodNotAllowed, "405 Method Not Allowed"); + } + case 429: + { + return Result(Result::HttpTooManyRequests, "429 Too Many Requests"); + } + case 503: + { + return Result(Result::HttpServiceNotAvailable, "503 Service Unavailable"); + } + default: + { + return Result(Result::HttpUnexpected, "Unexpected HTTP code " + std::to_string(httpCode)); + } + } +} + +bool IsRetriableHttpError(long httpCode) +{ + switch (httpCode) + { + case 429: // Too Many Requests - Rate Limiting + case 500: // InternalServerError - Can be triggered within server timeouts, network issue + case 502: // BadGateway - Likely an issue with routing + case 503: // ServerBusy + case 504: // GatewayTimeout + return true; + default: + return false; + } +} + +std::optional<std::string> GetResponseHeader(CURL* handle, + HttpHeader httpHeader, + const ReportingHandler& reportingHandler) +{ + const std::string headerName = ToString(httpHeader); + + // This struct only represents data inside the CURL handle, and must not be manually freed + curl_header* header; + const int lastRequest = -1; + CURLHcode curlhCode = curl_easy_header(handle, headerName.c_str(), 0 /*index*/, CURLH_HEADER, lastRequest, &header); + switch (curlhCode) + { + case CURLHE_OK: + return header->value; + case CURLHE_BADINDEX: + case CURLHE_MISSING: + case CURLHE_NOHEADERS: + return std::nullopt; + default: + THROW_LOG( + Result(Result::ConnectionUnexpectedError, + "Failed to get response header " + headerName + " with CURLH code " + std::to_string(curlhCode)), + reportingHandler); + } + return std::nullopt; +} + +std::chrono::milliseconds ParseRetryAfterValue(const std::string& retryAfter, const ReportingHandler& reportingHandler) +{ + LOG_VERBOSE(reportingHandler, "Parsing Retry-After value [%s]", retryAfter.c_str()); + std::chrono::seconds retryAfterSec{0}; + try + { + retryAfterSec = std::chrono::seconds(std::stoi(retryAfter)); + } + catch (std::invalid_argument&) + { + // Value is not an integer, but may still be in HTTP Date format + const time_t retryAfterSecSinceEpoch = curl_getdate(retryAfter.c_str(), nullptr /*unused*/); + if (retryAfterSecSinceEpoch == -1) + { + THROW_LOG(Result(Result::ConnectionUnexpectedError, + "Retry-After header value could not be converted to an integer or an HTTP Date"), + reportingHandler); + } + + // Get number of seconds since epoch for now to calculate the difference + const auto epoch = std::chrono::system_clock::now().time_since_epoch(); + const auto nowSecSinceEpoch = std::chrono::duration_cast<std::chrono::seconds>(epoch); + + retryAfterSec = std::chrono::seconds(retryAfterSecSinceEpoch) - nowSecSinceEpoch; + } + catch (std::out_of_range&) + { + THROW_LOG(Result(Result::ConnectionUnexpectedError, "Retry-After header value is not in the expected range"), + reportingHandler); + } + if (retryAfterSec <= 0s) + { + THROW_LOG(Result(Result::ConnectionUnexpectedError, "Invalid Retry-After header value"), reportingHandler); + } + return retryAfterSec; +} +} // namespace + +namespace SFS::details +{ +struct CurlHeaderList +{ + public: + CurlHeaderList() = default; + + ~CurlHeaderList() + { + curl_slist_free_all(m_slist); + } + + /** + * @throws SFSException if the header cannot be added to the list. + */ + void Add(HttpHeader header, const std::string& value) + { + const std::string data = ToString(header) + ": " + value; + const auto ret = curl_slist_append(m_slist, data.c_str()); + if (!ret) + { + throw SFSException(Result::ConnectionSetupFailed, "Failed to add header " + data + " to CurlHeaderList"); + } + m_slist = ret; + } + + struct curl_slist* m_slist{nullptr}; +}; +} // namespace SFS::details + +CurlConnection::CurlConnection(const ConnectionConfig& config, const ReportingHandler& handler) + : Connection(config, handler) +{ + m_handle = curl_easy_init(); + THROW_CODE_IF_NOT_LOG(ConnectionSetupFailed, m_handle, m_handler, "Failed to init curl connection"); + + // Turning timeout signals off to avoid issues with threads + // See https://curl.se/libcurl/c/threadsafe.html + THROW_CODE_IF_NOT_LOG(ConnectionSetupFailed, + curl_easy_setopt(m_handle, CURLOPT_NOSIGNAL, 1L) == CURLE_OK, + m_handler, + "Failed to set up curl"); + + // TODO #41: Pass AAD token in the header if it is available + // TODO #42: Cert pinning with service +} + +CurlConnection::~CurlConnection() +{ + if (m_handle) + { + curl_easy_cleanup(m_handle); + } +} + +std::string CurlConnection::Get(const std::string& url) +{ + THROW_CODE_IF_LOG(InvalidArg, url.empty(), m_handler, "url cannot be empty"); + + THROW_IF_CURL_SETUP_ERROR(curl_easy_setopt(m_handle, CURLOPT_HTTPGET, 1L)); + THROW_IF_CURL_SETUP_ERROR(curl_easy_setopt(m_handle, CURLOPT_HTTPHEADER, nullptr)); + + CurlHeaderList headers; + return CurlPerform(url, headers); +} + +std::string CurlConnection::Post(const std::string& url, const std::string& data) +{ + THROW_CODE_IF_LOG(InvalidArg, url.empty(), m_handler, "url cannot be empty"); + + CurlHeaderList headerList; + headerList.Add(HttpHeader::ContentType, "application/json"); + + THROW_IF_CURL_SETUP_ERROR(curl_easy_setopt(m_handle, CURLOPT_POST, 1L)); + THROW_IF_CURL_SETUP_ERROR(curl_easy_setopt(m_handle, CURLOPT_COPYPOSTFIELDS, data.c_str())); + + CurlHeaderList headers; + headers.Add(HttpHeader::ContentType, "application/json"); + return CurlPerform(url, headers); +} + +std::string CurlConnection::CurlPerform(const std::string& url, CurlHeaderList& headers) +{ + THROW_IF_CURL_SETUP_ERROR(curl_easy_setopt(m_handle, CURLOPT_URL, url.c_str())); + + const std::string cv = m_cv.IncrementAndGet(); + headers.Add(HttpHeader::MSCV, cv); + headers.Add(HttpHeader::UserAgent, GetUserAgentValue()); + + THROW_IF_CURL_SETUP_ERROR(curl_easy_setopt(m_handle, CURLOPT_HTTPHEADER, headers.m_slist)); + + // Setting up error buffer where error messages get written - this gets unset in the destructor + CurlErrorBuffer errorBuffer(m_handle, m_handler); + + std::string readBuffer; + THROW_IF_CURL_SETUP_ERROR(curl_easy_setopt(m_handle, CURLOPT_WRITEFUNCTION, WriteCallback)); + THROW_IF_CURL_SETUP_ERROR(curl_easy_setopt(m_handle, CURLOPT_WRITEDATA, &readBuffer)); + + // Retry the connection a specified number of times + const unsigned totalAttempts = 1 + m_maxRetries; + for (unsigned i = 0; i < totalAttempts; i++) + { + const unsigned attempt = i + 1; + LOG_INFO(m_handler, "Request attempt %u out of %u (cv: %s)", attempt, totalAttempts, cv.c_str()); + const bool lastAttempt = attempt == totalAttempts; + + // Clear the buffer before each attempt + readBuffer.clear(); + + // Perform the request + const auto result = curl_easy_perform(m_handle); + if (result != CURLE_OK) + { + THROW_LOG(CurlCodeToResult(result, errorBuffer.Get()), m_handler); + } + + // Check request status to stop or retry + long httpCode = 0; + THROW_IF_CURL_UNEXPECTED_ERROR(curl_easy_getinfo(m_handle, CURLINFO_RESPONSE_CODE, &httpCode)); + + if (IsSuccessfulSFSHttpCode(httpCode)) + { + break; + } + + const Result httpResult = HttpCodeToResult(httpCode); + if (!CanRetryRequest(lastAttempt, httpCode)) + { + THROW_LOG(httpResult, m_handler); + } + + ProcessRetry(attempt, httpResult); + } + + return readBuffer; +} + +bool CurlConnection::CanRetryRequest(bool lastAttempt, long httpCode) +{ + if (lastAttempt) + { + LOG_INFO(m_handler, "No retry as this is the last attempt"); + return false; + } + + if (!IsRetriableHttpError(httpCode)) + { + LOG_INFO(m_handler, "Error %ld is not retriable, stopping", httpCode); + return false; + } + + return true; +} + +void CurlConnection::ProcessRetry(int attempt, const Result& httpResult) +{ + // Wait before retrying. Prefer the Retry-After information if available + std::chrono::milliseconds retryDelay{0}; + const std::optional<std::string> retryAfter = GetResponseHeader(m_handle, HttpHeader::RetryAfter, m_handler); + if (retryAfter) + { + // TODO #93: Enforce Retry-After value across calls to avoid caller spamming server + retryDelay = ParseRetryAfterValue(*retryAfter, m_handler); + } + else + { + // Apply exponential back-off with a factor of 2 + static std::chrono::milliseconds s_baseRetryDelay = 15s; // Value recommended as interval by the service + + std::chrono::milliseconds baseRetryDelay = s_baseRetryDelay; + + // Value can be overriden in tests + if (auto override = test::GetTestOverrideAsInt(test::TestOverride::BaseRetryDelayMs)) + { + baseRetryDelay = std::chrono::milliseconds{*override}; + } + + retryDelay = baseRetryDelay * (1 << (attempt - 1)); + } + + LOG_IF_FAILED(httpResult, m_handler); + LOG_INFO(m_handler, "Sleeping for %lld ms", static_cast<long long>(retryDelay.count())); + std::this_thread::sleep_for(retryDelay); +} diff --git a/src/SfsClient/sfs-client/client/src/details/connection/CurlConnection.h b/src/SfsClient/sfs-client/client/src/details/connection/CurlConnection.h @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "Connection.h" + +#include <string> + +// Forward declaration +typedef void CURL; + +namespace SFS +{ +class Result; + +namespace details +{ +struct CurlHeaderList; +class ReportingHandler; + +class CurlConnection : public Connection +{ + public: + CurlConnection(const ConnectionConfig& config, const ReportingHandler& handler); + ~CurlConnection() override; + + /** + * @brief Perform a GET request to the given @param url + * @return The response body + * @throws SFSException if the request fails + */ + std::string Get(const std::string& url) override; + + /** + * @brief Perform a POST request to the given @param url with @param data as the request body + * @return The response body + * @throws SFSException if the request fails + */ + std::string Post(const std::string& url, const std::string& data) override; + + private: + /** + * @brief Perform checks that the request can be retried + */ + bool CanRetryRequest(bool lastAttempt, long httpCode); + + /** + * @brief Process retry and perform wait logic before retrying the request + */ + void ProcessRetry(int attempt, const Result& httpResult); + + protected: + /** + * @brief Perform a REST request to the given @param url with the given @param headers + * @return The response body + * @throws SFSException if the request fails + */ + virtual std::string CurlPerform(const std::string& url, CurlHeaderList& headers); + + CURL* m_handle; +}; +} // namespace details +} // namespace SFS diff --git a/src/SfsClient/sfs-client/client/src/details/connection/CurlConnectionManager.cpp b/src/SfsClient/sfs-client/client/src/details/connection/CurlConnectionManager.cpp @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "CurlConnectionManager.h" + +#include "../ErrorHandling.h" +#include "CurlConnection.h" + +#include <curl/curl.h> + +using namespace SFS; +using namespace SFS::details; + +namespace +{ +// Curl recommends checking for expected features in runtime +void CheckCurlFeatures(const ReportingHandler& handler) +{ + curl_version_info_data* ver = curl_version_info(CURLVERSION_NOW); + THROW_CODE_IF_NOT_LOG(HttpUnexpected, ver, handler); + + THROW_CODE_IF_NOT_LOG(HttpUnexpected, (ver->features & CURL_VERSION_SSL), handler, "Curl was not built with SSL"); + THROW_CODE_IF_NOT_LOG(HttpUnexpected, + (ver->features & CURL_VERSION_THREADSAFE), + handler, + "Curl is not thread safe"); + + // For thread safety we need the DNS resolutions to be asynchronous (which happens because of c-ares) + THROW_CODE_IF_NOT_LOG(HttpUnexpected, + (ver->features & CURL_VERSION_ASYNCHDNS), + handler, + "Curl was not built with async DNS resolutions"); +} +} // namespace + +CurlConnectionManager::CurlConnectionManager(const ReportingHandler& handler) : ConnectionManager(handler) +{ + THROW_CODE_IF_NOT_LOG(HttpUnexpected, + curl_global_init(CURL_GLOBAL_ALL) == CURLE_OK, + m_handler, + "Curl failed to initialize"); + CheckCurlFeatures(m_handler); +} + +CurlConnectionManager::~CurlConnectionManager() +{ + curl_global_cleanup(); +} + +std::unique_ptr<Connection> CurlConnectionManager::MakeConnection(const ConnectionConfig& config) +{ + return std::make_unique<CurlConnection>(config, m_handler); +} diff --git a/src/SfsClient/sfs-client/client/src/details/connection/CurlConnectionManager.h b/src/SfsClient/sfs-client/client/src/details/connection/CurlConnectionManager.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "ConnectionManager.h" + +#include <memory> + +namespace SFS::details +{ +class Connection; +class ReportingHandler; +struct ConnectionConfig; + +class CurlConnectionManager : public ConnectionManager +{ + public: + CurlConnectionManager(const ReportingHandler& handler); + ~CurlConnectionManager() override; + + std::unique_ptr<Connection> MakeConnection(const ConnectionConfig& config) override; +}; +} // namespace SFS::details diff --git a/src/SfsClient/sfs-client/client/src/details/connection/HttpHeader.cpp b/src/SfsClient/sfs-client/client/src/details/connection/HttpHeader.cpp @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "HttpHeader.h" + +#include "../OSInfo.h" + +#include <correlation_vector/correlation_vector.h> + +using namespace std::string_literals; + +constexpr const char* c_userAgent = "Microsoft-SFSClient/" SFS_VERSION; + +std::string SFS::details::ToString(HttpHeader header) +{ + switch (header) + { + case HttpHeader::ContentType: + return "Content-Type"; + case HttpHeader::MSCV: + return microsoft::correlation_vector::HEADER_NAME; + case HttpHeader::RetryAfter: + return "Retry-After"; + case HttpHeader::UserAgent: + return "User-Agent"; + } + + return ""; +} + +std::string SFS::details::GetUserAgentValue() +{ + // Examples: + // - Microsoft-SFSClient/1.0.0 (Windows; x64) + // - Microsoft-SFSClient/1.0.0 (Linux; x86_64) + + return c_userAgent + " ("s + osinfo::GetPlatform() + "; "s + osinfo::GetOSMachineInfo() + ")"s; +} diff --git a/src/SfsClient/sfs-client/client/src/details/connection/HttpHeader.h b/src/SfsClient/sfs-client/client/src/details/connection/HttpHeader.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include <string> + +namespace SFS::details +{ +enum class HttpHeader +{ + ContentType, + MSCV, + RetryAfter, + UserAgent, +}; + +std::string ToString(HttpHeader header); + +std::string GetUserAgentValue(); +} // namespace SFS::details diff --git a/src/SfsClient/sfs-client/client/src/details/connection/mock/MockConnection.cpp b/src/SfsClient/sfs-client/client/src/details/connection/mock/MockConnection.cpp @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "MockConnection.h" + +using namespace SFS; +using namespace SFS::details; + +MockConnection::MockConnection(const ConnectionConfig& config, const ReportingHandler& handler) + : Connection(config, handler) +{ +} + +MockConnection::~MockConnection() +{ +} + +std::string MockConnection::Get(const std::string&) +{ + return {}; +} + +std::string MockConnection::Post(const std::string&, const std::string&) +{ + return {}; +} diff --git a/src/SfsClient/sfs-client/client/src/details/connection/mock/MockConnection.h b/src/SfsClient/sfs-client/client/src/details/connection/mock/MockConnection.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "../Connection.h" + +#include <string> + +namespace SFS::details +{ +class ReportingHandler; + +class MockConnection : public Connection +{ + public: + MockConnection(const ConnectionConfig& config, const ReportingHandler& handler); + ~MockConnection() override; + + std::string Get(const std::string& url) override; + std::string Post(const std::string& url, const std::string& data) override; +}; +} // namespace SFS::details diff --git a/src/SfsClient/sfs-client/client/src/details/connection/mock/MockConnectionManager.cpp b/src/SfsClient/sfs-client/client/src/details/connection/mock/MockConnectionManager.cpp @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "MockConnectionManager.h" + +#include "MockConnection.h" + +using namespace SFS::details; + +MockConnectionManager::MockConnectionManager(const ReportingHandler& handler) : ConnectionManager(handler) +{ +} + +MockConnectionManager::~MockConnectionManager() +{ +} + +std::unique_ptr<Connection> MockConnectionManager::MakeConnection(const ConnectionConfig& config) +{ + return std::make_unique<MockConnection>(config, m_handler); +} diff --git a/src/SfsClient/sfs-client/client/src/details/connection/mock/MockConnectionManager.h b/src/SfsClient/sfs-client/client/src/details/connection/mock/MockConnectionManager.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "../ConnectionManager.h" + +#include <memory> + +namespace SFS::details +{ +class Connection; +class ReportingHandler; +struct ConnectionConfig; + +class MockConnectionManager : public ConnectionManager +{ + public: + MockConnectionManager(const ReportingHandler& handler); + ~MockConnectionManager() override; + + std::unique_ptr<Connection> MakeConnection(const ConnectionConfig& config) override; +}; +} // namespace SFS::details diff --git a/src/SfsClient/sfs-client/client/src/details/entity/ContentType.cpp b/src/SfsClient/sfs-client/client/src/details/entity/ContentType.cpp @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "ContentType.h" + +#include "../ErrorHandling.h" +#include "../ReportingHandler.h" +#include "Result.h" + +using namespace SFS; +using namespace SFS::details; + +namespace +{ +std::string ToString(ContentType type) +{ + switch (type) + { + case ContentType::Generic: + return "Generic"; + case ContentType::App: + return "App"; + default: + return "Unknown"; + } +} +} // namespace + +void SFS::details::ValidateContentType(ContentType currentType, + ContentType expectedType, + const ReportingHandler& handler) +{ + THROW_CODE_IF_LOG(Result::ServiceUnexpectedContentType, + currentType != expectedType, + handler, + "Unexpected content type [" + ::ToString(currentType) + + "] returned by the service does not match the expected [" + ::ToString(expectedType) + "]"); +} diff --git a/src/SfsClient/sfs-client/client/src/details/entity/ContentType.h b/src/SfsClient/sfs-client/client/src/details/entity/ContentType.h @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +namespace SFS::details +{ +class ReportingHandler; + +enum class ContentType +{ + Generic, + App, +}; + +void ValidateContentType(ContentType currentType, ContentType expectedType, const ReportingHandler& handler); +} // namespace SFS::details diff --git a/src/SfsClient/sfs-client/client/src/details/entity/FileEntity.cpp b/src/SfsClient/sfs-client/client/src/details/entity/FileEntity.cpp @@ -0,0 +1,279 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "FileEntity.h" + +#include "../ErrorHandling.h" +#include "../ReportingHandler.h" +#include "../Util.h" +#include "AppFile.h" +#include "File.h" + +#include <nlohmann/json.hpp> + +#define THROW_INVALID_RESPONSE_IF_NOT(condition, message, handler) \ + THROW_CODE_IF_NOT_LOG(ServiceInvalidResponse, condition, handler, message) + +using namespace SFS; +using namespace SFS::details; +using namespace SFS::details::util; +using json = nlohmann::json; + +namespace +{ +HashType HashTypeFromString(const std::string& hashType, const ReportingHandler& handler) +{ + if (AreEqualI(hashType, "Sha1")) + { + return HashType::Sha1; + } + else if (AreEqualI(hashType, "Sha256")) + { + return HashType::Sha256; + } + else + { + THROW_LOG(Result(Result::Unexpected, "Unknown hash type: " + hashType), handler); + return HashType::Sha1; // Unreachable code, but the compiler doesn't know that. + } +} + +Architecture ArchitectureFromString(const std::string& arch, const ReportingHandler& handler) +{ + if (AreEqualI(arch, "None")) + { + return Architecture::None; + } + else if (AreEqualI(arch, "x86")) + { + return Architecture::x86; + } + else if (AreEqualI(arch, "amd64")) + { + return Architecture::Amd64; + } + else if (AreEqualI(arch, "arm")) + { + return Architecture::Arm; + } + else if (AreEqualI(arch, "arm64")) + { + return Architecture::Arm64; + } + else + { + THROW_LOG(Result(Result::Unexpected, "Unknown architecture: " + arch), handler); + return Architecture::None; // Unreachable code, but the compiler doesn't know that. + } +} +} // namespace + +std::unique_ptr<FileEntity> FileEntity::FromJson(const nlohmann::json& file, const ReportingHandler& handler) +{ + // Expected format for a generic file entity: + // { + // "FileId": <fileid>, + // "Url": <url>, + // "SizeInBytes": <size>, + // "Hashes": { + // "Sha1": <sha1>, + // "Sha256": <sha2> + // }, + // "DeliveryOptimization": {} // ignored, not used by the client. + // } + // + // Expected extra elements for an app version entity: + // { + // ... + // "ApplicabilityDetails": { + // "Architectures": [ + // "<arch>" + // ], + // "PlatformApplicabilityForPackage": [ + // "<app>" + // ] + // }, + // "FileMoniker": "<moniker>", + // } + + THROW_INVALID_RESPONSE_IF_NOT(file.is_object(), "File is not a JSON object", handler); + + std::unique_ptr<FileEntity> tmp; + const bool isAppEntity = file.contains("FileMoniker"); + if (isAppEntity) + { + tmp = std::make_unique<AppFileEntity>(); + } + else + { + tmp = std::make_unique<GenericFileEntity>(); + } + + THROW_INVALID_RESPONSE_IF_NOT(file.contains("FileId"), "Missing File.FileId in response", handler); + THROW_INVALID_RESPONSE_IF_NOT(file["FileId"].is_string(), "File.FileId is not a string", handler); + tmp->fileId = file["FileId"]; + + THROW_INVALID_RESPONSE_IF_NOT(file.contains("Url"), "Missing File.Url in response", handler); + THROW_INVALID_RESPONSE_IF_NOT(file["Url"].is_string(), "File.Url is not a string", handler); + tmp->url = file["Url"]; + + THROW_INVALID_RESPONSE_IF_NOT(file.contains("SizeInBytes"), "Missing File.SizeInBytes in response", handler); + THROW_INVALID_RESPONSE_IF_NOT(file["SizeInBytes"].is_number_unsigned(), + "File.SizeInBytes is not an unsigned number", + handler); + tmp->sizeInBytes = file["SizeInBytes"]; + + THROW_INVALID_RESPONSE_IF_NOT(file.contains("Hashes"), "Missing File.Hashes in response", handler); + THROW_INVALID_RESPONSE_IF_NOT(file["Hashes"].is_object(), "File.Hashes is not an object", handler); + + for (const auto& [hashType, hashValue] : file["Hashes"].items()) + { + THROW_INVALID_RESPONSE_IF_NOT(hashValue.is_string(), "File.Hashes object value is not a string", handler); + tmp->hashes[hashType] = hashValue; + } + + if (isAppEntity) + { + auto appEntity = dynamic_cast<AppFileEntity*>(tmp.get()); + + THROW_INVALID_RESPONSE_IF_NOT(file["FileMoniker"].is_string(), "File.FileMoniker is not a string", handler); + appEntity->fileMoniker = file["FileMoniker"]; + + THROW_INVALID_RESPONSE_IF_NOT(file.contains("ApplicabilityDetails"), + "Missing File.ApplicabilityDetails in response", + handler); + + const auto& details = file["ApplicabilityDetails"]; + THROW_INVALID_RESPONSE_IF_NOT(details.is_object(), "File.ApplicabilityDetails is not an object", handler); + + THROW_INVALID_RESPONSE_IF_NOT(details.contains("Architectures"), + "Missing File.ApplicabilityDetails.Architectures in response", + handler); + THROW_INVALID_RESPONSE_IF_NOT(details["Architectures"].is_array(), + "File.ApplicabilityDetails.Architectures is not an array", + handler); + for (const auto& arch : details["Architectures"]) + { + THROW_INVALID_RESPONSE_IF_NOT(arch.is_string(), + "File.ApplicabilityDetails.Architectures array value is not a string", + handler); + } + appEntity->applicabilityDetails.architectures = details["Architectures"]; + + THROW_INVALID_RESPONSE_IF_NOT(details.contains("PlatformApplicabilityForPackage"), + "Missing File.ApplicabilityDetails.PlatformApplicabilityForPackage in response", + handler); + THROW_INVALID_RESPONSE_IF_NOT(details["PlatformApplicabilityForPackage"].is_array(), + "File.ApplicabilityDetails.PlatformApplicabilityForPackage is not an array", + handler); + for (const auto& app : details["PlatformApplicabilityForPackage"]) + { + THROW_INVALID_RESPONSE_IF_NOT( + app.is_string(), + "File.ApplicabilityDetails.PlatformApplicabilityForPackage array value is not a string", + handler); + } + appEntity->applicabilityDetails.platformApplicabilityForPackage = details["PlatformApplicabilityForPackage"]; + } + + return tmp; +} + +FileEntities FileEntity::DownloadInfoResponseToFileEntities(const nlohmann::json& data, const ReportingHandler& handler) +{ + // Expected format is an array of FileEntity + THROW_CODE_IF_NOT_LOG(ServiceInvalidResponse, data.is_array(), handler, "Response is not a JSON array"); + + FileEntities tmp; + for (const auto& fileData : data) + { + THROW_CODE_IF_NOT_LOG(ServiceInvalidResponse, + fileData.is_object(), + handler, + "Array element is not a JSON object"); + tmp.push_back(std::move(FileEntity::FromJson(fileData, handler))); + } + + return tmp; +} + +ContentType GenericFileEntity::GetContentType() const +{ + return ContentType::Generic; +} + +std::unique_ptr<File> GenericFileEntity::ToFile(FileEntity&& entity, const ReportingHandler& handler) +{ + ValidateContentType(entity.GetContentType(), ContentType::Generic, handler); + + std::unordered_map<HashType, std::string> hashes; + for (auto& [hashType, hashValue] : entity.hashes) + { + hashes[HashTypeFromString(hashType, handler)] = std::move(hashValue); + } + + std::unique_ptr<File> tmp; + THROW_IF_FAILED_LOG( + File::Make(std::move(entity.fileId), std::move(entity.url), entity.sizeInBytes, std::move(hashes), tmp), + handler); + return tmp; +} + +std::vector<File> GenericFileEntity::FileEntitiesToFileVector(FileEntities&& entities, const ReportingHandler& handler) +{ + std::vector<File> tmp; + for (auto& entity : entities) + { + tmp.push_back(std::move(*GenericFileEntity::ToFile(std::move(*entity), handler))); + } + + return tmp; +} + +ContentType AppFileEntity::GetContentType() const +{ + return ContentType::App; +} + +std::unique_ptr<AppFile> AppFileEntity::ToAppFile(FileEntity&& entity, const ReportingHandler& handler) +{ + ValidateContentType(entity.GetContentType(), ContentType::App, handler); + + auto appEntity = dynamic_cast<AppFileEntity&&>(entity); + + std::unordered_map<HashType, std::string> hashes; + for (auto& [hashType, hashValue] : appEntity.hashes) + { + hashes[HashTypeFromString(hashType, handler)] = std::move(hashValue); + } + + std::vector<Architecture> architectures; + for (auto& arch : appEntity.applicabilityDetails.architectures) + { + architectures.push_back(ArchitectureFromString(arch, handler)); + } + + std::unique_ptr<AppFile> tmp; + THROW_IF_FAILED_LOG(AppFile::Make(std::move(appEntity.fileId), + std::move(appEntity.url), + appEntity.sizeInBytes, + std::move(hashes), + std::move(architectures), + std::move(appEntity.applicabilityDetails.platformApplicabilityForPackage), + std::move(appEntity.fileMoniker), + tmp), + handler); + return tmp; +} + +std::vector<AppFile> AppFileEntity::FileEntitiesToAppFileVector(std::vector<std::unique_ptr<FileEntity>>&& entities, + const ReportingHandler& handler) +{ + std::vector<AppFile> tmp; + for (auto& entity : entities) + { + tmp.push_back(std::move(*AppFileEntity::ToAppFile(std::move(*entity), handler))); + } + + return tmp; +} diff --git a/src/SfsClient/sfs-client/client/src/details/entity/FileEntity.h b/src/SfsClient/sfs-client/client/src/details/entity/FileEntity.h @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "ContentType.h" + +#include <memory> +#include <string> +#include <unordered_map> +#include <vector> + +#include <nlohmann/json_fwd.hpp> + +namespace SFS +{ +class File; +class AppFile; + +namespace details +{ +class ReportingHandler; + +struct FileEntity; + +using FileEntities = std::vector<std::unique_ptr<FileEntity>>; + +struct FileEntity +{ + virtual ~FileEntity() + { + } + + virtual ContentType GetContentType() const = 0; + + std::string fileId; + std::string url; + uint64_t sizeInBytes; + std::unordered_map<std::string, std::string> hashes; + + static std::unique_ptr<FileEntity> FromJson(const nlohmann::json& file, const ReportingHandler& handler); + static FileEntities DownloadInfoResponseToFileEntities(const nlohmann::json& data, const ReportingHandler& handler); +}; + +struct GenericFileEntity : public FileEntity +{ + ContentType GetContentType() const override; + + static std::unique_ptr<File> ToFile(FileEntity&& entity, const ReportingHandler& handler); + static std::vector<File> FileEntitiesToFileVector(FileEntities&& entities, const ReportingHandler& handler); +}; + +struct ApplicabilityDetailsEntity +{ + std::vector<std::string> architectures; + std::vector<std::string> platformApplicabilityForPackage; +}; + +struct AppFileEntity : public FileEntity +{ + ContentType GetContentType() const override; + + std::string fileMoniker; + ApplicabilityDetailsEntity applicabilityDetails; + + static std::unique_ptr<AppFile> ToAppFile(FileEntity&& entity, const ReportingHandler& handler); + static std::vector<AppFile> FileEntitiesToAppFileVector(FileEntities&& entities, const ReportingHandler& handler); +}; + +} // namespace details +} // namespace SFS diff --git a/src/SfsClient/sfs-client/client/src/details/entity/VersionEntity.cpp b/src/SfsClient/sfs-client/client/src/details/entity/VersionEntity.cpp @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "VersionEntity.h" + +#include "../ErrorHandling.h" +#include "../ReportingHandler.h" +#include "ContentId.h" + +#include <nlohmann/json.hpp> + +#define THROW_INVALID_RESPONSE_IF_NOT(condition, message, handler) \ + THROW_CODE_IF_NOT_LOG(ServiceInvalidResponse, condition, handler, message) + +using namespace SFS; +using namespace SFS::details; +using json = nlohmann::json; + +std::unique_ptr<VersionEntity> VersionEntity::FromJson(const nlohmann::json& data, const ReportingHandler& handler) +{ + // Expected format for a generic version entity: + // { + // "ContentId": { + // "Namespace": <ns>, + // "Name": <name>, + // "Version": <version> + // } + // } + // + // Expected extra elements for an app version entity: + // { + // ... + // "UpdateId": "<id>", + // "Prerequisites": [ + // { + // "Namespace": "<ns>", + // "Name": "<name>", + // "Version": "<version>" + // } + // ] + // } + + THROW_INVALID_RESPONSE_IF_NOT(data.is_object(), "Response is not a JSON object", handler); + + std::unique_ptr<VersionEntity> tmp; + const bool isAppEntity = data.contains("UpdateId"); + if (isAppEntity) + { + tmp = std::make_unique<AppVersionEntity>(); + } + else + { + tmp = std::make_unique<GenericVersionEntity>(); + } + + THROW_INVALID_RESPONSE_IF_NOT(data.contains("ContentId"), "Missing ContentId in response", handler); + + const auto& contentId = data["ContentId"]; + THROW_INVALID_RESPONSE_IF_NOT(contentId.is_object(), "ContentId is not a JSON object", handler); + + THROW_INVALID_RESPONSE_IF_NOT(contentId.contains("Namespace"), "Missing ContentId.Namespace in response", handler); + THROW_INVALID_RESPONSE_IF_NOT(contentId["Namespace"].is_string(), "ContentId.Namespace is not a string", handler); + tmp->contentId.nameSpace = contentId["Namespace"]; + + THROW_INVALID_RESPONSE_IF_NOT(contentId.contains("Name"), "Missing ContentId.Name in response", handler); + THROW_INVALID_RESPONSE_IF_NOT(contentId["Name"].is_string(), "ContentId.Name is not a string", handler); + tmp->contentId.name = contentId["Name"]; + + THROW_INVALID_RESPONSE_IF_NOT(contentId.contains("Version"), "Missing ContentId.Version in response", handler); + THROW_INVALID_RESPONSE_IF_NOT(contentId["Version"].is_string(), "ContentId.Version is not a string", handler); + tmp->contentId.version = contentId["Version"]; + + if (isAppEntity) + { + auto appEntity = dynamic_cast<AppVersionEntity*>(tmp.get()); + + THROW_INVALID_RESPONSE_IF_NOT(data["UpdateId"].is_string(), "UpdateId is not a string", handler); + appEntity->updateId = data["UpdateId"]; + + THROW_INVALID_RESPONSE_IF_NOT(data.contains("Prerequisites"), "Missing Prerequisites in response", handler); + THROW_INVALID_RESPONSE_IF_NOT(data["Prerequisites"].is_array(), "Prerequisites is not an array", handler); + + for (const auto& prereq : data["Prerequisites"]) + { + THROW_INVALID_RESPONSE_IF_NOT(prereq.is_object(), "Prerequisite element is not a JSON object", handler); + + GenericVersionEntity prereqEntity; + THROW_INVALID_RESPONSE_IF_NOT(prereq.contains("Namespace"), + "Missing Prerequisite.Namespace in response", + handler); + THROW_INVALID_RESPONSE_IF_NOT(prereq["Namespace"].is_string(), + "Prerequisite.Namespace is not a string", + handler); + prereqEntity.contentId.nameSpace = prereq["Namespace"]; + + THROW_INVALID_RESPONSE_IF_NOT(prereq.contains("Name"), "Missing Prerequisite.Name in response", handler); + THROW_INVALID_RESPONSE_IF_NOT(prereq["Name"].is_string(), "Prerequisite.Name is not a string", handler); + prereqEntity.contentId.name = prereq["Name"]; + + THROW_INVALID_RESPONSE_IF_NOT(prereq.contains("Version"), + "Missing Prerequisite.Version in response", + handler); + THROW_INVALID_RESPONSE_IF_NOT(prereq["Version"].is_string(), + "Prerequisite.Version is not a string", + handler); + prereqEntity.contentId.version = prereq["Version"]; + + appEntity->prerequisites.push_back(std::move(prereqEntity)); + } + } + return tmp; +} + +std::unique_ptr<ContentId> VersionEntity::ToContentId(VersionEntity&& entity, const ReportingHandler& handler) +{ + std::unique_ptr<ContentId> tmp; + THROW_IF_FAILED_LOG(ContentId::Make(std::move(entity.contentId.nameSpace), + std::move(entity.contentId.name), + std::move(entity.contentId.version), + tmp), + handler); + return tmp; +} + +ContentType GenericVersionEntity::GetContentType() const +{ + return ContentType::Generic; +} + +ContentType AppVersionEntity::GetContentType() const +{ + return ContentType::App; +} + +AppVersionEntity* AppVersionEntity::GetAppVersionEntityPtr(std::unique_ptr<VersionEntity>& versionEntity, + const ReportingHandler& handler) +{ + ValidateContentType(versionEntity->GetContentType(), ContentType::App, handler); + return dynamic_cast<AppVersionEntity*>(versionEntity.get()); +} diff --git a/src/SfsClient/sfs-client/client/src/details/entity/VersionEntity.h b/src/SfsClient/sfs-client/client/src/details/entity/VersionEntity.h @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "ContentType.h" + +#include <memory> +#include <string> +#include <vector> + +#include <nlohmann/json_fwd.hpp> + +namespace SFS +{ +class ContentId; + +namespace details +{ +class ReportingHandler; + +struct ContentIdEntity +{ + std::string nameSpace; + std::string name; + std::string version; +}; + +struct VersionEntity +{ + virtual ~VersionEntity() + { + } + + virtual ContentType GetContentType() const = 0; + + ContentIdEntity contentId; + + static std::unique_ptr<VersionEntity> FromJson(const nlohmann::json& data, const ReportingHandler& handler); + static std::unique_ptr<ContentId> ToContentId(VersionEntity&& entity, const ReportingHandler& handler); +}; + +struct GenericVersionEntity : public VersionEntity +{ + ContentType GetContentType() const override; +}; + +struct AppVersionEntity : public VersionEntity +{ + ContentType GetContentType() const override; + + std::string updateId; + std::vector<GenericVersionEntity> prerequisites; + + static AppVersionEntity* GetAppVersionEntityPtr(std::unique_ptr<VersionEntity>& versionEntity, + const ReportingHandler& handler); +}; + +using VersionEntities = std::vector<std::unique_ptr<VersionEntity>>; +} // namespace details +} // namespace SFS diff --git a/src/SfsClient/sfs-client/client/tests/CMakeLists.txt b/src/SfsClient/sfs-client/client/tests/CMakeLists.txt @@ -0,0 +1,62 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +find_package(Catch2 CONFIG REQUIRED) +include(Catch) + +# For mock http server +find_package(httplib CONFIG REQUIRED) + +project(SFSClientTests LANGUAGES CXX) + +add_executable(${PROJECT_NAME}) + +target_sources( + ${PROJECT_NAME} + PRIVATE functional/details/CurlConnectionTests.cpp + functional/details/SFSClientImplTests.cpp + functional/SFSClientTests.cpp + mock/MockWebServer.cpp + unit/AppContentTests.cpp + unit/AppFileTests.cpp + unit/ApplicabilityDetailsTests.cpp + unit/ContentIdTests.cpp + unit/ContentTests.cpp + unit/details/CurlConnectionManagerTests.cpp + unit/details/CurlConnectionTests.cpp + unit/details/entity/FileEntityTests.cpp + unit/details/entity/VersionEntityTests.cpp + unit/details/EnvTests.cpp + unit/details/ErrorHandlingTests.cpp + unit/details/ReportingHandlerTests.cpp + unit/details/SFSClientImplTests.cpp + unit/details/SFSUrlBuilderTests.cpp + unit/details/TestOverrideTests.cpp + unit/details/UrlBuilderTests.cpp + unit/details/UtilTests.cpp + unit/FileTests.cpp + unit/ResultTests.cpp + unit/SFSClientTests.cpp + util/SFSExceptionMatcher.cpp + util/TestHelper.cpp) + +set(SFS_CLIENT_LIB_NAME "Microsoft::${CMAKE_PROJECT_NAME}") +add_dependencies(${PROJECT_NAME} ${SFS_CLIENT_LIB_NAME}) +target_link_libraries(${PROJECT_NAME} PRIVATE ${SFS_CLIENT_LIB_NAME}) +target_link_libraries(${PROJECT_NAME} PRIVATE Catch2::Catch2WithMain) +target_link_libraries(${PROJECT_NAME} PRIVATE httplib::httplib) + +target_include_directories(${PROJECT_NAME} PUBLIC ../include) +target_include_directories(${PROJECT_NAME} PUBLIC ../src/details) + +catch_discover_tests(${PROJECT_NAME}) + +set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY + ${CMAKE_BINARY_DIR}/tests/bin/) + +set_compile_options_for_target(${PROJECT_NAME}) + +if(SFS_ENABLE_TEST_OVERRIDES) + target_compile_definitions(${PROJECT_NAME} + PRIVATE SFS_ENABLE_TEST_OVERRIDES=1) +endif() diff --git a/src/SfsClient/sfs-client/client/tests/functional/SFSClientTests.cpp b/src/SfsClient/sfs-client/client/tests/functional/SFSClientTests.cpp @@ -0,0 +1,440 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "../mock/MockWebServer.h" +#include "../util/TestHelper.h" +#include "TestOverride.h" +#include "sfsclient/SFSClient.h" + +#include <catch2/catch_test_macros.hpp> + +#include <chrono> + +#define TEST(...) TEST_CASE("[Functional][SFSClientTests] " __VA_ARGS__) + +using namespace SFS; +using namespace SFS::test; +using namespace std::chrono; + +const std::string c_instanceId = "testInstanceId"; +const std::string c_namespace = "testNamespace"; +const std::string c_productName = "testProduct"; +const std::string c_version = "0.0.1"; +const std::string c_nextVersion = "0.0.2"; + +namespace +{ +void CheckContentId(const ContentId& contentId, const std::string& name, const std::string& version) +{ + REQUIRE(contentId.GetNameSpace() == c_namespace); + REQUIRE(contentId.GetName() == name); + REQUIRE(contentId.GetVersion() == version); +} + +void CheckFiles(const std::vector<File>& files) +{ + REQUIRE(files.size() == 2); + REQUIRE(files[0].GetFileId() == (c_productName + ".json")); + REQUIRE(files[0].GetUrl() == ("http://localhost/1.json")); + REQUIRE(files[1].GetFileId() == (c_productName + ".bin")); + REQUIRE(files[1].GetUrl() == ("http://localhost/2.bin")); +} + +void CheckAppFiles(const std::vector<AppFile>& files, const std::string& name) +{ + REQUIRE(files.size() == 2); + REQUIRE(files[0].GetFileId() == (name + ".json")); + REQUIRE(files[0].GetUrl() == ("http://localhost/1.json")); + REQUIRE_FALSE(files[0].GetFileMoniker().empty()); + REQUIRE_FALSE(files[0].GetApplicabilityDetails().GetArchitectures().empty()); + REQUIRE_FALSE(files[0].GetApplicabilityDetails().GetPlatformApplicabilityForPackage().empty()); + REQUIRE(files[1].GetFileId() == (name + ".bin")); + REQUIRE(files[1].GetUrl() == ("http://localhost/2.bin")); + REQUIRE_FALSE(files[1].GetFileMoniker().empty()); + REQUIRE_FALSE(files[1].GetApplicabilityDetails().GetArchitectures().empty()); + REQUIRE_FALSE(files[1].GetApplicabilityDetails().GetPlatformApplicabilityForPackage().empty()); +} + +void CheckMockContent(const Content& content, const std::string& version) +{ + CheckContentId(content.GetContentId(), c_productName, version); + CheckFiles(content.GetFiles()); +} + +void CheckMockAppContent(const AppContent& content, + const std::string& version, + const std::vector<MockPrerequisite> mockPrereqs) +{ + CheckContentId(content.GetContentId(), c_productName, version); + REQUIRE_FALSE(content.GetUpdateId().empty()); + CheckAppFiles(content.GetFiles(), c_productName); + + REQUIRE(content.GetPrerequisites().size() == mockPrereqs.size()); + for (size_t i = 0; i < mockPrereqs.size(); ++i) + { + const auto& prereq = content.GetPrerequisites()[i]; + const auto& mockPrereq = mockPrereqs[i]; + CheckContentId(prereq.GetContentId(), mockPrereq.name, mockPrereq.version); + + CheckAppFiles(prereq.GetFiles(), mockPrereq.name); + } +} +} // namespace + +TEST("Testing SFSClient::GetLatestDownloadInfo()") +{ + if (!AreTestOverridesAllowed()) + { + return; + } + + test::MockWebServer server; + ScopedTestOverride override(TestOverride::BaseUrl, server.GetBaseUrl()); + + std::unique_ptr<SFSClient> sfsClient; + REQUIRE(SFSClient::Make({"testAccountId", c_instanceId, c_namespace, LogCallbackToTest}, sfsClient) == + Result::Success); + REQUIRE(sfsClient != nullptr); + + server.RegisterProduct(c_productName, c_version); + + std::vector<Content> contents; + + SECTION("Single product request") + { + RequestParams params; + params.baseCV = "aaaaaaaaaaaaaaaa.1"; + SECTION("No attributes") + { + params.productRequests = {{c_productName, {}}}; + REQUIRE(sfsClient->GetLatestDownloadInfo(params, contents) == Result::Success); + REQUIRE(contents.size() == 1); + CheckMockContent(contents[0], c_version); + } + + SECTION("With attributes") + { + const TargetingAttributes attributes{{"attr1", "value"}}; + params.productRequests = {{c_productName, attributes}}; + REQUIRE(sfsClient->GetLatestDownloadInfo(params, contents) == Result::Success); + REQUIRE(contents.size() == 1); + CheckMockContent(contents[0], c_version); + } + + SECTION("Wrong product name") + { + params.productRequests = {{"badName", {}}}; + REQUIRE(sfsClient->GetLatestDownloadInfo(params, contents) == Result::HttpNotFound); + REQUIRE(contents.empty()); + + const TargetingAttributes attributes{{"attr1", "value"}}; + params.productRequests = {{"badName", attributes}}; + REQUIRE(sfsClient->GetLatestDownloadInfo(params, contents) == Result::HttpNotFound); + REQUIRE(contents.empty()); + } + + SECTION("Adding new version") + { + server.RegisterProduct(c_productName, c_nextVersion); + + params.productRequests = {{c_productName, {}}}; + REQUIRE(sfsClient->GetLatestDownloadInfo(params, contents) == Result::Success); + REQUIRE(contents.size() == 1); + CheckMockContent(contents[0], c_nextVersion); + } + } +} + +TEST("Testing SFSClient::GetLatestAppDownloadInfo()") +{ + if (!AreTestOverridesAllowed()) + { + return; + } + + test::MockWebServer server; + ScopedTestOverride override(TestOverride::BaseUrl, server.GetBaseUrl()); + + const std::string prereq1 = "prereq1"; + const std::string prereq1Version = "1.0"; + const std::string prereq2 = "prereq2"; + const std::string prereq2Version = "2.0"; + + std::unique_ptr<SFSClient> sfsClient; + REQUIRE(SFSClient::Make({"testAccountId", "storeapps", c_namespace, LogCallbackToTest}, sfsClient) == + Result::Success); + REQUIRE(sfsClient != nullptr); + + SECTION("App products") + { + auto CheckApp = [&](const std::vector<MockPrerequisite>& mockPrereqs) { + server.RegisterAppProduct(c_productName, c_version, mockPrereqs); + + std::vector<AppContent> contents; + + RequestParams params; + params.baseCV = "aaaaaaaaaaaaaaaa.1"; + SECTION("No attributes") + { + params.productRequests = {{c_productName, {}}}; + REQUIRE(sfsClient->GetLatestAppDownloadInfo(params, contents) == Result::Success); + REQUIRE(contents.size() == 1); + CheckMockAppContent(contents[0], c_version, mockPrereqs); + } + + SECTION("With attributes") + { + const TargetingAttributes attributes{{"attr1", "value"}}; + params.productRequests = {{c_productName, attributes}}; + REQUIRE(sfsClient->GetLatestAppDownloadInfo(params, contents) == Result::Success); + REQUIRE(contents.size() == 1); + CheckMockAppContent(contents[0], c_version, mockPrereqs); + } + + SECTION("Wrong product name") + { + params.productRequests = {{"badName", {}}}; + REQUIRE(sfsClient->GetLatestAppDownloadInfo(params, contents) == Result::HttpNotFound); + REQUIRE(contents.empty()); + + const TargetingAttributes attributes{{"attr1", "value"}}; + params.productRequests = {{"badName", attributes}}; + REQUIRE(sfsClient->GetLatestAppDownloadInfo(params, contents) == Result::HttpNotFound); + REQUIRE(contents.empty()); + } + + SECTION("Adding new version") + { + server.RegisterAppProduct(c_productName, c_nextVersion, mockPrereqs); + + params.productRequests = {{c_productName, {}}}; + REQUIRE(sfsClient->GetLatestAppDownloadInfo(params, contents) == Result::Success); + REQUIRE(contents.size() == 1); + CheckMockAppContent(contents[0], c_nextVersion, mockPrereqs); + } + }; + + SECTION("No prerequisites") + { + CheckApp({}); + } + + SECTION("1 prereq") + { + CheckApp({{prereq1, prereq1Version}}); + } + + SECTION("2 prereqs") + { + CheckApp({{prereq1, prereq1Version}, {prereq2, prereq2Version}}); + } + } + + SECTION("Non-app products") + { + server.RegisterProduct(c_productName, c_version); + + std::vector<AppContent> contents; + + RequestParams params; + params.productRequests = {{c_productName, {}}}; + auto result = sfsClient->GetLatestAppDownloadInfo(params, contents); + REQUIRE(result.GetCode() == Result::ServiceUnexpectedContentType); + REQUIRE(result.GetMsg() == + "Unexpected content type [Generic] returned by the service does not match the expected [App]"); + REQUIRE(contents.empty()); + } +} + +TEST("Testing SFSClient retry behavior") +{ + if (!AreTestOverridesAllowed()) + { + INFO("Skipping. Test overrides not enabled"); + return; + } + + MockWebServer server; + ScopedTestOverride urlOverride(TestOverride::BaseUrl, server.GetBaseUrl()); + + server.RegisterProduct(c_productName, c_version); + RequestParams params; + params.productRequests = {{c_productName, {}}}; + std::vector<Content> contents; + + std::unique_ptr<SFSClient> sfsClient; + ClientConfig clientConfig{"testAccountId", c_instanceId, c_namespace, LogCallbackToTest}; + + SECTION("Test exponential backoff") + { + INFO("Sets the retry delay to 50ms to speed up the test"); + ScopedTestOverride override(TestOverride::BaseRetryDelayMs, 50); + + REQUIRE(SFSClient::Make(clientConfig, sfsClient)); + REQUIRE(sfsClient != nullptr); + + const int retriableError = 503; // ServerBusy + + auto RunTimedGet = [&](bool success = true) -> long long { + auto begin = steady_clock::now(); + if (success) + { + REQUIRE(sfsClient->GetLatestDownloadInfo(params, contents)); + REQUIRE(contents.size() == 1); + } + else + { + REQUIRE(sfsClient->GetLatestDownloadInfo(params, contents) == Result::HttpServiceNotAvailable); + REQUIRE(contents.empty()); + } + auto end = steady_clock::now(); + return duration_cast<milliseconds>(end - begin).count(); + }; + + long long allowedTimeDeviation = 200LL; + std::queue<HttpCode> forcedHttpErrors({retriableError}); + SECTION("Should take at least 50ms with a single retriable error") + { + server.SetForcedHttpErrors(forcedHttpErrors); + const auto time = RunTimedGet(); + REQUIRE(time >= 50LL); + REQUIRE(time < 50LL + allowedTimeDeviation); + } + + forcedHttpErrors.push(retriableError); + SECTION("Should take at least 150ms (50ms + 2*50ms) with two retriable errors") + { + server.SetForcedHttpErrors(forcedHttpErrors); + const auto time = RunTimedGet(); + REQUIRE(time >= 150LL); + REQUIRE(time < 150LL + allowedTimeDeviation); + } + + forcedHttpErrors.push(retriableError); + SECTION("Should take at least 300ms (50ms + 2*50ms + 3*50ms) with three retriable errors") + { + server.SetForcedHttpErrors(forcedHttpErrors); + const auto time = RunTimedGet(); + REQUIRE(time >= 300LL); + REQUIRE(time < 300LL + allowedTimeDeviation); + } + + forcedHttpErrors.push(retriableError); + SECTION("Should take at least 300ms (50ms + 2*50ms + 3*50ms) with four retriable errors, but fail") + { + server.SetForcedHttpErrors(forcedHttpErrors); + const auto time = RunTimedGet(false /*success*/); + REQUIRE(time >= 300LL); + REQUIRE(time < 300LL + allowedTimeDeviation); + } + } + + SECTION("Test retriable errors with Retry-After headers") + { + INFO("Sets the retry delay to 200ms to speed up the test"); + ScopedTestOverride override(TestOverride::BaseRetryDelayMs, 200); + + REQUIRE(SFSClient::Make(clientConfig, sfsClient)); + REQUIRE(sfsClient != nullptr); + + const int retriableError = 503; // ServerBusy + const int retriableError2 = 502; // BadGateway + + auto RunTimedGet = [&]() -> long long { + auto begin = steady_clock::now(); + REQUIRE(sfsClient->GetLatestDownloadInfo(params, contents)); + REQUIRE(contents.size() == 1); + auto end = steady_clock::now(); + return duration_cast<milliseconds>(end - begin).count(); + }; + + std::unordered_map<HttpCode, HeaderMap> headersByCode; + headersByCode[retriableError] = {{"Retry-After", "1"}}; // 1s delay + server.SetResponseHeaders(headersByCode); + + long long allowedTimeDeviation = 200LL; + std::queue<HttpCode> forcedHttpErrors({retriableError}); + SECTION("Should take at least 1000ms with a single retriable error with 1s in Retry-After") + { + server.SetForcedHttpErrors(forcedHttpErrors); + const auto time = RunTimedGet(); + REQUIRE(time >= 1000LL); + REQUIRE(time < 1000LL + allowedTimeDeviation); + } + + SECTION( + "Should take at least 1000ms + 200ms with a retriable error with 1s in Retry-After and one with 200ms*2 as default value") + { + forcedHttpErrors.push(retriableError2); + server.SetForcedHttpErrors(forcedHttpErrors); + const auto time = RunTimedGet(); + REQUIRE(time >= 1400LL); + REQUIRE(time < 1400LL + allowedTimeDeviation); + } + } + + SECTION("Test maxRetries") + { + INFO("Sets the retry delay to 1ms to speed up the test"); + ScopedTestOverride override(TestOverride::BaseRetryDelayMs, 1); + + const int retriableError = 503; // ServerBusy + + SECTION("With default retries") + { + REQUIRE(SFSClient::Make(clientConfig, sfsClient)); + REQUIRE(sfsClient != nullptr); + + SECTION("Should pass with 3 errors") + { + server.SetForcedHttpErrors(std::queue<HttpCode>({retriableError, retriableError, retriableError})); + REQUIRE(sfsClient->GetLatestDownloadInfo(params, contents)); + } + + SECTION("Should fail with 4 errors") + { + server.SetForcedHttpErrors( + std::queue<HttpCode>({retriableError, retriableError, retriableError, retriableError})); + REQUIRE(sfsClient->GetLatestDownloadInfo(params, contents) == Result::HttpServiceNotAvailable); + } + } + + SECTION("Forcing retries to 3") + { + params.retryOnError = true; + REQUIRE(SFSClient::Make(clientConfig, sfsClient)); + REQUIRE(sfsClient != nullptr); + + SECTION("Should pass with 3 errors") + { + server.SetForcedHttpErrors(std::queue<HttpCode>({retriableError, retriableError, retriableError})); + REQUIRE(sfsClient->GetLatestDownloadInfo(params, contents)); + } + + SECTION("Should fail with 4 errors") + { + server.SetForcedHttpErrors( + std::queue<HttpCode>({retriableError, retriableError, retriableError, retriableError})); + REQUIRE(sfsClient->GetLatestDownloadInfo(params, contents) == Result::HttpServiceNotAvailable); + } + } + + SECTION("Reducing retries to 0") + { + params.retryOnError = false; + REQUIRE(SFSClient::Make(clientConfig, sfsClient)); + REQUIRE(sfsClient != nullptr); + + SECTION("Should pass with no errors") + { + REQUIRE(sfsClient->GetLatestDownloadInfo(params, contents)); + } + + SECTION("Should fail with 1 error") + { + server.SetForcedHttpErrors(std::queue<HttpCode>({retriableError})); + REQUIRE(sfsClient->GetLatestDownloadInfo(params, contents) == Result::HttpServiceNotAvailable); + } + } + } +} diff --git a/src/SfsClient/sfs-client/client/tests/functional/details/CurlConnectionTests.cpp b/src/SfsClient/sfs-client/client/tests/functional/details/CurlConnectionTests.cpp @@ -0,0 +1,554 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "../../mock/MockWebServer.h" +#include "../../util/SFSExceptionMatcher.h" +#include "../../util/TestHelper.h" +#include "ReportingHandler.h" +#include "SFSUrlBuilder.h" +#include "TestOverride.h" +#include "connection/CurlConnection.h" +#include "connection/CurlConnectionManager.h" +#include "connection/HttpHeader.h" + +#include <catch2/catch_test_macros.hpp> +#include <catch2/matchers/catch_matchers_string.hpp> +#include <curl/curl.h> +#include <nlohmann/json.hpp> + +#include <chrono> +#include <sstream> + +#define TEST(...) TEST_CASE("[Functional][CurlConnectionTests] " __VA_ARGS__) + +using namespace SFS; +using namespace SFS::details; +using namespace SFS::test; +using namespace std::chrono; +using json = nlohmann::json; + +const std::string c_instanceId = "default"; +const std::string c_namespace = "default"; +const std::string c_productName = "testProduct"; +const std::string c_version = "0.0.1"; +const std::string c_nextVersion = "0.0.2"; + +namespace +{ +class CurlConnectionTimeout : public CurlConnection +{ + public: + CurlConnectionTimeout(const ConnectionConfig& config, const ReportingHandler& handler) + : CurlConnection(config, handler) + { + } + + std::string Get(const std::string& url) override + { + // Timeout within 100ms + curl_easy_setopt(m_handle, CURLOPT_TIMEOUT_MS, 100L); + return CurlConnection::Get(url); + } + + std::string Post(const std::string& url, const std::string& data) override + { + // Timeout within 100ms + curl_easy_setopt(m_handle, CURLOPT_TIMEOUT_MS, 100L); + return CurlConnection::Post(url, data); + } +}; + +class CurlConnectionTimeoutManager : public CurlConnectionManager +{ + public: + CurlConnectionTimeoutManager(const ReportingHandler& handler) : CurlConnectionManager(handler) + { + } + + std::unique_ptr<Connection> MakeConnection(const ConnectionConfig& config) override + { + return std::make_unique<CurlConnectionTimeout>(config, m_handler); + } +}; + +std::string TimestampToHttpDateString(std::chrono::time_point<std::chrono::system_clock> time) +{ + auto timer = system_clock::to_time_t(time); + + std::stringstream timeStream; + struct tm gmTime; +#ifdef _WIN32 + gmtime_s(&gmTime, &timer); // gmtime_s is the safe version of gmtime, not available on Linux +#else + gmTime = (*std::gmtime(&timer)); +#endif + timeStream << std::put_time(&gmTime, "%a, %d %b %Y %X GMT"); // day, dd Mmm yyyy HH:MM:SS + + return timeStream.str(); +} +} // namespace + +TEST("Testing CurlConnection()") +{ + test::MockWebServer server; + ReportingHandler handler; + handler.SetLoggingCallback(LogCallbackToTest); + CurlConnectionManager connectionManager(handler); + auto connection = connectionManager.MakeConnection({}); + SFSUrlBuilder urlBuilder(SFSCustomUrl(server.GetBaseUrl()), c_instanceId, c_namespace, handler); + server.RegisterExpectedRequestHeader(HttpHeader::UserAgent, GetUserAgentValue()); + + SECTION("Testing CurlConnection::Get()") + { + const std::string url = urlBuilder.GetSpecificVersionUrl(c_productName, c_version); + + // Before registering the product, the URL returns 404 Not Found + REQUIRE_THROWS_CODE(connection->Get(url), HttpNotFound); + + // Register the product + server.RegisterProduct(c_productName, c_version); + + // After registering the product, the URL returns 200 OK + std::string out; + REQUIRE_NOTHROW(out = connection->Get(url)); + + json expectedResponse; + expectedResponse["ContentId"] = {{"Namespace", "default"}, {"Name", c_productName}, {"Version", c_version}}; + + REQUIRE(json::parse(out) == expectedResponse); + } + + SECTION("Testing CurlConnection::Post()") + { + server.RegisterExpectedRequestHeader(HttpHeader::ContentType, "application/json"); + + SECTION("With GetLatestVersionBatch mock") + { + const std::string url = urlBuilder.GetLatestVersionBatchUrl(); + + // Missing proper body returns HttpBadRequest + REQUIRE_THROWS_CODE(connection->Post(url), HttpBadRequest); + + // Before registering the product, the URL returns 404 Not Found + const json body = {{{"TargetingAttributes", {}}, {"Product", c_productName}}}; + REQUIRE_THROWS_CODE(connection->Post(url, body.dump()), HttpNotFound); + + // Register the product + server.RegisterProduct(c_productName, c_version); + + // After registering the product, the URL returns 200 OK + std::string out; + REQUIRE_NOTHROW(out = connection->Post(url, body.dump())); + + json expectedResponse = json::array(); + expectedResponse.push_back( + {{"ContentId", {{"Namespace", "default"}, {"Name", c_productName}, {"Version", c_version}}}}); + REQUIRE(json::parse(out) == expectedResponse); + + // Returns the next version now + server.RegisterProduct(c_productName, c_nextVersion); + REQUIRE_NOTHROW(out = connection->Post(url, body.dump())); + + expectedResponse = json::array(); + expectedResponse.push_back( + {{"ContentId", {{"Namespace", "default"}, {"Name", c_productName}, {"Version", c_nextVersion}}}}); + REQUIRE(json::parse(out) == expectedResponse); + } + + SECTION("With GetDownloadInfo mock") + { + const std::string url = urlBuilder.GetDownloadInfoUrl(c_productName, c_version); + + // Before registering the product, the URL returns 404 Not Found + REQUIRE_THROWS_CODE(connection->Post(url), HttpNotFound); + + // Register the product + server.RegisterProduct(c_productName, c_version); + + // After registering the product, the URL returns 200 OK + std::string out; + REQUIRE_NOTHROW(out = connection->Post(url)); + + json expectedResponse = json::array(); + expectedResponse.push_back({{"Url", "http://localhost/1.json"}, + {"FileId", c_productName + ".json"}, + {"SizeInBytes", 100}, + {"Hashes", {{"Sha1", "123"}, {"Sha256", "456"}}}}); + expectedResponse[0]["DeliveryOptimization"] = {{"CatalogId", "789"}}; + expectedResponse[0]["DeliveryOptimization"]["Properties"] = { + {"IntegrityCheckInfo", {{"PiecesHashFileUrl", "http://localhost/1.json"}, {"HashOfHashes", "abc"}}}}; + + expectedResponse.push_back({{"Url", "http://localhost/2.bin"}, + {"FileId", c_productName + ".bin"}, + {"SizeInBytes", 200}, + {"Hashes", {{"Sha1", "421"}, {"Sha256", "132"}}}}); + expectedResponse[1]["DeliveryOptimization"] = {{"CatalogId", "14"}}; + expectedResponse[1]["DeliveryOptimization"]["Properties"] = { + {"IntegrityCheckInfo", {{"PiecesHashFileUrl", "http://localhost/2.bin"}, {"HashOfHashes", "abcd"}}}}; + + REQUIRE(json::parse(out) == expectedResponse); + } + } + + SECTION("Testing with a malformed url") + { + std::string url = server.GetBaseUrl(); + REQUIRE_THROWS_CODE(connection->Get(url), HttpNotFound); + REQUIRE_THROWS_CODE(connection->Get(url + "/names"), HttpNotFound); + REQUIRE_THROWS_CODE(connection->Post(url + "/names", "{}"), HttpNotFound); + + url = server.GetBaseUrl() + "/api/v1/contents/" + c_instanceId + "/namespaces/" + c_namespace + "/names/" + + c_productName + "/versYions/" + c_version + "/files?action=GenerateDownloadInfo"; + + std::string out; + REQUIRE_THROWS_CODE(connection->Get(url), HttpNotFound); + REQUIRE_THROWS_CODE(connection->Post(url, std::string()), HttpNotFound); + REQUIRE_THROWS_CODE(out = connection->Post(url, {}), HttpNotFound); + CHECK(out.empty()); + } + + REQUIRE(server.Stop() == Result::Success); +} + +TEST("Testing CurlConnection when the server is not reachable") +{ + // Using a custom override class just to time out faster on an invalid URL + ReportingHandler handler; + CurlConnectionTimeoutManager connectionManager(handler); + handler.SetLoggingCallback(LogCallbackToTest); + auto connection = connectionManager.MakeConnection({}); + + // Using a non-routable IP address to ensure the server is not reachable + // https://www.rfc-editor.org/rfc/rfc5737#section-3: The blocks 192.0.2.0/24 (...) are provided for use in + // documentation. + std::string url = "192.0.2.0"; + std::string out; + REQUIRE_THROWS_CODE(connection->Get(url), HttpTimeout); + REQUIRE_THROWS_CODE(connection->Post(url + "/names", "{}"), HttpTimeout); + + url = "192.0.2.0/files?action=GenerateDownloadInfo"; + + REQUIRE_THROWS_CODE_MSG_MATCHES(connection->Get(url), + HttpTimeout, + Catch::Matchers::ContainsSubstring("timed out after")); + REQUIRE_THROWS_CODE_MSG_MATCHES(connection->Post(url, {}), + HttpTimeout, + Catch::Matchers::ContainsSubstring("timed out after")); +} + +TEST("Testing CurlConnection works from a second ConnectionManager") +{ + ReportingHandler handler; + + // Create a first connection manager and see it leave scope - curl initializes and uninitializes + { + CurlConnectionManager connectionManager(handler); + REQUIRE(connectionManager.MakeConnection({}) != nullptr); + } + + test::MockWebServer server; + CurlConnectionManager connectionManager(handler); + handler.SetLoggingCallback(LogCallbackToTest); + auto connection = connectionManager.MakeConnection({}); + SFSUrlBuilder urlBuilder(SFSCustomUrl(server.GetBaseUrl()), c_instanceId, c_namespace, handler); + + const std::string url = urlBuilder.GetSpecificVersionUrl(c_productName, c_version); + + // Register the product + server.RegisterProduct(c_productName, c_version); + + // After registering the product, the URL returns 200 OK + REQUIRE_NOTHROW(connection->Get(url)); + + SECTION("A second connection also works") + { + auto connection2 = connectionManager.MakeConnection({}); + REQUIRE_NOTHROW(connection2->Get(url)); + } +} + +TEST("Testing a url that's too big throws 414") +{ + ReportingHandler handler; + handler.SetLoggingCallback(LogCallbackToTest); + + test::MockWebServer server; + CurlConnectionManager connectionManager(handler); + auto connection = connectionManager.MakeConnection({}); + SFSUrlBuilder urlBuilder(SFSCustomUrl(server.GetBaseUrl()), c_instanceId, c_namespace, handler); + + // Will use a fake large product name to produce a large url + const std::string largeProductName(90000, 'a'); + + // Register the products + server.RegisterProduct(largeProductName, c_version); + + // Url produces: 414 URI Too Long + std::string out; + REQUIRE_THROWS_CODE_MSG(connection->Get(urlBuilder.GetSpecificVersionUrl(largeProductName, c_version)), + HttpUnexpected, + "Unexpected HTTP code 414"); +} + +TEST("Testing a response over the limit fails the operation") +{ + ReportingHandler handler; + handler.SetLoggingCallback(LogCallbackToTest); + + test::MockWebServer server; + CurlConnectionManager connectionManager(handler); + auto connection = connectionManager.MakeConnection({}); + SFSUrlBuilder urlBuilder(SFSCustomUrl(server.GetBaseUrl()), c_instanceId, c_namespace, handler); + + // Will use a fake large product name to produce a response over the limit of 100k characters + const std::string largeProductName(90000, 'a'); + const std::string overLimitProductName(1000000, 'a'); + + // Register the products + server.RegisterProduct(largeProductName, c_version); + server.RegisterProduct(overLimitProductName, c_version); + + // Using GetLatestVersionBatch api since the product name is in the body and not in the url, to avoid a 414 error + // like on the test above + const std::string url = urlBuilder.GetLatestVersionBatchUrl(); + + // Large one works + json body = {{{"TargetingAttributes", {}}, {"Product", largeProductName}}}; + REQUIRE_NOTHROW(connection->Post(url, body.dump())); + + // Over limit fails + body[0]["Product"] = overLimitProductName; + REQUIRE_THROWS_CODE_MSG(connection->Post(url, body.dump()), + ConnectionUnexpectedError, + "Failure writing output to destination"); +} + +TEST("Testing MS-CV is sent to server") +{ + test::MockWebServer server; + ReportingHandler handler; + handler.SetLoggingCallback(LogCallbackToTest); + CurlConnectionManager connectionManager(handler); + + const std::string cv = "aaaaaaaaaaaaaaaa.1"; + ConnectionConfig config; + config.baseCV = cv; + auto connection = connectionManager.MakeConnection(config); + SFSUrlBuilder urlBuilder(SFSCustomUrl(server.GetBaseUrl()), c_instanceId, c_namespace, handler); + + const std::string url = urlBuilder.GetSpecificVersionUrl(c_productName, c_version); + + server.RegisterProduct(c_productName, c_version); + + INFO("First value ends with .0"); + server.RegisterExpectedRequestHeader(HttpHeader::MSCV, cv + ".0"); + connection->Get(url); + + INFO("Value gets incremented on subsequent requests"); + server.RegisterExpectedRequestHeader(HttpHeader::MSCV, cv + ".1"); + connection->Get(url); + + server.RegisterExpectedRequestHeader(HttpHeader::MSCV, cv + ".2"); + connection->Get(url); +} + +TEST("Testing retry behavior") +{ + if (!AreTestOverridesAllowed()) + { + INFO("Skipping. Test overrides not enabled"); + return; + } + + ReportingHandler handler; + handler.SetLoggingCallback(LogCallbackToTest); + + MockWebServer server; + CurlConnectionManager connectionManager(handler); + SFSUrlBuilder urlBuilder(SFSCustomUrl(server.GetBaseUrl()), c_instanceId, c_namespace, handler); + + server.RegisterProduct(c_productName, c_version); + const std::string url = urlBuilder.GetSpecificVersionUrl(c_productName, c_version); + + SECTION("Test exponential backoff") + { + auto connection = connectionManager.MakeConnection({}); + + INFO("Sets the retry delay to 50ms to speed up the test"); + ScopedTestOverride override(TestOverride::BaseRetryDelayMs, 50); + + const int retriableError = 503; // ServerBusy + + auto RunTimedGet = [&](bool success = true) -> long long { + std::string out; + auto begin = steady_clock::now(); + if (success) + { + REQUIRE_NOTHROW(out = connection->Get(url)); + REQUIRE_FALSE(out.empty()); + } + else + { + REQUIRE_THROWS_CODE(out = connection->Get(url), HttpServiceNotAvailable); + REQUIRE(out.empty()); + } + auto end = steady_clock::now(); + return duration_cast<milliseconds>(end - begin).count(); + }; + + long long allowedTimeDeviation = 200LL; + std::queue<HttpCode> forcedHttpErrors({retriableError}); + SECTION("Should take at least 50ms with a single retriable error") + { + server.SetForcedHttpErrors(forcedHttpErrors); + const auto time = RunTimedGet(); + REQUIRE(time >= 50LL); + REQUIRE(time < 50LL + allowedTimeDeviation); + } + + forcedHttpErrors.push(retriableError); + SECTION("Should take at least 150ms (50ms + 2*50ms) with two retriable errors") + { + server.SetForcedHttpErrors(forcedHttpErrors); + const auto time = RunTimedGet(); + REQUIRE(time >= 150LL); + REQUIRE(time < 150LL + allowedTimeDeviation); + } + + forcedHttpErrors.push(retriableError); + SECTION("Should take at least 300ms (50ms + 2*50ms + 3*50ms) with three retriable errors") + { + server.SetForcedHttpErrors(forcedHttpErrors); + const auto time = RunTimedGet(); + REQUIRE(time >= 300LL); + REQUIRE(time < 300LL + allowedTimeDeviation); + } + + forcedHttpErrors.push(retriableError); + SECTION("Should take at least 300ms (50ms + 2*50ms + 3*50ms) with four retriable errors, but fail") + { + server.SetForcedHttpErrors(forcedHttpErrors); + const auto time = RunTimedGet(false /*success*/); + REQUIRE(time >= 300LL); + REQUIRE(time < 300LL + allowedTimeDeviation); + } + } + + SECTION("Test retriable errors with Retry-After headers") + { + auto connection = connectionManager.MakeConnection({}); + + INFO("Sets the retry delay to 200ms to speed up the test"); + ScopedTestOverride override(TestOverride::BaseRetryDelayMs, 200); + + const int retriableError = 503; // ServerBusy + const int retriableError2 = 502; // BadGateway + + auto RunTimedGet = [&]() -> long long { + std::string out; + auto begin = steady_clock::now(); + REQUIRE_NOTHROW(out = connection->Get(url)); + REQUIRE_FALSE(out.empty()); + auto end = steady_clock::now(); + return duration_cast<milliseconds>(end - begin).count(); + }; + + auto testForRetryAfterValue = [&](const std::string& retryAfterValue) -> void { + INFO("retryAfterValue = " << retryAfterValue); + + std::unordered_map<HttpCode, HeaderMap> headersByCode; + headersByCode[retriableError] = {{"Retry-After", retryAfterValue}}; + server.SetResponseHeaders(headersByCode); + + long long allowedTimeDeviation = 200LL; + std::queue<HttpCode> forcedHttpErrors({retriableError}); + SECTION("Should take at least 2000ms with a single retriable error, with 2s in Retry-After") + { + server.SetForcedHttpErrors(forcedHttpErrors); + const auto time = RunTimedGet(); + REQUIRE(time >= 2000LL); + REQUIRE(time < 2000LL + allowedTimeDeviation); + } + + SECTION( + "Should take at least 2000ms + 200ms*2 (exponential backoff) with two retriable errors, with 2s in Retry-After of the first error") + { + forcedHttpErrors.push(retriableError2); + server.SetForcedHttpErrors(forcedHttpErrors); + const auto time = RunTimedGet(); + REQUIRE(time >= 2400LL); + REQUIRE(time < 2400LL + allowedTimeDeviation); + } + }; + + SECTION("Using seconds") + { + testForRetryAfterValue("2"); // 2s delay + } + SECTION("Using date") + { + const auto time = std::chrono::system_clock::now() + std::chrono::seconds(2); + testForRetryAfterValue(TimestampToHttpDateString(time)); + } + } + + SECTION("Test maxRetries") + { + INFO("Sets the retry delay to 1ms to speed up the test"); + ScopedTestOverride override(TestOverride::BaseRetryDelayMs, 1); + + const int retriableError = 503; // ServerBusy + + SECTION("Should pass with 3 errors") + { + auto connection = connectionManager.MakeConnection({}); + server.SetForcedHttpErrors(std::queue<HttpCode>({retriableError, retriableError, retriableError})); + REQUIRE_NOTHROW(connection->Get(url)); + } + + SECTION("Should fail with 4 errors") + { + auto connection = connectionManager.MakeConnection({}); + server.SetForcedHttpErrors( + std::queue<HttpCode>({retriableError, retriableError, retriableError, retriableError})); + REQUIRE_THROWS_CODE(connection->Get(url), HttpServiceNotAvailable); + } + + SECTION("Reducing retries to 2") + { + ConnectionConfig config; + config.maxRetries = 2; + auto connection = connectionManager.MakeConnection(config); + + SECTION("Should pass with 2 errors") + { + server.SetForcedHttpErrors(std::queue<HttpCode>({retriableError, retriableError})); + REQUIRE_NOTHROW(connection->Get(url)); + } + + SECTION("Should fail with 3 errors") + { + server.SetForcedHttpErrors(std::queue<HttpCode>({retriableError, retriableError, retriableError})); + REQUIRE_THROWS_CODE(connection->Get(url), HttpServiceNotAvailable); + } + } + + SECTION("Reducing retries to 0") + { + ConnectionConfig config; + config.maxRetries = 0; + auto connection = connectionManager.MakeConnection(config); + + SECTION("Should pass with no errors") + { + REQUIRE_NOTHROW(connection->Get(url)); + } + + SECTION("Should fail with 1 error") + { + server.SetForcedHttpErrors(std::queue<HttpCode>({retriableError})); + REQUIRE_THROWS_CODE(connection->Get(url), HttpServiceNotAvailable); + } + } + } +} diff --git a/src/SfsClient/sfs-client/client/tests/functional/details/SFSClientImplTests.cpp b/src/SfsClient/sfs-client/client/tests/functional/details/SFSClientImplTests.cpp @@ -0,0 +1,358 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "../../mock/MockWebServer.h" +#include "../../util/SFSExceptionMatcher.h" +#include "../../util/TestHelper.h" +#include "SFSClientImpl.h" +#include "connection/Connection.h" +#include "connection/CurlConnectionManager.h" +#include "connection/HttpHeader.h" + +#include <catch2/catch_test_macros.hpp> + +#include <set> + +#define TEST(...) TEST_CASE("[Functional][SFSClientImplTests] " __VA_ARGS__) + +using namespace SFS; +using namespace SFS::details; +using namespace SFS::test; + +namespace +{ +void CheckProduct(const VersionEntity& entity, std::string_view ns, std::string_view name, std::string_view version) +{ + REQUIRE(entity.GetContentType() == ContentType::Generic); + REQUIRE(entity.contentId.nameSpace == ns); + REQUIRE(entity.contentId.name == name); + REQUIRE(entity.contentId.version == version); +} + +void CheckAppProduct(const VersionEntity& entity, std::string_view ns, std::string_view name, std::string_view version) +{ + REQUIRE(entity.GetContentType() == ContentType::App); + auto appEntity = dynamic_cast<const AppVersionEntity&>(entity); + REQUIRE(appEntity.contentId.nameSpace == ns); + REQUIRE(appEntity.contentId.name == name); + REQUIRE(appEntity.contentId.version == version); + REQUIRE_FALSE(appEntity.updateId.empty()); + REQUIRE(appEntity.prerequisites.empty()); +} + +void CheckProducts(const VersionEntities& entities, + std::string_view ns, + const std::set<std::pair<std::string, std::string>>& nameVersionPairs) +{ + // json arrays don't guarantee order, and they are the underlying structure, so we need to check using sets + std::set<std::pair<std::string, std::string>> uniqueNameVersionPairs; + for (const auto& entity : entities) + { + REQUIRE(entity->contentId.nameSpace == ns); + uniqueNameVersionPairs.emplace(entity->contentId.name, entity->contentId.version); + } + + for (const auto& nameVersionPair : nameVersionPairs) + { + REQUIRE(uniqueNameVersionPairs.count(nameVersionPair)); + } +} + +void CheckDownloadInfo(const FileEntities& files, const std::string& name) +{ + REQUIRE(files.size() == 2); + REQUIRE(files[0]->GetContentType() == ContentType::Generic); + REQUIRE(files[0]->fileId == (name + ".json")); + REQUIRE(files[0]->url == ("http://localhost/1.json")); + REQUIRE(files[1]->GetContentType() == ContentType::Generic); + REQUIRE(files[1]->fileId == (name + ".bin")); + REQUIRE(files[1]->url == ("http://localhost/2.bin")); +} + +void CheckAppDownloadInfo(const FileEntities& files, const std::string& name) +{ + REQUIRE(files.size() == 2); + REQUIRE(files[0]->GetContentType() == ContentType::App); + REQUIRE(files[0]->fileId == (name + ".json")); + REQUIRE(files[0]->url == ("http://localhost/1.json")); + REQUIRE(files[1]->GetContentType() == ContentType::App); + REQUIRE(files[1]->fileId == (name + ".bin")); + REQUIRE(files[1]->url == ("http://localhost/2.bin")); +} +} // namespace + +TEST("Testing class SFSClientImpl()") +{ + test::MockWebServer server; + const std::string ns = "testNameSpace"; + SFSClientImpl<CurlConnectionManager> sfsClient({"testAccountId", "testInstanceId", ns, LogCallbackToTest}); + sfsClient.SetCustomBaseUrl(server.GetBaseUrl()); + + const std::string cv = "aaaaaaaaaaaaaaaa.1"; + ConnectionConfig config; + config.baseCV = cv; + auto connection = sfsClient.MakeConnection(config); + server.RegisterExpectedRequestHeader(HttpHeader::UserAgent, GetUserAgentValue()); + + SECTION("Generic products") + { + server.RegisterProduct("productName", "0.0.0.2"); + server.RegisterProduct("productName", "0.0.0.1"); + + SECTION("Testing SFSClientImpl::GetLatestVersion()") + { + server.RegisterExpectedRequestHeader(HttpHeader::ContentType, "application/json"); + std::unique_ptr<VersionEntity> entity; + + SECTION("No attributes") + { + REQUIRE_NOTHROW(entity = sfsClient.GetLatestVersion({"productName", {}}, *connection)); + REQUIRE(entity); + CheckProduct(*entity, ns, "productName", "0.0.0.2"); + } + + SECTION("With attributes") + { + const TargetingAttributes attributes{{"attr1", "value"}}; + REQUIRE_NOTHROW(entity = sfsClient.GetLatestVersion({"productName", attributes}, *connection)); + REQUIRE(entity); + CheckProduct(*entity, ns, "productName", "0.0.0.2"); + } + + SECTION("Wrong product name") + { + REQUIRE_THROWS_CODE(entity = sfsClient.GetLatestVersion({"badName", {}}, *connection), HttpNotFound); + REQUIRE(!entity); + + const TargetingAttributes attributes{{"attr1", "value"}}; + REQUIRE_THROWS_CODE(entity = sfsClient.GetLatestVersion({"badName", attributes}, *connection), + HttpNotFound); + REQUIRE(!entity); + } + } + + SECTION("Testing SFSClientImpl::GetLatestVersionBatch()") + { + server.RegisterExpectedRequestHeader(HttpHeader::ContentType, "application/json"); + VersionEntities entities; + + SECTION("No attributes") + { + REQUIRE_NOTHROW(entities = sfsClient.GetLatestVersionBatch({{"productName", {}}}, *connection)); + REQUIRE(!entities.empty()); + CheckProduct(*entities[0], ns, "productName", "0.0.0.2"); + } + + SECTION("With attributes") + { + const TargetingAttributes attributes{{"attr1", "value"}}; + REQUIRE_NOTHROW(entities = sfsClient.GetLatestVersionBatch({{"productName", attributes}}, *connection)); + REQUIRE(!entities.empty()); + CheckProduct(*entities[0], ns, "productName", "0.0.0.2"); + } + + SECTION("Wrong product name") + { + REQUIRE_THROWS_CODE(entities = sfsClient.GetLatestVersionBatch({{"badName", {}}}, *connection), + HttpNotFound); + REQUIRE(entities.empty()); + + const TargetingAttributes attributes{{"attr1", "value"}}; + REQUIRE_THROWS_CODE(entities = sfsClient.GetLatestVersionBatch({{"badName", attributes}}, *connection), + HttpNotFound); + REQUIRE(entities.empty()); + } + + SECTION("Multiple unique products") + { + server.RegisterProduct("productName2", "0.0.0.3"); + + REQUIRE_NOTHROW(entities = sfsClient.GetLatestVersionBatch({{"productName", {}}, {"productName2", {}}}, + *connection)); + REQUIRE(entities.size() == 2); + CheckProducts(entities, ns, {{"productName", "0.0.0.2"}, {"productName2", "0.0.0.3"}}); + + server.RegisterProduct("productName3", "0.0.0.4"); + + REQUIRE_NOTHROW(entities = sfsClient.GetLatestVersionBatch( + {{"productName", {}}, {"productName2", {}}, {"productName3", {}}}, + *connection)); + REQUIRE(entities.size() == 3); + CheckProducts(entities, + ns, + {{"productName", "0.0.0.2"}, {"productName2", "0.0.0.3"}, {"productName3", "0.0.0.4"}}); + } + + SECTION("Multiple repeated products") + { + REQUIRE_NOTHROW(entities = sfsClient.GetLatestVersionBatch({{"productName", {}}, {"productName", {}}}, + *connection)); + REQUIRE(entities.size() == 1); + CheckProduct(*entities[0], ns, "productName", "0.0.0.2"); + + server.RegisterProduct("productName2", "0.0.0.3"); + + REQUIRE_NOTHROW( + entities = sfsClient.GetLatestVersionBatch( + {{"productName", {}}, {"productName", {}}, {"productName2", {}}, {"productName2", {}}}, + *connection)); + REQUIRE(entities.size() == 2); + CheckProducts(entities, ns, {{"productName", "0.0.0.2"}, {"productName2", "0.0.0.3"}}); + } + + SECTION("Multiple wrong products returns 404") + { + REQUIRE_THROWS_CODE( + entities = sfsClient.GetLatestVersionBatch({{"badName", {}}, {"badName2", {}}}, *connection), + HttpNotFound); + REQUIRE(entities.empty()); + } + + SECTION("Multiple products, one wrong returns 200") + { + REQUIRE_NOTHROW( + entities = sfsClient.GetLatestVersionBatch({{"productName", {}}, {"badName", {}}}, *connection)); + REQUIRE(entities.size() == 1); + CheckProduct(*entities[0], ns, "productName", "0.0.0.2"); + } + } + + SECTION("Testing SFSClientImpl::GetSpecificVersion()") + { + std::unique_ptr<VersionEntity> entity; + SECTION("Getting 0.0.0.1") + { + REQUIRE_NOTHROW(entity = sfsClient.GetSpecificVersion("productName", "0.0.0.1", *connection)); + REQUIRE(entity); + CheckProduct(*entity, ns, "productName", "0.0.0.1"); + } + + SECTION("Getting 0.0.0.2") + { + REQUIRE_NOTHROW(entity = sfsClient.GetSpecificVersion("productName", "0.0.0.2", *connection)); + REQUIRE(entity); + CheckProduct(*entity, ns, "productName", "0.0.0.2"); + } + + SECTION("Wrong product name") + { + REQUIRE_THROWS_CODE(entity = sfsClient.GetSpecificVersion("badName", "0.0.0.2", *connection), + HttpNotFound); + REQUIRE(!entity); + } + + SECTION("Wrong version") + { + REQUIRE_THROWS_CODE(entity = sfsClient.GetSpecificVersion("productName", "0.0.0.3", *connection), + HttpNotFound); + REQUIRE(!entity); + } + } + + SECTION("Testing SFSClientImpl::GetDownloadInfo()") + { + server.RegisterExpectedRequestHeader(HttpHeader::ContentType, "application/json"); + FileEntities files; + + SECTION("Getting 0.0.0.1") + { + REQUIRE_NOTHROW(files = sfsClient.GetDownloadInfo("productName", "0.0.0.1", *connection)); + REQUIRE(!files.empty()); + CheckDownloadInfo(files, "productName"); + } + + SECTION("Getting 0.0.0.2") + { + REQUIRE_NOTHROW(files = sfsClient.GetDownloadInfo("productName", "0.0.0.2", *connection)); + REQUIRE(!files.empty()); + CheckDownloadInfo(files, "productName"); + } + + SECTION("Wrong product name") + { + REQUIRE_THROWS_CODE(files = sfsClient.GetDownloadInfo("badName", "0.0.0.2", *connection), HttpNotFound); + REQUIRE(files.empty()); + } + + SECTION("Wrong version") + { + REQUIRE_THROWS_CODE(files = sfsClient.GetDownloadInfo("productName", "0.0.0.3", *connection), + HttpNotFound); + REQUIRE(files.empty()); + } + } + } + + SECTION("App products") + { + server.RegisterAppProduct("productName", "0.0.0.2", {}); + server.RegisterAppProduct("productName", "0.0.0.1", {}); + + SECTION("Testing SFSClientImpl::GetLatestVersion()") + { + server.RegisterExpectedRequestHeader(HttpHeader::ContentType, "application/json"); + std::unique_ptr<VersionEntity> entity; + + SECTION("No attributes") + { + REQUIRE_NOTHROW(entity = sfsClient.GetLatestVersion({"productName", {}}, *connection)); + REQUIRE(entity); + CheckAppProduct(*entity, ns, "productName", "0.0.0.2"); + } + + SECTION("With attributes") + { + const TargetingAttributes attributes{{"attr1", "value"}}; + REQUIRE_NOTHROW(entity = sfsClient.GetLatestVersion({"productName", attributes}, *connection)); + REQUIRE(entity); + CheckAppProduct(*entity, ns, "productName", "0.0.0.2"); + } + + SECTION("Wrong product name") + { + REQUIRE_THROWS_CODE(entity = sfsClient.GetLatestVersion({"badName", {}}, *connection), HttpNotFound); + REQUIRE(!entity); + + const TargetingAttributes attributes{{"attr1", "value"}}; + REQUIRE_THROWS_CODE(entity = sfsClient.GetLatestVersion({"badName", attributes}, *connection), + HttpNotFound); + REQUIRE(!entity); + } + } + + SECTION("Testing SFSClientImpl::GetDownloadInfo()") + { + server.RegisterExpectedRequestHeader(HttpHeader::ContentType, "application/json"); + FileEntities files; + + SECTION("Getting 0.0.0.1") + { + REQUIRE_NOTHROW(files = sfsClient.GetDownloadInfo("productName", "0.0.0.1", *connection)); + REQUIRE(!files.empty()); + CheckAppDownloadInfo(files, "productName"); + } + + SECTION("Getting 0.0.0.2") + { + REQUIRE_NOTHROW(files = sfsClient.GetDownloadInfo("productName", "0.0.0.2", *connection)); + REQUIRE(!files.empty()); + CheckAppDownloadInfo(files, "productName"); + } + + SECTION("Wrong product name") + { + REQUIRE_THROWS_CODE(files = sfsClient.GetDownloadInfo("badName", "0.0.0.2", *connection), HttpNotFound); + REQUIRE(files.empty()); + } + + SECTION("Wrong version") + { + REQUIRE_THROWS_CODE(files = sfsClient.GetDownloadInfo("productName", "0.0.0.3", *connection), + HttpNotFound); + REQUIRE(files.empty()); + } + } + } + + REQUIRE(server.Stop() == Result::Success); +} diff --git a/src/SfsClient/sfs-client/client/tests/mock/MockWebServer.cpp b/src/SfsClient/sfs-client/client/tests/mock/MockWebServer.cpp @@ -0,0 +1,835 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "MockWebServer.h" + +#include "../util/TestHelper.h" +#include "ErrorHandling.h" +#include "Util.h" +#include "connection/HttpHeader.h" + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include <windows.h> +#endif + +#include <httplib.h> +#include <nlohmann/json.hpp> + +#include <chrono> +#include <mutex> +#include <optional> +#include <thread> +#include <unordered_map> + +using namespace SFS; +using namespace SFS::details; +using namespace SFS::details::util; +using namespace SFS::test; +using namespace SFS::test::details; +using namespace std::string_literals; +using json = nlohmann::json; + +#define BUILD_BUFFERED_LOG_DATA(message) \ + BufferedLogData \ + { \ + message, __FILE__, __LINE__, __FUNCTION__, std::chrono::system_clock::now() \ + } + +#define BUFFER_LOG(message) BufferLog(BUILD_BUFFERED_LOG_DATA(message)) + +const char* c_listenHostName = "localhost"; + +namespace +{ +std::string ToString(httplib::StatusCode status) +{ + return std::to_string(status) + " " + std::string(httplib::status_message(status)); +} + +class StatusCodeException : public std::exception +{ + public: + StatusCodeException(httplib::StatusCode status) : m_status(status) + { + } + + const char* what() const noexcept override + { + return ToString(m_status).c_str(); + } + + httplib::StatusCode GetStatusCode() const + { + return m_status; + } + + private: + httplib::StatusCode m_status; +}; + +struct App +{ + std::string version; + std::vector<MockPrerequisite> prerequisites; +}; + +struct AppCmp +{ + bool operator()(App a, App b) const + { + return a.version < b.version; + } +}; + +json GenerateContentIdJsonObject(const std::string& name, const std::string& latestVersion, const std::string& ns) +{ + // { + // "ContentId": { + // "Namespace": <ns>, + // "Name": <name>, + // "Version": <version> + // } + // } + + return {{"ContentId", {{"Namespace", ns}, {"Name", name}, {"Version", latestVersion}}}}; +} + +json GenerateGetAppVersionJsonObject(const std::string& name, const App& app, const std::string& ns) +{ + // { + // "ContentId": { + // "Namespace": <ns>, + // "Name": <name>, + // "Version": <version> + // }, + // "UpdateId": "<id>", + // "Prerequisites": [ + // { + // "Namespace": "<ns>", + // "Name": "<name>", + // "Version": "<version>" + // } + // ] + // } + + json prereqs = json::array(); + for (const auto& prereq : app.prerequisites) + { + prereqs.push_back({{"Namespace", ns}, {"Name", prereq.name}, {"Version", prereq.version}}); + } + return {{"ContentId", {{"Namespace", ns}, {"Name", name}, {"Version", app.version}}}, + {"UpdateId", "123"}, + {"Prerequisites", prereqs}}; +} + +json GeneratePostDownloadInfo(const std::string& name) +{ + // [ + // { + // "Url": <url>, + // "FileId": <fileid>, + // "SizeInBytes": <size>, + // "Hashes": { + // "Sha1": <sha1>, + // "Sha256": <sha2> + // }, + // "DeliveryOptimization": { + // "CatalogId": <catalogid>, + // "Properties": { + // "IntegrityCheckInfo": { + // "PiecesHashFileUrl": <url>, + // "HashOfHashes": <hash> + // } + // } + // } + // }, + // ... + // ] + + // Generating DeliveryOptimizationData to simulate the server response, but it's not being parsed by the Client + + json response; + response = json::array(); + response.push_back({{"Url", "http://localhost/1.json"}, + {"FileId", name + ".json"}, + {"SizeInBytes", 100}, + {"Hashes", {{"Sha1", "123"}, {"Sha256", "456"}}}}); + response[0]["DeliveryOptimization"] = {{"CatalogId", "789"}}; + response[0]["DeliveryOptimization"]["Properties"] = { + {"IntegrityCheckInfo", {{"PiecesHashFileUrl", "http://localhost/1.json"}, {"HashOfHashes", "abc"}}}}; + + response.push_back({{"Url", "http://localhost/2.bin"}, + {"FileId", name + ".bin"}, + {"SizeInBytes", 200}, + {"Hashes", {{"Sha1", "421"}, {"Sha256", "132"}}}}); + response[1]["DeliveryOptimization"] = {{"CatalogId", "14"}}; + response[1]["DeliveryOptimization"]["Properties"] = { + {"IntegrityCheckInfo", {{"PiecesHashFileUrl", "http://localhost/2.bin"}, {"HashOfHashes", "abcd"}}}}; + return response; +} + +json GeneratePostAppDownloadInfo(const std::string& name) +{ + // [ + // { + // "Url": <url>, + // "FileId": <fileid>, + // "SizeInBytes": <size>, + // "Hashes": { + // "Sha1": <sha1>, + // "Sha256": <sha2> + // }, + // "DeliveryOptimization": { + // "CatalogId": <catalogid>, + // "Properties": { + // "IntegrityCheckInfo": { + // "PiecesHashFileUrl": <url>, + // "HashOfHashes": <hash> + // } + // } + // }, + // "ApplicabilityDetails": { + // "Architectures": [ + // "<arch>" + // ], + // "PlatformApplicabilityForPackage": [ + // "<app>" + // ] + // }, + // "FileMoniker": "<moniker>" + // } + // ] + + // Generating DeliveryOptimizationData to simulate the server response, but it's not being parsed by the Client + + json response; + response = json::array(); + response.push_back({{"Url", "http://localhost/1.json"}, + {"FileId", name + ".json"}, + {"SizeInBytes", 100}, + {"Hashes", {{"Sha1", "123"}, {"Sha256", "456"}}}}); + response[0]["DeliveryOptimization"] = {{"CatalogId", "789"}}; + response[0]["DeliveryOptimization"]["Properties"] = { + {"IntegrityCheckInfo", {{"PiecesHashFileUrl", "http://localhost/1.json"}, {"HashOfHashes", "abc"}}}}; + response[0]["ApplicabilityDetails"] = {{"Architectures", {"x86"}}, + {"PlatformApplicabilityForPackage", {"Windows"}}}; + response[0]["FileMoniker"] = "1.json"; + + response.push_back({{"Url", "http://localhost/2.bin"}, + {"FileId", name + ".bin"}, + {"SizeInBytes", 200}, + {"Hashes", {{"Sha1", "421"}, {"Sha256", "132"}}}}); + response[1]["DeliveryOptimization"] = {{"CatalogId", "14"}}; + response[1]["DeliveryOptimization"]["Properties"] = { + {"IntegrityCheckInfo", {{"PiecesHashFileUrl", "http://localhost/2.bin"}, {"HashOfHashes", "abcd"}}}}; + response[1]["ApplicabilityDetails"] = {{"Architectures", {"amd64"}}, + {"PlatformApplicabilityForPackage", {"Linux"}}}; + response[1]["FileMoniker"] = "2.bin"; + + return response; +} + +struct BufferedLogData +{ + std::string message; + std::string file; + unsigned line; + std::string function; + std::chrono::time_point<std::chrono::system_clock> time; +}; + +LogData ToLogData(const BufferedLogData& data) +{ + return {LogSeverity::Info, data.message.c_str(), data.file.c_str(), data.line, data.function.c_str(), data.time}; +} + +void CheckApiVersion(const httplib::Request& req, std::string_view apiVersion) +{ + if (util::AreNotEqualI(req.path_params.at("apiVersion"), apiVersion)) + { + throw StatusCodeException(httplib::StatusCode::NotFound_404); + } +} +} // namespace + +namespace SFS::test::details +{ +class MockWebServerImpl +{ + public: + MockWebServerImpl() = default; + ~MockWebServerImpl() = default; + + MockWebServerImpl(const MockWebServerImpl&) = delete; + MockWebServerImpl& operator=(const MockWebServerImpl&) = delete; + + void Start(); + Result Stop(); + + std::string GetUrl() const; + + void RegisterProduct(std::string&& name, std::string&& version); + void RegisterAppProduct(std::string&& name, std::string&& version, std::vector<MockPrerequisite>&& prerequisites); + void RegisterExpectedRequestHeader(std::string&& header, std::string&& value); + void SetForcedHttpErrors(std::queue<HttpCode> forcedErrors); + void SetResponseHeaders(std::unordered_map<HttpCode, HeaderMap> headersByCode); + + private: + void ConfigureServerSettings(); + void ConfigureRequestHandlers(); + + void ConfigurePostLatestVersion(); + void ConfigurePostLatestVersionBatch(); + void ConfigureGetSpecificVersion(); + void ConfigurePostDownloadInfo(); + + void RunHttpCallback(const httplib::Request& req, + httplib::Response& res, + const std::string& methodName, + const std::string& apiVersion, + const std::function<void(const httplib::Request, httplib::Response&)>& callback); + void CheckRequestHeaders(const httplib::Request& req); + + void BufferLog(const BufferedLogData& data); + void ProcessBufferedLogs(); + + httplib::Server m_server; + int m_port{-1}; + + std::optional<Result> m_lastException; + + std::thread m_listenerThread; + + using VersionList = std::set<std::string>; + std::unordered_map<std::string, VersionList> m_products; + + using AppList = std::set<App, AppCmp>; + std::unordered_map<std::string, AppList> m_appProducts; + + std::unordered_map<std::string, std::string> m_expectedRequestHeaders; + std::queue<HttpCode> m_forcedHttpErrors; + std::unordered_map<HttpCode, HeaderMap> m_headersByCode; + + std::vector<BufferedLogData> m_bufferedLog; + std::mutex m_logMutex; +}; +} // namespace SFS::test::details + +MockWebServer::MockWebServer() +{ + m_impl = std::make_unique<MockWebServerImpl>(); + m_impl->Start(); +} + +MockWebServer::~MockWebServer() +{ + const auto ret = Stop(); + if (!ret) + { + TEST_UNSCOPED_INFO("Failed to stop: " + std::string(ToString(ret.GetCode()))); + } +} + +Result MockWebServer::Stop() +{ + return m_impl->Stop(); +} + +std::string MockWebServer::GetBaseUrl() const +{ + return m_impl->GetUrl(); +} + +void MockWebServer::RegisterProduct(std::string name, std::string version) +{ + m_impl->RegisterProduct(std::move(name), std::move(version)); +} + +void MockWebServer::RegisterAppProduct(std::string name, + std::string version, + std::vector<MockPrerequisite> prerequisites) +{ + m_impl->RegisterAppProduct(std::move(name), std::move(version), std::move(prerequisites)); +} + +void MockWebServer::RegisterExpectedRequestHeader(HttpHeader header, std::string value) +{ + std::string headerName = ToString(header); + m_impl->RegisterExpectedRequestHeader(std::move(headerName), std::move(value)); +} + +void MockWebServer::SetForcedHttpErrors(std::queue<HttpCode> forcedErrors) +{ + m_impl->SetForcedHttpErrors(std::move(forcedErrors)); +} + +void MockWebServer::SetResponseHeaders(std::unordered_map<HttpCode, HeaderMap> headersByCode) +{ + m_impl->SetResponseHeaders(std::move(headersByCode)); +} + +void MockWebServerImpl::Start() +{ + ConfigureServerSettings(); + ConfigureRequestHandlers(); + + m_port = m_server.bind_to_any_port(c_listenHostName); + m_listenerThread = std::thread([&]() { m_server.listen_after_bind(); }); +} + +void MockWebServerImpl::ConfigureServerSettings() +{ + m_server.set_logger([&](const httplib::Request& req, const httplib::Response& res) { + BUFFER_LOG("Request: " + req.method + " " + req.path + " " + req.version); + BUFFER_LOG("Request Body: " + req.body); + + BUFFER_LOG("Response: " + res.version + " " + ::ToString(static_cast<httplib::StatusCode>(res.status)) + " " + + res.reason); + BUFFER_LOG("Response body: " + res.body); + }); + + m_server.set_exception_handler([&](const httplib::Request&, httplib::Response& res, std::exception_ptr ep) { + try + { + std::rethrow_exception(ep); + } + catch (std::exception& e) + { + m_lastException = Result(Result::HttpUnexpected, e.what()); + } + catch (...) + { + m_lastException = Result(Result::HttpUnexpected, "Unknown Exception"); + } + + ProcessBufferedLogs(); + + res.status = httplib::StatusCode::InternalServerError_500; + }); + + // Keeping this interval to a minimum ensures tests run quicker + m_server.set_keep_alive_timeout(1); // 1 second +} + +void MockWebServerImpl::ConfigureRequestHandlers() +{ + ConfigurePostLatestVersion(); + ConfigurePostLatestVersionBatch(); + ConfigureGetSpecificVersion(); + ConfigurePostDownloadInfo(); +} + +void MockWebServerImpl::ConfigurePostLatestVersion() +{ + // Path: /api/<apiVersion:v2>/contents/<instanceId>/namespaces/<ns>/names/<name>/versions/latest?action=select + const std::string pattern = "/api/:apiVersion/contents/:instanceId/namespaces/:ns/names/:name/versions/latest"; + m_server.Post(pattern, [&](const httplib::Request& req, httplib::Response& res) { + RunHttpCallback(req, res, "PostLatestVersion", "v2", [&](const httplib::Request& req, httplib::Response& res) { + // TODO: Ignoring instanceId for now + + if (!req.has_param("action") || util::AreNotEqualI(req.get_param_value("action"), "select")) + { + // TODO: SFS might throw a different error when the query string is unexpected + throw StatusCodeException(httplib::StatusCode::NotFound_404); + } + + // Checking body has expected format, but won't use it for the response + { + if (req.body.empty()) + { + throw StatusCodeException(httplib::StatusCode::BadRequest_400); + } + + json body; + try + { + body = json::parse(req.body); + } + catch (const json::parse_error& ex) + { + BUFFER_LOG("JSON parse error: " + std::string(ex.what())); + throw StatusCodeException(httplib::StatusCode::BadRequest_400); + } + + // The GetLatestVersion API expects an object as a body, with a "TargetingAttributes" object element. + if (!body.is_object() || !body.contains("TargetingAttributes") || + !body["TargetingAttributes"].is_object()) + { + throw StatusCodeException(httplib::StatusCode::BadRequest_400); + } + } + + const std::string ns = req.path_params.at("ns"); + + json response; + const std::string& name = req.path_params.at("name"); + if (auto it = m_products.find(name); it != m_products.end()) + { + const auto& versions = it->second; + if (versions.empty()) + { + throw StatusCodeException(httplib::StatusCode::InternalServerError_500); + } + + const auto& latestVersion = *versions.rbegin(); + response = GenerateContentIdJsonObject(name, latestVersion, ns); + } + else if (auto appIt = m_appProducts.find(name); appIt != m_appProducts.end()) + { + const auto& appList = appIt->second; + if (appList.empty()) + { + throw StatusCodeException(httplib::StatusCode::InternalServerError_500); + } + + const auto& latestApp = *appList.rbegin(); + response = GenerateGetAppVersionJsonObject(name, latestApp, ns); + } + else + { + throw StatusCodeException(httplib::StatusCode::NotFound_404); + } + + res.set_content(response.dump(), "application/json"); + }); + }); +} + +void MockWebServerImpl::ConfigurePostLatestVersionBatch() +{ + // Path: /api/<apiVersion:v2>/contents/<instanceId>/namespaces/<ns>/names?action=BatchUpdates + const std::string pattern = "/api/:apiVersion/contents/:instanceId/namespaces/:ns/names"; + m_server.Post(pattern, [&](const httplib::Request& req, httplib::Response& res) { + RunHttpCallback( + req, + res, + "PostLatestVersionBatch", + "v2", + [&](const httplib::Request& req, httplib::Response& res) { + // TODO: Ignoring instanceId for now + + if (!req.has_param("action") || util::AreNotEqualI(req.get_param_value("action"), "BatchUpdates")) + { + // TODO: SFS might throw a different error when the query string is unexpected + throw StatusCodeException(httplib::StatusCode::NotFound_404); + } + + if (req.body.empty()) + { + throw StatusCodeException(httplib::StatusCode::BadRequest_400); + } + + json body; + try + { + body = json::parse(req.body); + } + catch (const json::parse_error& ex) + { + BUFFER_LOG("JSON parse error: " + std::string(ex.what())); + throw StatusCodeException(httplib::StatusCode::BadRequest_400); + } + + // The BatchUpdates API returns an array of objects, each with a "Product" key. + // If repeated, the same product is only returned once. + // TODO: We are ignoring the TargetingAttributes for now. + if (!body.is_array()) + { + throw StatusCodeException(httplib::StatusCode::BadRequest_400); + } + + // Iterate over the array and collect the unique products + std::unordered_map<std::string, json> requestedProducts; + for (const auto& productRequest : body) + { + if (!productRequest.is_object() || !productRequest.contains("Product") || + !productRequest["Product"].is_string() || !productRequest.contains("TargetingAttributes")) + { + throw StatusCodeException(httplib::StatusCode::BadRequest_400); + } + if (requestedProducts.count(productRequest["Product"])) + { + continue; + } + requestedProducts.emplace(productRequest["Product"], productRequest["TargetingAttributes"]); + } + + // If at least one product exists, we will return a 200 OK with that. Non-existing products are ignored. + // Otherwise, a 404 is sent. + json response = json::array(); + for (const auto& [name, _] : requestedProducts) + { + auto it = m_products.find(name); + if (it == m_products.end()) + { + continue; + } + + const VersionList& versions = it->second; + if (versions.empty()) + { + throw StatusCodeException(httplib::StatusCode::InternalServerError_500); + } + + const std::string ns = req.path_params.at("ns"); + const auto& latestVersion = *versions.rbegin(); + + response.push_back(GenerateContentIdJsonObject(name, latestVersion, ns)); + } + + if (response.empty()) + { + throw StatusCodeException(httplib::StatusCode::NotFound_404); + } + + res.set_content(response.dump(), "application/json"); + }); + }); +} + +void MockWebServerImpl::ConfigureGetSpecificVersion() +{ + // Path: /api/<apiVersion:v2>/contents/<instanceId>/namespaces/<ns>/names/<name>/versions/<version> + const std::string pattern = "/api/:apiVersion/contents/:instanceId/namespaces/:ns/names/:name/versions/:version"; + m_server.Get(pattern, [&](const httplib::Request& req, httplib::Response& res) { + RunHttpCallback(req, res, "GetSpecificVersion", "v2", [&](const httplib::Request& req, httplib::Response& res) { + // TODO: Ignoring instanceId for now + + const std::string& name = req.path_params.at("name"); + auto it = m_products.find(name); + if (it == m_products.end()) + { + throw StatusCodeException(httplib::StatusCode::NotFound_404); + } + + const VersionList& versions = it->second; + if (versions.empty()) + { + throw StatusCodeException(httplib::StatusCode::InternalServerError_500); + } + + // TODO: Are apps suppported? + + const std::string& version = req.path_params.at("version"); + if (version.empty() || !versions.count(version)) + { + throw StatusCodeException(httplib::StatusCode::NotFound_404); + } + + const std::string ns = req.path_params.at("ns"); + + res.set_content(GenerateContentIdJsonObject(name, version, ns).dump(), "application/json"); + }); + }); +} + +void MockWebServerImpl::ConfigurePostDownloadInfo() +{ + // Path: + // /api/<apiVersion:v2>/contents/<instanceId>/namespaces/<ns>/names/<name>/versions/<version>/files?action=GenerateDownloadInfo + const std::string pattern = + "/api/:apiVersion/contents/:instanceId/namespaces/:ns/names/:name/versions/:version/files"; + m_server.Post(pattern, [&](const httplib::Request& req, httplib::Response& res) { + RunHttpCallback(req, res, "PostDownloadInfo", "v2", [&](const httplib::Request& req, httplib::Response& res) { + // TODO: Ignoring instanceId and ns for now + + if (!req.has_param("action") || util::AreNotEqualI(req.get_param_value("action"), "GenerateDownloadInfo")) + { + // TODO: SFS might throw a different error when the query string is unexpected + throw StatusCodeException(httplib::StatusCode::NotFound_404); + } + + const std::string& version = req.path_params.at("version"); + if (version.empty()) + { + throw StatusCodeException(httplib::StatusCode::NotFound_404); + } + + json response; + const std::string& name = req.path_params.at("name"); + if (auto it = m_products.find(name); it != m_products.end()) + { + const auto& versions = it->second; + if (versions.empty()) + { + throw StatusCodeException(httplib::StatusCode::InternalServerError_500); + } + + if (!versions.count(version)) + { + throw StatusCodeException(httplib::StatusCode::NotFound_404); + } + + // Response is a dummy, doesn't use the version above + response = GeneratePostDownloadInfo(name); + } + else if (auto appIt = m_appProducts.find(name); appIt != m_appProducts.end()) + { + const auto& appList = appIt->second; + if (appList.empty()) + { + throw StatusCodeException(httplib::StatusCode::InternalServerError_500); + } + + auto app = std::find_if(appList.begin(), appList.end(), [&](const App& app) { + return app.version == version; + }); + if (app == appList.end()) + { + throw StatusCodeException(httplib::StatusCode::NotFound_404); + } + + // Response is a dummy, doesn't use the version above + response = GeneratePostAppDownloadInfo(name); + } + else + { + throw StatusCodeException(httplib::StatusCode::NotFound_404); + } + + res.set_content(response.dump(), "application/json"); + }); + }); +} + +void MockWebServerImpl::RunHttpCallback(const httplib::Request& req, + httplib::Response& res, + const std::string& methodName, + const std::string& apiVersion, + const std::function<void(const httplib::Request, httplib::Response&)>& callback) +{ + if (m_forcedHttpErrors.size() > 0) + { + res.status = m_forcedHttpErrors.front(); + m_forcedHttpErrors.pop(); + + BUFFER_LOG("Forcing HTTP error: " + std::to_string(res.status)); + } + else + { + try + { + BUFFER_LOG("Matched " + methodName); + CheckApiVersion(req, apiVersion); + CheckRequestHeaders(req); + callback(req, res); + res.status = httplib::StatusCode::OK_200; + } + catch (const StatusCodeException& ex) + { + res.status = ex.GetStatusCode(); + } + catch (const std::exception&) + { + res.status = httplib::StatusCode::InternalServerError_500; + } + catch (...) + { + res.status = httplib::StatusCode::InternalServerError_500; + } + } + + if (m_headersByCode.count(res.status) > 0) + { + BUFFER_LOG("HTTP code " + std::to_string(res.status) + " has response headers to be sent"); + for (const auto& header : m_headersByCode[res.status]) + { + BUFFER_LOG("Adding header [" + header.first + "] with value [" + header.second + "]"); + res.set_header(header.first, header.second); + } + } +} + +void MockWebServerImpl::CheckRequestHeaders(const httplib::Request& req) +{ + for (const auto& header : m_expectedRequestHeaders) + { + std::optional<std::string> errorMessage; + if (!req.has_header(header.first)) + { + errorMessage = "Expected header [" + header.first + "] not found"; + } + else if (util::AreNotEqualI(req.get_header_value(header.first), header.second)) + { + errorMessage = "Header [" + header.first + "] with value [" + req.get_header_value(header.first) + + "] does not match the expected value [" + header.second + "]"; + } + + if (errorMessage) + { + BUFFER_LOG(*errorMessage); + throw std::runtime_error(errorMessage->c_str()); + } + } +} + +void MockWebServerImpl::BufferLog(const BufferedLogData& data) +{ + std::lock_guard guard(m_logMutex); + m_bufferedLog.push_back(data); +} + +void MockWebServerImpl::ProcessBufferedLogs() +{ + for (const auto& data : m_bufferedLog) + { + LogCallbackToTest(ToLogData(data)); + } + m_bufferedLog.clear(); +} + +Result MockWebServerImpl::Stop() +{ + if (m_listenerThread.joinable()) + { + m_server.stop(); + m_listenerThread.join(); + } + ProcessBufferedLogs(); + return m_lastException.value_or(Result::Success); +} + +std::string MockWebServerImpl::GetUrl() const +{ + return "http://"s + c_listenHostName + ":"s + std::to_string(m_port); +} + +void MockWebServerImpl::RegisterProduct(std::string&& name, std::string&& version) +{ + m_products[std::move(name)].emplace(std::move(version)); +} + +void MockWebServerImpl::RegisterAppProduct(std::string&& name, + std::string&& version, + std::vector<MockPrerequisite>&& prerequisites) +{ + for (const auto& prereq : prerequisites) + { + m_appProducts[prereq.name].emplace(App{prereq.version, {}}); + } + m_appProducts[std::move(name)].emplace(App{std::move(version), std::move(prerequisites)}); +} + +void MockWebServerImpl::RegisterExpectedRequestHeader(std::string&& header, std::string&& value) +{ + if (auto it = m_expectedRequestHeaders.find(header); it != m_expectedRequestHeaders.end()) + { + it->second = std::move(value); + return; + } + else + { + m_expectedRequestHeaders.emplace(std::move(header), std::move(value)); + } +} + +void MockWebServerImpl::SetForcedHttpErrors(std::queue<int> forcedErrors) +{ + m_forcedHttpErrors = std::move(forcedErrors); +} + +void MockWebServerImpl::SetResponseHeaders(std::unordered_map<HttpCode, HeaderMap> headersByCode) +{ + m_headersByCode = std::move(headersByCode); +} diff --git a/src/SfsClient/sfs-client/client/tests/mock/MockWebServer.h b/src/SfsClient/sfs-client/client/tests/mock/MockWebServer.h @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "Result.h" + +#include <memory> +#include <queue> +#include <string> +#include <unordered_map> + +namespace SFS::details +{ +enum class HttpHeader; +} + +namespace SFS::test +{ +namespace details +{ +class MockWebServerImpl; +} + +using HttpCode = int; +using HeaderMap = std::unordered_map<std::string, std::string>; + +struct MockPrerequisite +{ + std::string name; + std::string version; +}; + +class MockWebServer +{ + public: + MockWebServer(); + ~MockWebServer(); + + MockWebServer(const MockWebServer&) = delete; + MockWebServer& operator=(const MockWebServer&) = delete; + + [[nodiscard]] Result Stop(); + + std::string GetBaseUrl() const; + + /// @brief Registers a product with the server. Will fill the other data with gibberish for testing purposes + void RegisterProduct(std::string name, std::string version); + + /// @brief Registers an app with the server. Will fill the other data with gibberish for testing purposes + void RegisterAppProduct(std::string name, std::string version, std::vector<MockPrerequisite> prerequisites); + + /// @brief Registers the expectation of a given header to the present in the request + void RegisterExpectedRequestHeader(SFS::details::HttpHeader header, std::string value); + + /** + * @brief Registers a sequence of HTTP error codes that will be sent by the server in the order in which they are + * passed. + */ + void SetForcedHttpErrors(std::queue<HttpCode> forcedErrors); + + /// @brief Registers a set of headers that will be sent depending on the HTTP code + void SetResponseHeaders(std::unordered_map<HttpCode, HeaderMap> headersByCode); + + private: + std::unique_ptr<details::MockWebServerImpl> m_impl; +}; +} // namespace SFS::test diff --git a/src/SfsClient/sfs-client/client/tests/unit/AppContentTests.cpp b/src/SfsClient/sfs-client/client/tests/unit/AppContentTests.cpp @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "ContentUtil.h" +#include "sfsclient/AppContent.h" + +#include <catch2/catch_test_macros.hpp> + +#define TEST(...) TEST_CASE("[AppContentTests] " __VA_ARGS__) +#define TEST_SCENARIO(...) TEST_CASE("[AppContentTests] Scenario: " __VA_ARGS__) + +using namespace SFS; +using namespace SFS::details; +using namespace SFS::details::contentutil; + +namespace +{ +std::unique_ptr<ContentId> GetContentId(const std::string& nameSpace, + const std::string& name, + const std::string& version) +{ + std::unique_ptr<ContentId> contentId; + REQUIRE(ContentId::Make(nameSpace, name, version, contentId) == Result::Success); + REQUIRE(contentId != nullptr); + return contentId; +} + +std::unique_ptr<AppFile> GetAppFile(const std::string& fileId, + const std::string& url, + uint64_t sizeInBytes, + const std::unordered_map<HashType, std::string>& hashes, + std::vector<Architecture> architectures, + std::vector<std::string> platformApplicabilityForPackage, + std::string fileMoniker) +{ + std::unique_ptr<AppFile> file; + REQUIRE(AppFile::Make(fileId, + url, + sizeInBytes, + hashes, + architectures, + platformApplicabilityForPackage, + fileMoniker, + file) == Result::Success); + REQUIRE(file != nullptr); + return file; +} + +std::unique_ptr<AppPrerequisiteContent> GetPrerequisiteContent(const std::string& contentNameSpace, + const std::string& contentName, + const std::string& contentVersion, + const std::vector<AppFile>& files) +{ + std::unique_ptr<ContentId> contentId = GetContentId(contentNameSpace, contentName, contentVersion); + std::vector<AppFile> clonedFiles; + for (auto& file : files) + { + clonedFiles.push_back(std::move(*GetAppFile(file.GetFileId(), + file.GetUrl(), + file.GetSizeInBytes(), + file.GetHashes(), + file.GetApplicabilityDetails().GetArchitectures(), + file.GetApplicabilityDetails().GetPlatformApplicabilityForPackage(), + file.GetFileMoniker()))); + } + + std::unique_ptr<AppPrerequisiteContent> content; + REQUIRE(AppPrerequisiteContent::Make(std::move(contentId), std::move(clonedFiles), content) == Result::Success); + REQUIRE(content != nullptr); + return content; +} + +std::unique_ptr<AppContent> GetAppContent(const std::string& contentNameSpace, + const std::string& contentName, + const std::string& contentVersion, + const std::string& updateId, + const std::vector<AppPrerequisiteContent>& prerequisites, + const std::vector<AppFile>& files) +{ + std::unique_ptr<ContentId> contentId = GetContentId(contentNameSpace, contentName, contentVersion); + std::vector<AppPrerequisiteContent> clonedPreqs; + for (const auto& prereq : prerequisites) + { + clonedPreqs.push_back(std::move(*GetPrerequisiteContent(prereq.GetContentId().GetNameSpace(), + prereq.GetContentId().GetName(), + prereq.GetContentId().GetVersion(), + prereq.GetFiles()))); + } + + std::vector<AppFile> clonedFiles; + for (auto& file : files) + { + clonedFiles.push_back(std::move(*GetAppFile(file.GetFileId(), + file.GetUrl(), + file.GetSizeInBytes(), + file.GetHashes(), + file.GetApplicabilityDetails().GetArchitectures(), + file.GetApplicabilityDetails().GetPlatformApplicabilityForPackage(), + file.GetFileMoniker()))); + } + + std::unique_ptr<AppContent> appContent; + REQUIRE( + AppContent::Make(std::move(contentId), updateId, std::move(clonedPreqs), std::move(clonedFiles), appContent) == + Result::Success); + REQUIRE(appContent != nullptr); + return appContent; +} +} // namespace + +TEST("Testing AppContent::Make()") +{ + const std::string contentNameSpace{"myNameSpace"}; + const std::string contentName{"myName"}; + const std::string contentVersion{"myVersion"}; + + std::unique_ptr<AppFile> file1 = GetAppFile("fileId1", + "url1", + 1 /*sizeInBytes*/, + {{HashType::Sha1, "sha1"}}, + {Architecture::Amd64}, + {"myPlatformApplicabilityForPackage1"}, + "fileMoniker1"); + std::unique_ptr<AppFile> file2 = GetAppFile("fileId2", + "url2", + 1 /*sizeInBytes*/, + {{HashType::Sha256, "sha256"}}, + {Architecture::Amd64}, + {"myPlatformApplicabilityForPackage1"}, + "fileMoniker1"); + + std::vector<AppFile> files; + files.push_back(std::move(*file1)); + files.push_back(std::move(*file2)); + + std::vector<AppFile> prereqFiles; + prereqFiles.push_back(std::move(*GetAppFile("prereqFileId", + "url", + 1 /*sizeInBytes*/, + {{HashType::Sha1, "sha1"}}, + {Architecture::Amd64}, + {"myPlatformApplicabilityForPackage"}, + "fileMoniker"))); + + std::vector<AppPrerequisiteContent> prerequisites; + prerequisites.push_back( + std::move(*GetPrerequisiteContent(contentNameSpace, "prereqName", "prereqVersion", prereqFiles))); + + std::unique_ptr<AppContent> appContent = + GetAppContent(contentNameSpace, contentName, contentVersion, "updateId", prerequisites, files); + REQUIRE(appContent != nullptr); +} + +TEST("Testing AppContent equality operators") +{ + const std::string contentNameSpace{"myNameSpace"}; + const std::string contentName{"myName"}; + const std::string contentVersion{"myVersion"}; + const std::string updateId{"myUpdateId"}; + + std::unique_ptr<AppFile> file = GetAppFile("fileId", + "url", + 1 /*sizeInBytes*/, + {{HashType::Sha1, "sha1"}}, + {Architecture::Amd64}, + {"myPlatformApplicabilityForPackage"}, + "fileMoniker"); + + std::vector<AppFile> files; + files.push_back(std::move(*file)); + + std::vector<AppFile> prereqFiles; + prereqFiles.push_back(std::move(*GetAppFile("prereqFileId", + "url", + 1 /*sizeInBytes*/, + {{HashType::Sha1, "sha1"}}, + {Architecture::Amd64}, + {"myPlatformApplicabilityForPackage"}, + "fileMoniker"))); + + std::vector<AppPrerequisiteContent> prerequisites; + prerequisites.push_back( + std::move(*GetPrerequisiteContent(contentNameSpace, "prereqName", "prereqVersion", prereqFiles))); + + const std::unique_ptr<AppContent> content = + GetAppContent(contentNameSpace, contentName, contentVersion, updateId, prerequisites, files); + + SECTION("Equal") + { + auto CompareAppContentEqual = [&content](const std::unique_ptr<AppContent>& sameContent) { + REQUIRE((*content == *sameContent)); + REQUIRE_FALSE((*content != *sameContent)); + }; + + CompareAppContentEqual( + GetAppContent(contentNameSpace, contentName, contentVersion, updateId, prerequisites, files)); + } + + SECTION("Not equal") + { + auto CompareAppContentNotEqual = [&content](const std::unique_ptr<AppContent>& otherContent) { + REQUIRE((*content != *otherContent)); + REQUIRE_FALSE((*content == *otherContent)); + }; + + CompareAppContentNotEqual(GetAppContent("", contentName, contentVersion, updateId, prerequisites, files)); + CompareAppContentNotEqual(GetAppContent(contentNameSpace, "", contentVersion, updateId, prerequisites, files)); + CompareAppContentNotEqual(GetAppContent(contentNameSpace, contentName, "", updateId, prerequisites, files)); + CompareAppContentNotEqual( + GetAppContent(contentNameSpace, contentName, contentVersion, "", prerequisites, files)); + CompareAppContentNotEqual(GetAppContent(contentNameSpace, contentName, contentVersion, updateId, {}, files)); + CompareAppContentNotEqual( + GetAppContent(contentNameSpace, contentName, contentVersion, updateId, prerequisites, {})); + } +} diff --git a/src/SfsClient/sfs-client/client/tests/unit/AppFileTests.cpp b/src/SfsClient/sfs-client/client/tests/unit/AppFileTests.cpp @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "ContentUtil.h" +#include "sfsclient/AppFile.h" + +#include <catch2/catch_test_macros.hpp> + +#define TEST(...) TEST_CASE("[AppFileTests] " __VA_ARGS__) +#define TEST_SCENARIO(...) TEST_CASE("[AppFileTests] Scenario: " __VA_ARGS__) + +using namespace SFS; +using namespace SFS::details; +using namespace SFS::details::contentutil; + +namespace +{ +std::unique_ptr<AppFile> GetAppFile(const std::string& fileId, + const std::string& url, + uint64_t sizeInBytes, + const std::unordered_map<HashType, std::string>& hashes, + std::vector<Architecture> architectures, + std::vector<std::string> platformApplicabilityForPackage, + std::string fileMoniker) +{ + std::unique_ptr<AppFile> file; + REQUIRE(AppFile::Make(fileId, + url, + sizeInBytes, + hashes, + architectures, + platformApplicabilityForPackage, + fileMoniker, + file) == Result::Success); + REQUIRE(file != nullptr); + return file; +} +} // namespace + +TEST("Testing AppFile::Make()") +{ + const std::string fileId{"myFileId"}; + const std::string url{"myUrl"}; + const uint64_t sizeInBytes{1234}; + const std::unordered_map<HashType, std::string> hashes{{HashType::Sha1, "mySha1"}, {HashType::Sha256, "mySha256"}}; + const std::vector<Architecture> architectures{Architecture::Amd64}; + const std::vector<std::string> platformApplicabilityForPackage{"myPlatformApplicabilityForPackage"}; + const std::string fileMoniker{"myFileMoniker"}; + + const std::unique_ptr<AppFile> file = + GetAppFile(fileId, url, sizeInBytes, hashes, architectures, platformApplicabilityForPackage, fileMoniker); + + CHECK(fileId == file->GetFileId()); + CHECK(url == file->GetUrl()); + CHECK(sizeInBytes == file->GetSizeInBytes()); + CHECK(hashes == file->GetHashes()); + + SECTION("Testing File equality operators") + { + SECTION("Equal") + { + auto CompareFileEqual = [&file](const std::unique_ptr<AppFile>& sameFile) { + REQUIRE((*file == *sameFile)); + REQUIRE_FALSE((*file != *sameFile)); + }; + + CompareFileEqual(GetAppFile(fileId, + url, + sizeInBytes, + hashes, + architectures, + platformApplicabilityForPackage, + fileMoniker)); + } + + SECTION("Not equal") + { + auto CompareFileNotEqual = [&file](const std::unique_ptr<AppFile>& otherFile) { + REQUIRE((*file != *otherFile)); + REQUIRE_FALSE((*file == *otherFile)); + }; + + CompareFileNotEqual( + GetAppFile("", url, sizeInBytes, hashes, architectures, platformApplicabilityForPackage, fileMoniker)); + CompareFileNotEqual(GetAppFile(fileId, + "", + sizeInBytes, + hashes, + architectures, + platformApplicabilityForPackage, + fileMoniker)); + CompareFileNotEqual( + GetAppFile(fileId, url, 0, hashes, architectures, platformApplicabilityForPackage, fileMoniker)); + CompareFileNotEqual( + GetAppFile(fileId, url, sizeInBytes, {}, architectures, platformApplicabilityForPackage, fileMoniker)); + CompareFileNotEqual( + GetAppFile(fileId, url, sizeInBytes, hashes, {}, platformApplicabilityForPackage, fileMoniker)); + CompareFileNotEqual(GetAppFile(fileId, url, sizeInBytes, hashes, architectures, {}, fileMoniker)); + CompareFileNotEqual(GetAppFile(fileId, url, sizeInBytes, hashes, {}, platformApplicabilityForPackage, {})); + CompareFileNotEqual(GetAppFile("", "", 0, {}, architectures, platformApplicabilityForPackage, fileMoniker)); + CompareFileNotEqual(GetAppFile("MYFILEID", + url, + sizeInBytes, + hashes, + architectures, + platformApplicabilityForPackage, + fileMoniker)); + CompareFileNotEqual(GetAppFile(fileId, + "MYURL", + sizeInBytes, + hashes, + architectures, + platformApplicabilityForPackage, + fileMoniker)); + } + } +} diff --git a/src/SfsClient/sfs-client/client/tests/unit/ApplicabilityDetailsTests.cpp b/src/SfsClient/sfs-client/client/tests/unit/ApplicabilityDetailsTests.cpp @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "ApplicabilityDetails.h" +#include "ContentUtil.h" + +#include <catch2/catch_test_macros.hpp> + +#define TEST(...) TEST_CASE("[ApplicabilityDetailsTests] " __VA_ARGS__) + +using namespace SFS; +using namespace SFS::details::contentutil; + +namespace +{ +std::unique_ptr<ApplicabilityDetails> GetDetails(const std::vector<Architecture>& architectures, + const std::vector<std::string>& platformApplicabilityForPackage) +{ + std::unique_ptr<ApplicabilityDetails> details; + REQUIRE(ApplicabilityDetails::Make(architectures, platformApplicabilityForPackage, details) == Result::Success); + REQUIRE(details != nullptr); + return details; +}; +} // namespace + +TEST("Testing ApplicabilityDetails::Make()") +{ + const std::vector<Architecture> architectures{Architecture::x86, Architecture::Amd64}; + const std::vector<std::string> platformApplicabilityForPackage{"Windows.Desktop", "Windows.Server"}; + const std::string fileMoniker{"myApp"}; + + const std::unique_ptr<ApplicabilityDetails> details = GetDetails(architectures, platformApplicabilityForPackage); + + CHECK(architectures == details->GetArchitectures()); + CHECK(platformApplicabilityForPackage == details->GetPlatformApplicabilityForPackage()); + + SECTION("Testing ApplicabilityDetails equality operators") + { + SECTION("Equal") + { + auto CompareDetailsEqual = [&details](const std::unique_ptr<ApplicabilityDetails>& sameDetails) { + REQUIRE((*details == *sameDetails)); + REQUIRE_FALSE((*details != *sameDetails)); + }; + + CompareDetailsEqual(GetDetails(architectures, platformApplicabilityForPackage)); + } + + SECTION("Not equal") + { + auto CompareDetailsNotEqual = [&details](const std::unique_ptr<ApplicabilityDetails>& otherDetails) { + REQUIRE((*details != *otherDetails)); + REQUIRE_FALSE((*details == *otherDetails)); + }; + + CompareDetailsNotEqual(GetDetails({}, platformApplicabilityForPackage)); + CompareDetailsNotEqual(GetDetails(architectures, {})); + CompareDetailsNotEqual(GetDetails({}, {})); + } + } +} diff --git a/src/SfsClient/sfs-client/client/tests/unit/ContentIdTests.cpp b/src/SfsClient/sfs-client/client/tests/unit/ContentIdTests.cpp @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "ContentUtil.h" +#include "sfsclient/ContentId.h" + +#include <catch2/catch_test_macros.hpp> + +#define TEST(...) TEST_CASE("[ContentIdTests] " __VA_ARGS__) +#define TEST_SCENARIO(...) TEST_CASE("[ContentIdTests] Scenario: " __VA_ARGS__) + +using namespace SFS; +using namespace SFS::details; +using namespace SFS::details::contentutil; + +namespace +{ +std::unique_ptr<ContentId> GetContentId(const std::string& nameSpace, + const std::string& name, + const std::string& version) +{ + std::unique_ptr<ContentId> contentId; + REQUIRE(ContentId::Make(nameSpace, name, version, contentId) == Result::Success); + REQUIRE(contentId != nullptr); + return contentId; +}; +} // namespace + +TEST("Testing ContentId::Make()") +{ + const std::string nameSpace{"myNameSpace"}; + const std::string name{"myName"}; + const std::string version{"myVersion"}; + + const std::unique_ptr<ContentId> contentId = GetContentId(nameSpace, name, version); + + CHECK(nameSpace == contentId->GetNameSpace()); + CHECK(name == contentId->GetName()); + CHECK(version == contentId->GetVersion()); + + SECTION("Testing ContentId equality operators") + { + SECTION("Equal") + { + auto CompareContentIdEqual = [&contentId](const std::unique_ptr<ContentId>& sameContentId) { + REQUIRE((*contentId == *sameContentId)); + REQUIRE_FALSE((*contentId != *sameContentId)); + }; + + CompareContentIdEqual(GetContentId(nameSpace, name, version)); + } + + SECTION("Not equal") + { + auto CompareContentIdNotEqual = [&contentId](const std::unique_ptr<ContentId>& otherContentId) { + REQUIRE((*contentId != *otherContentId)); + REQUIRE_FALSE((*contentId == *otherContentId)); + }; + + CompareContentIdNotEqual(GetContentId("", name, version)); + CompareContentIdNotEqual(GetContentId(nameSpace, "", version)); + CompareContentIdNotEqual(GetContentId(nameSpace, name, "")); + CompareContentIdNotEqual(GetContentId("", "", "")); + CompareContentIdNotEqual(GetContentId("MYNAMESPACE", name, version)); + CompareContentIdNotEqual(GetContentId(nameSpace, "MYNAME", version)); + CompareContentIdNotEqual(GetContentId(nameSpace, name, "MYVERSION")); + } + } +} diff --git a/src/SfsClient/sfs-client/client/tests/unit/ContentTests.cpp b/src/SfsClient/sfs-client/client/tests/unit/ContentTests.cpp @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "ContentUtil.h" +#include "sfsclient/Content.h" + +#include <catch2/catch_test_macros.hpp> + +#define TEST(...) TEST_CASE("[ContentTests] " __VA_ARGS__) +#define TEST_SCENARIO(...) TEST_CASE("[ContentTests] Scenario: " __VA_ARGS__) + +using namespace SFS; +using namespace SFS::details; +using namespace SFS::details::contentutil; + +namespace +{ +std::unique_ptr<File> GetFile(const std::string& fileId, + const std::string& url, + uint64_t sizeInBytes, + const std::unordered_map<HashType, std::string>& hashes) +{ + std::unique_ptr<File> file; + REQUIRE(File::Make(fileId, url, sizeInBytes, hashes, file) == Result::Success); + REQUIRE(file != nullptr); + return file; +}; + +std::unique_ptr<Content> GetContent(const std::string& contentNameSpace, + const std::string& contentName, + const std::string& contentVersion, + const std::vector<File>& files) +{ + std::unique_ptr<Content> content; + REQUIRE(Content::Make(contentNameSpace, contentName, contentVersion, files, content) == Result::Success); + REQUIRE(content != nullptr); + return content; +}; +} // namespace + +TEST_SCENARIO("Testing Content::Make()") +{ + GIVEN("Elements that make up a content") + { + const std::string contentNameSpace{"myNameSpace"}; + const std::string contentName{"myName"}; + const std::string contentVersion{"myVersion"}; + + std::unique_ptr<File> file1 = GetFile("fileId1", "url1", 1 /*sizeInBytes*/, {{HashType::Sha1, "sha1"}}); + std::unique_ptr<File> file2 = GetFile("fileId2", "url2", 1 /*sizeInBytes*/, {{HashType::Sha256, "sha256"}}); + + std::vector<File> files; + files.push_back(std::move(*file1)); + files.push_back(std::move(*file2)); + + // Getting raw pointers to check they don't match after copy but match after move + std::vector<File*> filePointers; + for (size_t i = 0; i < files.size(); ++i) + { + filePointers.push_back(&files[i]); + } + + WHEN("A Content is created by copying the parameters") + { + std::unique_ptr<Content> copiedContent; + REQUIRE(Content::Make(contentNameSpace, contentName, contentVersion, files, copiedContent) == + Result::Success); + REQUIRE(copiedContent != nullptr); + + THEN("The content elements are copies") + { + CHECK(contentNameSpace == copiedContent->GetContentId().GetNameSpace()); + CHECK(contentName == copiedContent->GetContentId().GetName()); + CHECK(contentVersion == copiedContent->GetContentId().GetVersion()); + + // Files were cloned, so the pointers are different, but the contents should be similar + REQUIRE(files.size() == copiedContent->GetFiles().size()); + REQUIRE(filePointers.size() == copiedContent->GetFiles().size()); + for (size_t i = 0; i < files.size(); ++i) + { + // Checking ptrs + REQUIRE(filePointers[i] != &copiedContent->GetFiles()[i]); + REQUIRE(&files[i] != &copiedContent->GetFiles()[i]); + + // Checking contents + CHECK((files[i] == copiedContent->GetFiles()[i])); + } + } + + AND_THEN("Using the Make() that moves the file parameter really moves the parameter") + { + std::unique_ptr<Content> movedContent; + REQUIRE(Content::Make(contentNameSpace, contentName, contentVersion, std::move(files), movedContent) == + Result::Success); + REQUIRE(movedContent != nullptr); + + // Checking contents + CHECK((*copiedContent == *movedContent)); + + // Checking underlying pointers are the same since they were moved + REQUIRE(filePointers.size() == movedContent->GetFiles().size()); + REQUIRE(copiedContent->GetFiles().size() == movedContent->GetFiles().size()); + for (size_t i = 0; i < filePointers.size(); ++i) + { + REQUIRE(&copiedContent->GetFiles()[i] != &movedContent->GetFiles()[i]); + REQUIRE(filePointers[i] == &movedContent->GetFiles()[i]); + } + } + } + } +} + +TEST("Testing Content equality operators") +{ + const std::string contentNameSpace{"myNameSpace"}; + const std::string contentName{"myName"}; + const std::string contentVersion{"myVersion"}; + + std::unique_ptr<File> file = GetFile("fileId", "url", 1 /*sizeInBytes*/, {{HashType::Sha1, "sha1"}}); + + std::unique_ptr<File> clonedFile; + REQUIRE(File::Make(file->GetFileId(), file->GetUrl(), file->GetSizeInBytes(), file->GetHashes(), clonedFile) == + Result::Success); + + std::vector<File> files; + files.push_back(std::move(*file)); + + std::vector<File> clonedFiles; + clonedFiles.push_back(std::move(*clonedFile)); + + const std::unique_ptr<Content> content = GetContent(contentNameSpace, contentName, contentVersion, files); + + SECTION("Equal") + { + auto CompareContentEqual = [&content](const std::unique_ptr<Content>& sameContent) { + REQUIRE((*content == *sameContent)); + REQUIRE_FALSE((*content != *sameContent)); + }; + + CompareContentEqual(GetContent(contentNameSpace, contentName, contentVersion, files)); + CompareContentEqual(GetContent(contentNameSpace, contentName, contentVersion, clonedFiles)); + } + + SECTION("Not equal") + { + auto CompareContentNotEqual = [&content](const std::unique_ptr<Content>& otherContent) { + REQUIRE((*content != *otherContent)); + REQUIRE_FALSE((*content == *otherContent)); + }; + + CompareContentNotEqual(GetContent("", contentName, contentVersion, files)); + CompareContentNotEqual(GetContent(contentNameSpace, "", contentVersion, files)); + CompareContentNotEqual(GetContent(contentNameSpace, contentName, "", files)); + CompareContentNotEqual(GetContent(contentNameSpace, contentName, contentVersion, {})); + CompareContentNotEqual(GetContent("", "", "", {})); + CompareContentNotEqual(GetContent("MYNAMESPACE", contentName, contentVersion, files)); + CompareContentNotEqual(GetContent(contentNameSpace, "MYNAME", contentVersion, files)); + CompareContentNotEqual(GetContent(contentNameSpace, contentName, "MYVERSION", files)); + } +} diff --git a/src/SfsClient/sfs-client/client/tests/unit/FileTests.cpp b/src/SfsClient/sfs-client/client/tests/unit/FileTests.cpp @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "ContentUtil.h" +#include "sfsclient/File.h" + +#include <catch2/catch_test_macros.hpp> + +#define TEST(...) TEST_CASE("[FileTests] " __VA_ARGS__) +#define TEST_SCENARIO(...) TEST_CASE("[FileTests] Scenario: " __VA_ARGS__) + +using namespace SFS; +using namespace SFS::details; +using namespace SFS::details::contentutil; + +namespace +{ +std::unique_ptr<File> GetFile(const std::string& fileId, + const std::string& url, + uint64_t sizeInBytes, + const std::unordered_map<HashType, std::string>& hashes) +{ + std::unique_ptr<File> file; + REQUIRE(File::Make(fileId, url, sizeInBytes, hashes, file) == Result::Success); + REQUIRE(file != nullptr); + return file; +}; +} // namespace + +TEST("Testing File::Make()") +{ + const std::string fileId{"myFileId"}; + const std::string url{"myUrl"}; + const uint64_t sizeInBytes{1234}; + const std::unordered_map<HashType, std::string> hashes{{HashType::Sha1, "mySha1"}, {HashType::Sha256, "mySha256"}}; + + const std::unique_ptr<File> file = GetFile(fileId, url, sizeInBytes, hashes); + + CHECK(fileId == file->GetFileId()); + CHECK(url == file->GetUrl()); + CHECK(sizeInBytes == file->GetSizeInBytes()); + CHECK(hashes == file->GetHashes()); + + SECTION("Testing File equality operators") + { + SECTION("Equal") + { + auto CompareFileEqual = [&file](const std::unique_ptr<File>& sameFile) { + REQUIRE((*file == *sameFile)); + REQUIRE_FALSE((*file != *sameFile)); + }; + + CompareFileEqual(GetFile(fileId, url, sizeInBytes, hashes)); + } + + SECTION("Not equal") + { + auto CompareFileNotEqual = [&file](const std::unique_ptr<File>& otherFile) { + REQUIRE((*file != *otherFile)); + REQUIRE_FALSE((*file == *otherFile)); + }; + + CompareFileNotEqual(GetFile("", url, sizeInBytes, hashes)); + CompareFileNotEqual(GetFile(fileId, "", sizeInBytes, hashes)); + CompareFileNotEqual(GetFile(fileId, url, 0, hashes)); + CompareFileNotEqual(GetFile(fileId, url, sizeInBytes, {})); + CompareFileNotEqual(GetFile("", "", 0, {})); + CompareFileNotEqual(GetFile("MYFILEID", url, sizeInBytes, hashes)); + CompareFileNotEqual(GetFile(fileId, "MYURL", sizeInBytes, hashes)); + } + } +} diff --git a/src/SfsClient/sfs-client/client/tests/unit/ResultTests.cpp b/src/SfsClient/sfs-client/client/tests/unit/ResultTests.cpp @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "sfsclient/Result.h" + +#include <catch2/catch_test_macros.hpp> + +#define TEST(...) TEST_CASE("[ResultTests] " __VA_ARGS__) + +using namespace SFS; + +TEST("Testing Result() class methods") +{ + SECTION("Default constructor") + { + Result resultSuccess(Result::Code::Success); + + REQUIRE(resultSuccess.GetCode() == Result::Code::Success); + REQUIRE(resultSuccess.GetMsg().empty()); + REQUIRE(resultSuccess.IsSuccess()); + REQUIRE_FALSE(resultSuccess.IsFailure()); + + INFO("Comparison operators"); + REQUIRE(resultSuccess == Result::Code::Success); + REQUIRE(resultSuccess == Result::Success); + REQUIRE(resultSuccess != Result::Code::NotSet); + REQUIRE(resultSuccess != Result::NotSet); + REQUIRE_FALSE(resultSuccess == Result::NotSet); + + INFO("bool operator"); + REQUIRE(resultSuccess); + } + + SECTION("Constructor with message") + { + Result resultUnexpected(Result::Code::Unexpected, "message"); + REQUIRE(resultUnexpected.GetCode() == Result::Code::Unexpected); + REQUIRE(resultUnexpected.GetMsg() == "message"); + REQUIRE_FALSE(resultUnexpected.IsSuccess()); + REQUIRE(resultUnexpected.IsFailure()); + + INFO("Comparison operators on constructor with message"); + REQUIRE(resultUnexpected == Result::Code::Unexpected); + REQUIRE(resultUnexpected != Result::Code::NotSet); + + INFO("bool operator"); + REQUIRE_FALSE(resultUnexpected); + } +} + +TEST("Testing ToString(Result)") +{ + REQUIRE(SFS::ToString(Result::Code::Success) == "Success"); + REQUIRE(SFS::ToString(Result::Code::NotImpl) == "NotImpl"); + REQUIRE(SFS::ToString(Result::Code::NotSet) == "NotSet"); + REQUIRE(SFS::ToString(Result::Code::OutOfMemory) == "OutOfMemory"); + REQUIRE(SFS::ToString(Result::Code::Unexpected) == "Unexpected"); +} diff --git a/src/SfsClient/sfs-client/client/tests/unit/SFSClientTests.cpp b/src/SfsClient/sfs-client/client/tests/unit/SFSClientTests.cpp @@ -0,0 +1,300 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "sfsclient/ApplicabilityDetails.h" +#include "sfsclient/SFSClient.h" + +#include <catch2/catch_test_macros.hpp> + +#include <optional> + +#define TEST(...) TEST_CASE("[SFSClientTests] " __VA_ARGS__) +#define TEST_SCENARIO(...) TEST_CASE("[SFSClientTests] Scenario: " __VA_ARGS__) + +using namespace SFS; + +namespace +{ +std::unique_ptr<SFSClient> GetSFSClient(std::optional<std::string> instanceId = std::nullopt) +{ + std::unique_ptr<SFSClient> sfsClient; + ClientConfig options; + options.accountId = "testAccountId"; + if (instanceId) + { + options.instanceId = *instanceId; + } + REQUIRE(SFSClient::Make(options, sfsClient) == Result::Success); + REQUIRE(sfsClient != nullptr); + return sfsClient; +} + +void TestLoggingCallback(const LogData&) +{ +} + +struct TestLoggingCallbackStruct +{ + static void TestLoggingCallback(const LogData&) + { + } +}; +} // namespace + +static void StaticTestLoggingCallback(const LogData&) +{ +} + +TEST("Testing SFSClient::Make()") +{ +#ifdef __GNUG__ +// For GCC, explicitly turning off "missing-field-initializers" warning as this block is testing the scenario +// in which a user calls explicitly onto the API with field initializers for ClientConfig +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" +#endif + + const std::string accountId{"testAccountId"}; + const std::string instanceId{"testInstanceId"}; + const std::string nameSpace{"testNameSpace"}; + + std::unique_ptr<SFSClient> sfsClient; + + SECTION("Make({accountId}, out)") + { + REQUIRE(SFSClient::Make({accountId}, sfsClient) == Result::Success); + REQUIRE(sfsClient != nullptr); + + ClientConfig config{accountId}; + REQUIRE(SFSClient::Make(config, sfsClient) == Result::Success); + REQUIRE(sfsClient != nullptr); + } + + SECTION("Make(accountId, instanceId, out)") + { + REQUIRE(SFSClient::Make({accountId, instanceId}, sfsClient) == Result::Success); + REQUIRE(sfsClient != nullptr); + + ClientConfig config{accountId, instanceId}; + REQUIRE(SFSClient::Make(config, sfsClient) == Result::Success); + REQUIRE(sfsClient != nullptr); + } + + SECTION("Make(accountId, instanceId, namespace, out)") + { + REQUIRE(SFSClient::Make({accountId, instanceId, nameSpace}, sfsClient) == Result::Success); + REQUIRE(sfsClient != nullptr); + + SECTION("Call make when the pointer is reset") + { + sfsClient.reset(); + REQUIRE(SFSClient::Make({accountId, instanceId, nameSpace}, sfsClient) == Result::Success); + REQUIRE(sfsClient != nullptr); + } + + SECTION("Call make if the pointer is not reset, as Make() resets it") + { + REQUIRE(SFSClient::Make({accountId, instanceId, nameSpace}, sfsClient) == Result::Success); + REQUIRE(sfsClient != nullptr); + } + + SECTION("We can also use a separate ClientConfig object") + { + ClientConfig config{accountId, instanceId, nameSpace}; + REQUIRE(SFSClient::Make(config, sfsClient) == Result::Success); + REQUIRE(sfsClient != nullptr); + } + + SECTION("We can also move a separate ClientConfig object") + { + ClientConfig config{accountId, instanceId, nameSpace}; + REQUIRE(SFSClient::Make(std::move(config), sfsClient) == Result::Success); + REQUIRE(sfsClient != nullptr); + } + } + + SECTION("Make(accountId, std::nullopt, nameSpace, out)") + { + REQUIRE(SFSClient::Make({accountId, std::nullopt, nameSpace}, sfsClient) == Result::Success); + REQUIRE(sfsClient != nullptr); + + ClientConfig config; + config.accountId = accountId; + config.nameSpace = nameSpace; + REQUIRE(SFSClient::Make(config, sfsClient) == Result::Success); + REQUIRE(sfsClient != nullptr); + } + + SECTION("Make(accountId, instanceId, namespace, logCallbackFn, out) works") + { + SECTION("Using a lambda with {} initialization") + { + REQUIRE(SFSClient::Make({accountId, instanceId, nameSpace, [](const LogData&) {}}, sfsClient) == + Result::Success); + REQUIRE(sfsClient != nullptr); + } + + SECTION("Using a lambda with a ClientConfig object") + { + ClientConfig config{accountId, instanceId, nameSpace, [](const LogData&) {}}; + REQUIRE(SFSClient::Make(config, sfsClient) == Result::Success); + REQUIRE(sfsClient != nullptr); + } + + SECTION("Using a nullptr with a ClientConfig object") + { + ClientConfig config{accountId, instanceId, nameSpace, nullptr}; + REQUIRE(SFSClient::Make(config, sfsClient) == Result::Success); + REQUIRE(sfsClient != nullptr); + } + + SECTION("Using a valid empty-namespace function within a ClientConfig object") + { + ClientConfig config{accountId, instanceId, nameSpace, TestLoggingCallback}; + REQUIRE(SFSClient::Make(config, sfsClient) == Result::Success); + REQUIRE(sfsClient != nullptr); + } + + SECTION("Using a valid static function within a ClientConfig object") + { + ClientConfig config{accountId, instanceId, nameSpace, StaticTestLoggingCallback}; + REQUIRE(SFSClient::Make(config, sfsClient) == Result::Success); + REQUIRE(sfsClient != nullptr); + } + + SECTION("Using a valid static member method within a ClientConfig object") + { + ClientConfig config{accountId, instanceId, nameSpace, &TestLoggingCallbackStruct::TestLoggingCallback}; + REQUIRE(SFSClient::Make(config, sfsClient) == Result::Success); + REQUIRE(sfsClient != nullptr); + } + + SECTION("Can also move a lambda") + { + ClientConfig config{accountId, instanceId, nameSpace, [](const LogData&) {}}; + REQUIRE(SFSClient::Make(std::move(config), sfsClient) == Result::Success); + REQUIRE(sfsClient != nullptr); + } + } + + SECTION("AccountId cannot be empty") + { + REQUIRE(SFSClient::Make({}, sfsClient) == Result::InvalidArg); + REQUIRE(sfsClient == nullptr); + + ClientConfig config; + REQUIRE(SFSClient::Make(config, sfsClient) == Result::InvalidArg); + REQUIRE(sfsClient == nullptr); + + config = {}; + REQUIRE(SFSClient::Make(config, sfsClient) == Result::InvalidArg); + REQUIRE(sfsClient == nullptr); + + config.accountId = std::string(); + REQUIRE(SFSClient::Make(config, sfsClient) == Result::InvalidArg); + REQUIRE(sfsClient == nullptr); + + config.instanceId = instanceId; + REQUIRE(SFSClient::Make(config, sfsClient) == Result::InvalidArg); + REQUIRE(sfsClient == nullptr); + } + +#ifdef __GNUG__ +// For "-Wmissing-field-initializers" +#pragma GCC diagnostic pop +#endif +} + +namespace +{ +void TestProductInRequestParams(const std::function<Result(const RequestParams&)>& apiCall, + const std::function<void()>& checkContents) +{ + RequestParams params; + SECTION("Product must not be empty") + { + const std::string expectedErrorMsg = "product must not be empty"; + + params.productRequests = {{"", {}}}; + auto result = apiCall(params); + REQUIRE(result.GetCode() == Result::InvalidArg); + REQUIRE(result.GetMsg() == expectedErrorMsg); + checkContents(); + + const TargetingAttributes attributes{{"attr1", "value"}}; + params.productRequests = {{"", attributes}}; + result = apiCall(params); + REQUIRE(result.GetCode() == Result::InvalidArg); + REQUIRE(result.GetMsg() == expectedErrorMsg); + checkContents(); + } + + SECTION("Does not allow an empty request") + { + auto result = apiCall(params); + REQUIRE(result.GetCode() == Result::InvalidArg); + REQUIRE(result.GetMsg() == "productRequests cannot be empty"); + checkContents(); + } + + SECTION("Accepting multiple products is not implemented yet") + { + params.productRequests = {{"p1", {}}, {"p2", {}}}; + auto result = apiCall(params); + REQUIRE(result.GetCode() == Result::NotImpl); + REQUIRE(result.GetMsg() == "There cannot be more than 1 productRequest at the moment"); + checkContents(); + } + + SECTION("Fails if base cv is not correct") + { + params.productRequests = {{"p1", {}}}; + params.baseCV = ""; + auto result = apiCall(params); + REQUIRE(result.GetCode() == Result::InvalidArg); + REQUIRE(result.GetMsg() == "cv must not be empty"); + checkContents(); + + params.baseCV = "cv"; + result = apiCall(params); + REQUIRE(result.GetCode() == Result::InvalidArg); + REQUIRE(result.GetMsg().find("baseCV is not a valid correlation vector:") == 0); + checkContents(); + } +} +} // namespace + +TEST("Testing SFSClient::GetLatestDownloadInfo()") +{ + auto sfsClient = GetSFSClient(); + std::vector<Content> contents; + + TestProductInRequestParams( + [&](const RequestParams& params) { return sfsClient->GetLatestDownloadInfo(params, contents); }, + [&contents] { REQUIRE(contents.empty()); }); +} + +TEST("Testing SFSClient::GetAppLatestDownloadInfo()") +{ + SECTION("With storeapps instance") + { + auto sfsClient = GetSFSClient("storeapps"); + std::vector<AppContent> contents; + + TestProductInRequestParams( + [&](const RequestParams& params) { return sfsClient->GetLatestAppDownloadInfo(params, contents); }, + [&contents] { REQUIRE(contents.empty()); }); + } + + SECTION("Fails if not storeapps instanceId") + { + auto sfsClient = GetSFSClient("testInstanceId"); + std::vector<AppContent> contents; + RequestParams params; + params.productRequests = {{"a", {}}}; + auto result = sfsClient->GetLatestAppDownloadInfo(params, contents); + REQUIRE(result.GetCode() == Result::Unexpected); + REQUIRE(result.GetMsg() == "At this moment only the \"storeapps\" instanceId can send app requests"); + REQUIRE(contents.empty()); + } +} diff --git a/src/SfsClient/sfs-client/client/tests/unit/details/CurlConnectionManagerTests.cpp b/src/SfsClient/sfs-client/client/tests/unit/details/CurlConnectionManagerTests.cpp @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "ReportingHandler.h" +#include "connection/Connection.h" +#include "connection/CurlConnection.h" +#include "connection/CurlConnectionManager.h" + +#include <catch2/catch_test_macros.hpp> +#include <curl/curl.h> + +using namespace SFS; +using namespace SFS::details; + +#define TEST(...) TEST_CASE("[CurlConnectionManagerTests] " __VA_ARGS__) + +TEST("Testing expected values in curl_version_info_data") +{ + curl_version_info_data* ver = curl_version_info(CURLVERSION_NOW); + REQUIRE(ver != nullptr); + CHECK(ver->features & CURL_VERSION_SSL); + + // Checking thread safety is on + CHECK(ver->features & CURL_VERSION_THREADSAFE); + + // For thread safety we need the DNS resolutions to be asynchronous (which happens because of c-ares) + CHECK(ver->features & CURL_VERSION_ASYNCHDNS); +} + +TEST("Testing CurlConnectionManager()") +{ + ReportingHandler handler; + CurlConnectionManager curlConnectionManager(handler); + + // Check that the CurlConnectionManager generates a CurlConnection object + std::unique_ptr<Connection> Connection = curlConnectionManager.MakeConnection({}); + REQUIRE(Connection != nullptr); + REQUIRE(dynamic_cast<CurlConnection*>(Connection.get()) != nullptr); + + // Having many CurlConnectionManager objects should not cause any issues, curl is smart enough to + // handle multiple initialization and cleanup calls + CurlConnectionManager curlConnectionManager2(handler); + auto Connection2 = curlConnectionManager2.MakeConnection({}); + + CurlConnectionManager curlConnectionManager3(handler); + auto Connection3 = curlConnectionManager3.MakeConnection({}); + auto Connection4 = curlConnectionManager3.MakeConnection({}); +} diff --git a/src/SfsClient/sfs-client/client/tests/unit/details/CurlConnectionTests.cpp b/src/SfsClient/sfs-client/client/tests/unit/details/CurlConnectionTests.cpp @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "../../util/SFSExceptionMatcher.h" +#include "../../util/TestHelper.h" +#include "ReportingHandler.h" +#include "Result.h" +#include "connection/CurlConnection.h" +#include "connection/mock/MockConnection.h" + +#include <catch2/catch_test_macros.hpp> +#include <catch2/matchers/catch_matchers_string.hpp> +#include <nlohmann/json.hpp> + +#define TEST(...) TEST_CASE("[CurlConnectionTests] " __VA_ARGS__) + +using namespace SFS; +using namespace SFS::details; +using namespace SFS::test; +using json = nlohmann::json; + +namespace +{ +class MockCurlConnection : public CurlConnection +{ + public: + MockCurlConnection(const ReportingHandler& handler, Result::Code& responseCode, std::string& response) + : CurlConnection({}, handler) + , m_responseCode(responseCode) + , m_response(response) + { + } + + protected: + std::string CurlPerform(const std::string&, CurlHeaderList&) override + { + if (m_responseCode == Result::Success) + { + return m_response; + } + throw SFSException(m_responseCode); + } + + private: + Result::Code& m_responseCode; + std::string& m_response; +}; +} // namespace + +TEST("Testing CurlConnection()") +{ + ReportingHandler handler; + handler.SetLoggingCallback(LogCallbackToTest); + Result::Code responseCode = Result::HttpNotFound; + std::string response = "expected"; + std::unique_ptr<Connection> connection = std::make_unique<MockCurlConnection>(handler, responseCode, response); + + SECTION("Testing CurlConnection::Get()") + { + std::string out; + REQUIRE_THROWS_CODE(out = connection->Get("url"), HttpNotFound); + REQUIRE(out.empty()); + + responseCode = Result::Success; + REQUIRE_NOTHROW(out = connection->Get("url")); + REQUIRE(out == response); + } + + SECTION("Testing CurlConnection::Post()") + { + std::string out; + REQUIRE_THROWS_CODE(out = connection->Post("url"), HttpNotFound); + REQUIRE(out.empty()); + + const json body = {{{"dummy", {}}}}; + REQUIRE_THROWS_CODE(out = connection->Post("url", body.dump()), HttpNotFound); + REQUIRE(out.empty()); + + responseCode = Result::Success; + REQUIRE_NOTHROW(out = connection->Get("url")); + REQUIRE(out == response); + + response.clear(); + REQUIRE_NOTHROW(out = connection->Post("url", body.dump())); + REQUIRE(out == response); + } +} + +TEST("Testing CurlConnection constructor passing a cv") +{ + ReportingHandler handler; + handler.SetLoggingCallback(LogCallbackToTest); + + ConnectionConfig config; + config.baseCV = ""; + + REQUIRE_THROWS_CODE_MSG(std::make_unique<CurlConnection>(config, handler), InvalidArg, "cv must not be empty"); + + config.baseCV = "cv"; + REQUIRE_THROWS_CODE_MSG_MATCHES(std::make_unique<CurlConnection>(config, handler), + InvalidArg, + Catch::Matchers::ContainsSubstring("baseCV is not a valid correlation vector:")); + + config.baseCV = "aaaaaaaaaaaaaaaa.1"; + REQUIRE_NOTHROW(std::make_unique<CurlConnection>(config, handler)); +} diff --git a/src/SfsClient/sfs-client/client/tests/unit/details/EnvTests.cpp b/src/SfsClient/sfs-client/client/tests/unit/details/EnvTests.cpp @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "Env.h" + +#include <catch2/catch_test_macros.hpp> + +#define TEST(...) TEST_CASE("[EnvTests] " __VA_ARGS__) + +using namespace SFS::details::env; + +TEST("Testing GetEnv()") +{ + SECTION("Testing GetEnv() on an existing environment variable") + { + // Get the value of an existing environment variable per platform +#ifdef _WIN32 + const std::string varName = "COMPUTERNAME"; +#else + const std::string varName = "LANG"; +#endif + auto env = GetEnv(varName); + REQUIRE(env.has_value()); + REQUIRE(env.value().size() > 0); + } + + SECTION("Testing GetEnv() on a non-existing variable") + { + auto env = GetEnv("DUMMYVARIABLESHOULDNOTEXIST"); + REQUIRE(!env.has_value()); + } +} + +TEST("Testing SetEnv()") +{ + SECTION("Testing SetEnv() on a non-existing variable") + { + auto env = GetEnv("DUMMYVARIABLE"); + REQUIRE(!env.has_value()); + + REQUIRE(SetEnv("DUMMYVARIABLE", "dummyValue")); + env = GetEnv("DUMMYVARIABLE"); + REQUIRE(env.has_value()); + REQUIRE(*env == "dummyValue"); + + SECTION("Testing SetEnv() on an existing variable overwrites it") + { + REQUIRE(SetEnv("DUMMYVARIABLE", "dummyValue2")); + + env = GetEnv("DUMMYVARIABLE"); + REQUIRE(env.has_value()); + REQUIRE(*env == "dummyValue2"); + } + + INFO("Unsetting the environment variable"); + REQUIRE(UnsetEnv("DUMMYVARIABLE")); + env = GetEnv("DUMMYVARIABLE"); + REQUIRE(!env.has_value()); + } + + SECTION("Testing SetEnv() with empty strings fails") + { + REQUIRE_FALSE(SetEnv("DUMMYVARIABLE", "")); + REQUIRE_FALSE(SetEnv("DUMMYVARIABLE", std::string())); + REQUIRE_FALSE(SetEnv("", "dummy")); + REQUIRE_FALSE(SetEnv(std::string(), "dummy")); + REQUIRE_FALSE(SetEnv(std::string(), std::string())); + } +} + +TEST("Testing UnsetEnv()") +{ + SECTION("Testing UnsetEnv() on a non-existing variable still succeeds") + { + auto env = GetEnv("DUMMYVARIABLE"); + REQUIRE(!env.has_value()); + + REQUIRE(UnsetEnv("DUMMYVARIABLE")); + } +} + +TEST("Testing ScopedEnv") +{ + SECTION("Testing ScopedEnv on a non-existing variable") + { + auto env = GetEnv("DUMMYVARIABLE"); + REQUIRE(!env.has_value()); + + { + ScopedEnv scopedEnv("DUMMYVARIABLE", "dummyValue"); + + INFO("Variable exists within scope"); + env = GetEnv("DUMMYVARIABLE"); + REQUIRE(env.has_value()); + REQUIRE(*env == "dummyValue"); + + SECTION("Testing ScopedEnv on an existing variable overwrites it") + { + { + ScopedEnv scopedEnv2("DUMMYVARIABLE", "dummyValue2"); + + INFO("Variable has value overwritten within scope"); + env = GetEnv("DUMMYVARIABLE"); + REQUIRE(env.has_value()); + REQUIRE(*env == "dummyValue2"); + } + + INFO("Variable goes back to previous value"); + env = GetEnv("DUMMYVARIABLE"); + REQUIRE(env.has_value()); + REQUIRE(*env == "dummyValue"); + } + } + + INFO("Variable should be unset after the scope ends"); + env = GetEnv("DUMMYVARIABLE"); + REQUIRE(!env.has_value()); + } +} diff --git a/src/SfsClient/sfs-client/client/tests/unit/details/ErrorHandlingTests.cpp b/src/SfsClient/sfs-client/client/tests/unit/details/ErrorHandlingTests.cpp @@ -0,0 +1,255 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "ErrorHandling.h" +#include "ReportingHandler.h" +#include "Result.h" +#include "SFSException.h" + +#include <catch2/catch_test_macros.hpp> + +#define TEST(...) TEST_CASE("[ErrorHandlingTests] " __VA_ARGS__) + +using namespace SFS; +using namespace SFS::details; + +namespace +{ +Result TestSFS_Catch_Return_bad_alloc() +try +{ + throw std::bad_alloc(); +} +SFS_CATCH_RETURN(); + +class MyException : public std::exception +{ +}; + +Result TestSFS_Catch_Return_std_exception() +try +{ + throw MyException(); +} +SFS_CATCH_RETURN(); + +Result TestSFS_Catch_Return_unknown() +try +{ + throw std::error_code(); +} +SFS_CATCH_RETURN(); +} // namespace + +TEST("Testing ErrorHandling's SFS_CATCH_RETURN()") +{ + REQUIRE(TestSFS_Catch_Return_bad_alloc() == Result::OutOfMemory); + REQUIRE(TestSFS_Catch_Return_std_exception() == Result::Unexpected); + REQUIRE(TestSFS_Catch_Return_unknown() == Result::Unexpected); +} + +namespace +{ +void TestSFS_Catch_Log_Rethrow_bad_alloc(const ReportingHandler& handler) +try +{ + throw std::bad_alloc(); +} +SFS_CATCH_LOG_RETHROW(handler); + +void TestSFS_Catch_Log_Rethrow_SFSException(const ReportingHandler& handler) +try +{ + throw SFS::details::SFSException(Result::Unexpected); +} +SFS_CATCH_LOG_RETHROW(handler); +} // namespace + +TEST("Testing ErrorHandling's SFS_CATCH_LOG_RETHROW()") +{ + ReportingHandler handler; + bool called = false; + handler.SetLoggingCallback([&](const auto&) { called = true; }); + + SECTION("Test that SFS_CATCH_LOG_RETHROW does not rethrow if not SFSException") + { + REQUIRE_THROWS_AS(TestSFS_Catch_Log_Rethrow_bad_alloc(handler), std::bad_alloc); + REQUIRE(!called); + } + + SECTION("Test that SFS_CATCH_LOG_RETHROW rethrows and logs if SFSException") + { + REQUIRE_THROWS_AS(TestSFS_Catch_Log_Rethrow_SFSException(handler), SFSException); + REQUIRE(called); + } +} + +namespace +{ +Result TestSFS_ReturnIfFailed(const Result& result, const Result& ifNotFailed = Result::Success) +{ + RETURN_IF_FAILED(result); + return ifNotFailed; +} +} // namespace + +TEST("Testing ErrorHandling's RETURN_IF_FAILED()") +{ + SECTION("Test that RETURN_IF_FAILED returns the result if it is a failure") + { + REQUIRE(TestSFS_ReturnIfFailed(Result::Code::Unexpected) == Result::Code::Unexpected); + } + + SECTION("Test that RETURN_IF_FAILED does not return if the result is a success") + { + REQUIRE(TestSFS_ReturnIfFailed(Result::Code::Success) == Result::Code::Success); + REQUIRE(TestSFS_ReturnIfFailed(Result::Code::Success, Result::Code::Unexpected) == Result::Code::Unexpected); + } +} + +namespace +{ +Result TestSFS_ReturnIfFailedLog(const ReportingHandler& handler, + const Result& result, + const Result& ifNotFailed = Result::Success) +{ + RETURN_IF_FAILED_LOG(result, handler); + return ifNotFailed; +} +} // namespace + +TEST("Testing ErrorHandling's RETURN_IF_FAILED_LOG()") +{ + ReportingHandler handler; + bool called = false; + handler.SetLoggingCallback([&](const auto&) { called = true; }); + + SECTION("Test that RETURN_IF_FAILED_LOG returns and logs if the result is a failure") + { + REQUIRE(TestSFS_ReturnIfFailedLog(handler, Result::Code::Unexpected) == Result::Code::Unexpected); + REQUIRE(called); + } + + SECTION("Test that RETURN_IF_FAILED_LOG does not return and log if the result is a success") + { + REQUIRE(TestSFS_ReturnIfFailedLog(handler, Result::Code::Success) == Result::Code::Success); + REQUIRE(!called); + } +} + +TEST("Testing ErrorHandling's LOG_IF_FAILED()") +{ + ReportingHandler handler; + bool called = false; + handler.SetLoggingCallback([&](const auto&) { called = true; }); + + SECTION("Test that LOG_IF_FAILED logs if the result is a failure") + { + LOG_IF_FAILED(Result(Result::Unexpected), handler); + REQUIRE(called); + } + + SECTION("Test that LOG_IF_FAILED does not log if the result is a success") + { + LOG_IF_FAILED(Result(Result::Success), handler); + REQUIRE(!called); + } +} + +namespace +{ +void TestSFS_ThrowLog(const ReportingHandler& handler, const Result& result) +{ + THROW_LOG(result, handler); +} +} // namespace + +TEST("Testing ErrorHandling's THROW_LOG()") +{ + INFO("Test that ErrorHandling's THROW_LOG throws and logs the result"); + + ReportingHandler handler; + bool called = false; + handler.SetLoggingCallback([&](const auto&) { called = true; }); + + REQUIRE_THROWS_AS(TestSFS_ThrowLog(handler, Result::Code::Unexpected), SFSException); + REQUIRE(called); +} + +namespace +{ +void TestSFS_ThrowIfFailedLog(const ReportingHandler& handler, const Result& result) +{ + THROW_IF_FAILED_LOG(result, handler); +} +} // namespace + +TEST("Testing ErrorHandling's THROW_IF_FAILED_LOG()") +{ + ReportingHandler handler; + bool called = false; + handler.SetLoggingCallback([&](const auto&) { called = true; }); + + SECTION("Test that ErrorHandling's THROW_IF_FAILED_LOG throws and logs the result if it is a failure") + { + REQUIRE_THROWS_AS(TestSFS_ThrowIfFailedLog(handler, Result::Code::Unexpected), SFSException); + REQUIRE(called); + } + + SECTION("Test that ErrorHandling's THROW_IF_FAILED_LOG does not throw and log the result if it is not a failure") + { + REQUIRE_NOTHROW(TestSFS_ThrowIfFailedLog(handler, Result::Code::Success)); + REQUIRE(!called); + } +} + +TEST("Testing ErrorHandling's THROW_CODE_IF()") +{ + SECTION("Test that THROW_CODE_IF throws if the condition is true") + { + REQUIRE_THROWS_AS([]() { THROW_CODE_IF(Unexpected, true); }(), SFSException); + } + + SECTION("Test that THROW_CODE_IF does not throw if the condition is false") + { + REQUIRE_NOTHROW([]() { THROW_CODE_IF(Unexpected, false); }()); + } +} + +TEST("Testing ErrorHandling's THROW_CODE_IF_LOG()") +{ + ReportingHandler handler; + bool called = false; + handler.SetLoggingCallback([&](const auto&) { called = true; }); + + SECTION("Test that THROW_CODE_IF_LOG throws and logs the result if the condition is true") + { + REQUIRE_THROWS_AS([&handler]() { THROW_CODE_IF_LOG(Unexpected, true, handler); }(), SFSException); + REQUIRE(called); + } + + SECTION("Test that THROW_CODE_IF_LOG does not throw and log if the condition is false") + { + REQUIRE_NOTHROW([&handler]() { THROW_CODE_IF_LOG(Unexpected, false, handler); }()); + REQUIRE(!called); + } +} + +TEST("Testing ErrorHandling's THROW_CODE_IF_NOT_LOG()") +{ + ReportingHandler handler; + bool called = false; + handler.SetLoggingCallback([&](const auto&) { called = true; }); + + SECTION("Test that THROW_CODE_IF_NOT_LOG throws and logs the result if the condition is false") + { + REQUIRE_THROWS_AS([&handler]() { THROW_CODE_IF_NOT_LOG(Unexpected, false, handler); }(), SFSException); + REQUIRE(called); + } + + SECTION("Test that THROW_CODE_IF_NOT_LOG does not throw and log if the condition is true") + { + REQUIRE_NOTHROW([&handler]() { THROW_CODE_IF_NOT_LOG(Unexpected, true, handler); }()); + REQUIRE(!called); + } +} diff --git a/src/SfsClient/sfs-client/client/tests/unit/details/ReportingHandlerTests.cpp b/src/SfsClient/sfs-client/client/tests/unit/details/ReportingHandlerTests.cpp @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "ReportingHandler.h" + +#include <catch2/catch_test_macros.hpp> + +#include <chrono> +#include <mutex> +#include <optional> +#include <thread> + +#define TEST(...) TEST_CASE("[ReportingHandlerTests] " __VA_ARGS__) + +using namespace SFS; +using namespace SFS::details; + +TEST("Testing SetLoggingCallback()") +{ + ReportingHandler handler; + + bool called = false; + auto handling = [&](const LogData&) { called = true; }; + + handler.SetLoggingCallback(handling); + REQUIRE_FALSE(called); + + LOG_INFO(handler, "Test"); + + called = false; + handler.SetLoggingCallback(nullptr); + + LOG_INFO(handler, "Test"); + REQUIRE_FALSE(called); +} + +TEST("Testing Severities") +{ + ReportingHandler handler; + + std::optional<LogSeverity> severity; + auto handling = [&](const LogData& data) { severity = data.severity; }; + + handler.SetLoggingCallback(handling); + + REQUIRE(!severity.has_value()); + + LOG_INFO(handler, "Test"); + REQUIRE(severity.has_value()); + REQUIRE(*severity == LogSeverity::Info); + severity.reset(); + + LOG_WARNING(handler, "Test"); + REQUIRE(severity.has_value()); + REQUIRE(*severity == LogSeverity::Warning); + severity.reset(); + + LOG_ERROR(handler, "Test"); + REQUIRE(severity.has_value()); + REQUIRE(*severity == LogSeverity::Error); + severity.reset(); + + LOG_VERBOSE(handler, "Test"); + REQUIRE(severity.has_value()); + REQUIRE(*severity == LogSeverity::Verbose); + + handler.SetLoggingCallback(nullptr); +} + +TEST("Testing file/line/function") +{ + ReportingHandler handler; + + std::string file; + int line = 0; + std::string function; + auto handling = [&](const LogData& data) { + file = std::string(data.file); + line = data.line; + function = std::string(data.function); + }; + + handler.SetLoggingCallback(handling); + + LOG_INFO(handler, "Test"); + CHECK(file.find("ReportingHandlerTests.cpp") != std::string::npos); + CHECK(line == (__LINE__ - 2)); + CHECK(function == "CATCH2_INTERNAL_TEST_4"); + + LOG_WARNING(handler, "Test"); + CHECK(file.find("ReportingHandlerTests.cpp") != std::string::npos); + CHECK(line == (__LINE__ - 2)); + CHECK(function == "CATCH2_INTERNAL_TEST_4"); +} + +TEST("Testing LogFormatting") +{ + ReportingHandler handler; + + std::string message; + auto handling = [&](const LogData& data) { message = data.message; }; + + handler.SetLoggingCallback(handling); + + REQUIRE(message.empty()); + + LOG_INFO(handler, "Test %s", "Test"); + REQUIRE(message == "Test Test"); + + LOG_WARNING(handler, "Test %s %s", "Test1", "Test2"); + REQUIRE(message == "Test Test1 Test2"); + + LOG_ERROR(handler, "Test %s %s %s", "Test1", "Test2", "Test3"); + REQUIRE(message == "Test Test1 Test2 Test3"); + + LOG_INFO(handler, "Test %d %d", 1, true); + REQUIRE(message == "Test 1 1"); + + LOG_INFO(handler, "Test %d %s", 2, false ? "true" : "false"); + REQUIRE(message == "Test 2 false"); + + handler.SetLoggingCallback(nullptr); +} + +TEST("Testing setting another logging callback waits for an existing call to finish") +{ + ReportingHandler handler; + + // Set a callback that will be blocked by a mutex + std::mutex mutex; + bool called = false; + std::chrono::time_point<std::chrono::system_clock> time1; + bool startedCall = false; + auto handling = [&](const LogData& logData) { + startedCall = true; + std::lock_guard guard(mutex); + called = true; + time1 = logData.time; + }; + + handler.SetLoggingCallback(handling); + + // Make sure the callback is blocked + std::unique_lock lock(mutex); + + // Spawn a thread that will be blocked by the callback + std::thread t([&]() { LOG_INFO(handler, "Test"); }); + + LoggingCallbackFn anotherHandling; + SECTION("Setting another callback") + { + anotherHandling = [&](const LogData&) {}; + } + SECTION("Setting a nullptr callback") + { + anotherHandling = nullptr; + } + + // Spawn a second thread that tries to set another callback + std::chrono::time_point<std::chrono::system_clock> time2; + std::thread t2([&]() { + // Make sure the callback has started and is now blocked + while (!startedCall) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + // Now setting another callback should be blocked until we unlock the mutex + handler.SetLoggingCallback(std::move(anotherHandling)); + INFO("The first callback should have been called at this point"); + REQUIRE(called); + time2 = std::chrono::system_clock::now(); + }); + + // Unlocking the mutex will allow the threads to continue + lock.unlock(); + t.join(); + t2.join(); + + INFO("The first callback should have been called before the second callback was set"); + REQUIRE(time1 < time2); +} + +TEST("Testing ToString(LogSeverity)") +{ + REQUIRE(SFS::ToString(LogSeverity::Info) == "Info"); + REQUIRE(SFS::ToString(LogSeverity::Warning) == "Warning"); + REQUIRE(SFS::ToString(LogSeverity::Error) == "Error"); + REQUIRE(SFS::ToString(LogSeverity::Verbose) == "Verbose"); +} diff --git a/src/SfsClient/sfs-client/client/tests/unit/details/SFSClientImplTests.cpp b/src/SfsClient/sfs-client/client/tests/unit/details/SFSClientImplTests.cpp @@ -0,0 +1,410 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "../../util/SFSExceptionMatcher.h" +#include "../../util/TestHelper.h" +#include "SFSClientImpl.h" +#include "TestOverride.h" +#include "connection/Connection.h" +#include "connection/ConnectionManager.h" +#include "connection/CurlConnection.h" +#include "connection/CurlConnectionManager.h" +#include "connection/mock/MockConnectionManager.h" + +#include <catch2/catch_test_macros.hpp> +#include <nlohmann/json.hpp> + +#define TEST(...) TEST_CASE("[SFSClientImplTests] " __VA_ARGS__) + +using namespace SFS; +using namespace SFS::details; +using namespace SFS::test; +using json = nlohmann::json; + +namespace +{ +class MockCurlConnection : public CurlConnection +{ + public: + MockCurlConnection(const ReportingHandler& handler, + Result::Code& responseCode, + std::string& getResponse, + std::string& postResponse, + bool& expectEmptyPostBody) + : CurlConnection({}, handler) + , m_responseCode(responseCode) + , m_getResponse(getResponse) + , m_postResponse(postResponse) + , m_expectEmptyPostBody(expectEmptyPostBody) + { + } + + std::string Get(const std::string&) override + { + if (m_responseCode == Result::Success) + { + INFO("MockCurlConnection::Get() called, response: " << m_getResponse); + return m_getResponse; + } + throw SFSException(m_responseCode); + } + + std::string Post(const std::string&, const std::string& data) override + { + if (m_responseCode == Result::Success) + { + INFO("MockCurlConnection::Post() called, response: " << m_postResponse); + if (m_expectEmptyPostBody) + { + REQUIRE(data.empty()); + } + else + { + REQUIRE(!data.empty()); + } + return m_postResponse; + } + throw SFSException(m_responseCode); + } + + private: + Result::Code& m_responseCode; + std::string& m_getResponse; + std::string& m_postResponse; + bool& m_expectEmptyPostBody; +}; + +void CheckProduct(const VersionEntity& entity, std::string_view ns, std::string_view name, std::string_view version) +{ + REQUIRE(entity.GetContentType() == ContentType::Generic); + REQUIRE(entity.contentId.nameSpace == ns); + REQUIRE(entity.contentId.name == name); + REQUIRE(entity.contentId.version == version); +} + +void CheckDownloadInfo(const FileEntities& files, const std::string& name) +{ + REQUIRE(files.size() == 2); + REQUIRE(files[0]->fileId == (name + ".json")); + REQUIRE(files[0]->url == ("http://localhost/1.json")); + REQUIRE(files[1]->fileId == (name + ".bin")); + REQUIRE(files[1]->url == ("http://localhost/2.bin")); +} +} // namespace + +TEST("Testing class SFSClientImpl()") +{ + const std::string ns = "testNameSpace"; + SFSClientImpl<CurlConnectionManager> sfsClient({"testAccountId", "testInstanceId", ns, LogCallbackToTest}); + + Result::Code responseCode = Result::Success; + std::string getResponse; + std::string postResponse; + bool expectEmptyPostBody = true; + std::unique_ptr<Connection> connection = std::make_unique<MockCurlConnection>(sfsClient.GetReportingHandler(), + responseCode, + getResponse, + postResponse, + expectEmptyPostBody); + + const std::string productName = "productName"; + const std::string expectedVersion = "0.0.0.2"; + + SECTION("Testing SFSClientImpl::GetLatestVersion()") + { + expectEmptyPostBody = false; + std::unique_ptr<VersionEntity> entity; + + SECTION("Expected response") + { + const json latestVersionResponse = { + {"ContentId", {{"Namespace", ns}, {"Name", productName}, {"Version", expectedVersion}}}}; + postResponse = latestVersionResponse.dump(); + SECTION("No attributes") + { + REQUIRE_NOTHROW(entity = sfsClient.GetLatestVersion({productName, {}}, *connection)); + REQUIRE(entity); + CheckProduct(*entity, ns, productName, expectedVersion); + } + + SECTION("With attributes") + { + const TargetingAttributes attributes{{"attr1", "value"}}; + REQUIRE_NOTHROW(entity = sfsClient.GetLatestVersion({productName, attributes}, *connection)); + REQUIRE(entity); + CheckProduct(*entity, ns, productName, expectedVersion); + } + + SECTION("Failing") + { + responseCode = Result::HttpNotFound; + REQUIRE_THROWS_CODE(entity = sfsClient.GetLatestVersion({"badName", {}}, *connection), HttpNotFound); + REQUIRE(!entity); + + const TargetingAttributes attributes{{"attr1", "value"}}; + REQUIRE_THROWS_CODE(entity = sfsClient.GetLatestVersion({"badName", attributes}, *connection), + HttpNotFound); + REQUIRE(!entity); + } + } + + SECTION("Unexpected response") + { + SECTION("Wrong ns") + { + const json latestVersionResponse = { + {"ContentId", {{"Namespace", "wrong"}, {"Name", productName}, {"Version", expectedVersion}}}}; + postResponse = latestVersionResponse.dump(); + } + + SECTION("Wrong name") + { + const json latestVersionResponse = { + {"ContentId", {{"Namespace", ns}, {"Name", "wrong"}, {"Version", expectedVersion}}}}; + postResponse = latestVersionResponse.dump(); + } + + REQUIRE_THROWS_CODE_MSG(entity = sfsClient.GetLatestVersion({productName, {}}, *connection), + ServiceInvalidResponse, + "Response does not match the requested product"); + REQUIRE(!entity); + } + } + + SECTION("Testing SFSClientImpl::GetLatestVersionBatch()") + { + expectEmptyPostBody = false; + json latestVersionResponse = json::array(); + latestVersionResponse.push_back( + {{"ContentId", {{"Namespace", ns}, {"Name", productName}, {"Version", expectedVersion}}}}); + postResponse = latestVersionResponse.dump(); + VersionEntities entities; + SECTION("No attributes") + { + REQUIRE_NOTHROW(entities = sfsClient.GetLatestVersionBatch({{productName, {}}}, *connection)); + REQUIRE(!entities.empty()); + CheckProduct(*entities[0], ns, productName, expectedVersion); + } + + SECTION("With attributes") + { + const TargetingAttributes attributes{{"attr1", "value"}}; + REQUIRE_NOTHROW(entities = sfsClient.GetLatestVersionBatch({{productName, attributes}}, *connection)); + REQUIRE(!entities.empty()); + CheckProduct(*entities[0], ns, productName, expectedVersion); + } + + SECTION("Failing") + { + responseCode = Result::HttpNotFound; + REQUIRE_THROWS_CODE(entities = sfsClient.GetLatestVersionBatch({{"badName", {}}}, *connection), + HttpNotFound); + + const TargetingAttributes attributes{{"attr1", "value"}}; + REQUIRE_THROWS_CODE(entities = sfsClient.GetLatestVersionBatch({{"badName", attributes}}, *connection), + HttpNotFound); + } + } + + SECTION("Testing SFSClientImpl::GetSpecificVersion()") + { + json specificVersionResponse; + specificVersionResponse["ContentId"] = {{"Namespace", ns}, {"Name", productName}, {"Version", expectedVersion}}; + specificVersionResponse["Files"] = json::array({productName + ".json", productName + ".bin"}); + getResponse = specificVersionResponse.dump(); + std::unique_ptr<VersionEntity> entity; + SECTION("Getting version") + { + REQUIRE_NOTHROW(entity = sfsClient.GetSpecificVersion(productName, expectedVersion, *connection)); + REQUIRE(entity); + CheckProduct(*entity, ns, productName, expectedVersion); + } + + SECTION("Failing") + { + responseCode = Result::HttpNotFound; + REQUIRE_THROWS_CODE(entity = sfsClient.GetSpecificVersion(productName, expectedVersion, *connection), + HttpNotFound); + } + } + + SECTION("Testing SFSClientImpl::GetDownloadInfo()") + { + expectEmptyPostBody = true; + json downloadInfoResponse; + downloadInfoResponse = json::array(); + downloadInfoResponse.push_back({{"Url", "http://localhost/1.json"}, + {"FileId", productName + ".json"}, + {"SizeInBytes", 100}, + {"Hashes", {{"Sha1", "123"}, {"Sha256", "456"}}}}); + downloadInfoResponse[0]["DeliveryOptimization"] = {{"CatalogId", "789"}}; + downloadInfoResponse[0]["DeliveryOptimization"]["Properties"] = { + {"IntegrityCheckInfo", {{"PiecesHashFileUrl", "http://localhost/1.json"}, {"HashOfHashes", "abc"}}}}; + + downloadInfoResponse.push_back({{"Url", "http://localhost/2.bin"}, + {"FileId", productName + ".bin"}, + {"SizeInBytes", 200}, + {"Hashes", {{"Sha1", "421"}, {"Sha256", "132"}}}}); + downloadInfoResponse[1]["DeliveryOptimization"] = downloadInfoResponse[0]["DeliveryOptimization"]; + postResponse = downloadInfoResponse.dump(); + + FileEntities files; + SECTION("Getting version") + { + REQUIRE_NOTHROW(files = sfsClient.GetDownloadInfo(productName, expectedVersion, *connection)); + REQUIRE(!files.empty()); + CheckDownloadInfo(files, productName); + } + + SECTION("Failing") + { + responseCode = Result::HttpNotFound; + REQUIRE_THROWS_CODE(files = sfsClient.GetDownloadInfo(productName, expectedVersion, *connection), + HttpNotFound); + REQUIRE(files.empty()); + } + } +} + +TEST("Testing ClientConfig validation") +{ + SECTION("accountId must not be empty") + { + const std::string expectedErrorMsg = "ClientConfig::accountId must not be empty"; + for (size_t i = 0; i <= 10; ++i) + { + ClientConfig config; + config.accountId = std::string(i, 'a'); + if (i >= 1) + { + REQUIRE_NOTHROW(SFSClientImpl<CurlConnectionManager>(std::move(config))); + } + else + { + REQUIRE_THROWS_CODE_MSG(SFSClientImpl<CurlConnectionManager>(std::move(config)), + InvalidArg, + expectedErrorMsg); + } + } + } + + SECTION("instanceId must not be empty") + { + const std::string expectedErrorMsg = "ClientConfig::instanceId must not be empty"; + for (size_t i = 0; i <= 10; ++i) + { + ClientConfig config; + config.accountId = "testAccountId"; + config.instanceId = std::string(i, 'a'); + if (i >= 1) + { + REQUIRE_NOTHROW(SFSClientImpl<CurlConnectionManager>(std::move(config))); + } + else + { + REQUIRE_THROWS_CODE_MSG(SFSClientImpl<CurlConnectionManager>(std::move(config)), + InvalidArg, + expectedErrorMsg); + } + } + } + + SECTION("NameSpace must not be empty") + { + const std::string expectedErrorMsg = "ClientConfig::nameSpace must not be empty"; + for (size_t i = 0; i <= 10; ++i) + { + ClientConfig config; + config.accountId = "testAccountId"; + config.nameSpace = std::string(i, 'a'); + if (i >= 1) + { + REQUIRE_NOTHROW(SFSClientImpl<CurlConnectionManager>(std::move(config))); + } + else + { + REQUIRE_THROWS_CODE_MSG(SFSClientImpl<CurlConnectionManager>(std::move(config)), + InvalidArg, + expectedErrorMsg); + } + } + } +} + +TEST("Testing SFSClientImpl::SetCustomBaseUrl()") +{ + ClientConfig config; + config.accountId = "testAccountId"; + SFSClientImpl<MockConnectionManager> sfsClient(std::move(config)); + + REQUIRE(sfsClient.MakeUrlBuilder().GetUrl() == "https://testAccountId.api.cdp.microsoft.com/"); + + sfsClient.SetCustomBaseUrl("customUrl"); + REQUIRE_THROWS_CODE_MSG(sfsClient.MakeUrlBuilder().GetUrl(), + ConnectionUrlSetupFailed, + "Curl URL error: Bad scheme"); + + sfsClient.SetCustomBaseUrl("http://customUrl.com/"); + REQUIRE(sfsClient.MakeUrlBuilder().GetUrl() == "http://customUrl.com/"); +} + +TEST("Testing test override SFS_TEST_OVERRIDE_BASE_URL") +{ + ClientConfig config; + config.accountId = "testAccountId"; + SFSClientImpl<MockConnectionManager> sfsClient(std::move(config)); + + REQUIRE(sfsClient.MakeUrlBuilder().GetUrl() == "https://testAccountId.api.cdp.microsoft.com/"); + + { + INFO("Can override the base url with the test key"); + ScopedTestOverride override(TestOverride::BaseUrl, "http://override.com"); + if (AreTestOverridesAllowed()) + { + REQUIRE(sfsClient.MakeUrlBuilder().GetUrl() == "http://override.com/"); + } + else + { + REQUIRE(sfsClient.MakeUrlBuilder().GetUrl() == "https://testAccountId.api.cdp.microsoft.com/"); + } + } + + if (AreTestOverridesAllowed()) + { + INFO("Fails if the override is not a valid URL"); + ScopedTestOverride override(TestOverride::BaseUrl, "override"); + { + REQUIRE_THROWS_CODE_MSG(sfsClient.MakeUrlBuilder().GetUrl(), + ConnectionUrlSetupFailed, + "Curl URL error: Bad scheme"); + } + } + + INFO("Override is unset after ScopedEnv goes out of scope"); + REQUIRE(sfsClient.MakeUrlBuilder().GetUrl() == "https://testAccountId.api.cdp.microsoft.com/"); + + sfsClient.SetCustomBaseUrl("http://customUrl.com"); + REQUIRE(sfsClient.MakeUrlBuilder().GetUrl() == "http://customUrl.com/"); + + { + INFO("Can also override a custom base base url with the test key"); + ScopedTestOverride override(TestOverride::BaseUrl, "http://override.com"); + if (AreTestOverridesAllowed()) + { + REQUIRE(sfsClient.MakeUrlBuilder().GetUrl() == "http://override.com/"); + } + else + { + REQUIRE(sfsClient.MakeUrlBuilder().GetUrl() == "http://customUrl.com/"); + } + } + + REQUIRE(sfsClient.MakeUrlBuilder().GetUrl() == "http://customUrl.com/"); +} + +TEST("Testing passing a logging callback to constructor of SFSClientImpl") +{ + SFSClientImpl<MockConnectionManager> sfsClient( + {"testAccountId", "testInstanceId", "testNameSpace", [](const LogData&) {}}); + SFSClientImpl<MockConnectionManager> sfsClient2({"testAccountId", "testInstanceId", "testNameSpace", nullptr}); +} diff --git a/src/SfsClient/sfs-client/client/tests/unit/details/SFSUrlBuilderTests.cpp b/src/SfsClient/sfs-client/client/tests/unit/details/SFSUrlBuilderTests.cpp @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "../../util/SFSExceptionMatcher.h" +#include "../../util/TestHelper.h" +#include "ReportingHandler.h" +#include "SFSUrlBuilder.h" + +#include <catch2/catch_test_macros.hpp> + +#define TEST(...) TEST_CASE("[SFSUrlBuilderTests] " __VA_ARGS__) + +using namespace SFS::details; +using namespace SFS::test; + +const std::string c_accountId = "accountId"; +const std::string c_instanceId = "instanceId"; +const std::string c_nameSpace = "nameSpace"; + +TEST("SFSUrlBuilder") +{ + ReportingHandler handler; + handler.SetLoggingCallback(LogCallbackToTest); + + SECTION("CreateFromAccountId()") + { + SECTION("ASCII strings") + { + SFSUrlBuilder builder(c_accountId, c_instanceId, c_nameSpace, handler); + REQUIRE(builder.GetUrl() == "https://accountId.api.cdp.microsoft.com/"); + + REQUIRE( + builder.GetLatestVersionUrl("product") == + "https://accountId.api.cdp.microsoft.com/api/v2/contents/instanceId/namespaces/nameSpace/names/product/versions/latest?action=select"); + + REQUIRE( + builder.GetLatestVersionBatchUrl() == + "https://accountId.api.cdp.microsoft.com/api/v2/contents/instanceId/namespaces/nameSpace/names?action=BatchUpdates"); + + REQUIRE( + builder.GetSpecificVersionUrl("product", "version") == + "https://accountId.api.cdp.microsoft.com/api/v2/contents/instanceId/namespaces/nameSpace/names/product/versions/version"); + + REQUIRE( + builder.GetDownloadInfoUrl("product", "version") == + "https://accountId.api.cdp.microsoft.com/api/v2/contents/instanceId/namespaces/nameSpace/names/product/versions/version/files?action=GenerateDownloadInfo"); + } + + SECTION("Non-ASCII strings") + { + REQUIRE_THROWS_CODE_MSG(SFSUrlBuilder("a&b", c_instanceId, c_nameSpace, handler), + ConnectionUrlSetupFailed, + "Curl URL error: Bad hostname"); + + REQUIRE_THROWS_CODE_MSG(SFSUrlBuilder("a\nb", c_instanceId, c_nameSpace, handler), + ConnectionUrlSetupFailed, + "Curl URL error: Bad hostname"); + + REQUIRE_THROWS_CODE_MSG(SFSUrlBuilder("a\tb", c_instanceId, c_nameSpace, handler), + ConnectionUrlSetupFailed, + "Curl URL error: Bad hostname"); + + SFSUrlBuilder builder(c_accountId, "instanceId@", "namespace+", handler); + REQUIRE(builder.GetUrl() == "https://accountId.api.cdp.microsoft.com/"); + + REQUIRE( + builder.GetLatestVersionUrl("pr$duct") == + "https://accountId.api.cdp.microsoft.com/api/v2/contents/instanceId%40/namespaces/namespace%2b/names/pr%24duct/versions/latest?action=select"); + + REQUIRE( + builder.GetLatestVersionBatchUrl() == + "https://accountId.api.cdp.microsoft.com/api/v2/contents/instanceId%40/namespaces/namespace%2b/names?action=BatchUpdates"); + + REQUIRE( + builder.GetSpecificVersionUrl("pr$duct", "versi/n") == + "https://accountId.api.cdp.microsoft.com/api/v2/contents/instanceId%40/namespaces/namespace%2b/names/pr%24duct/versions/versi%2fn"); + + REQUIRE( + builder.GetDownloadInfoUrl("pr$duct", "versi/n") == + "https://accountId.api.cdp.microsoft.com/api/v2/contents/instanceId%40/namespaces/namespace%2b/names/pr%24duct/versions/versi%2fn/files?action=GenerateDownloadInfo"); + } + } + + SECTION("CreateFromCustomUrl()") + { + SFSUrlBuilder builder(SFSCustomUrl("http://www.example.com"), c_instanceId, c_nameSpace, handler); + REQUIRE(builder.GetUrl() == "http://www.example.com/"); + + SFSUrlBuilder builder2(SFSCustomUrl("http://www.example2.com"), c_instanceId, c_nameSpace, handler); + REQUIRE(builder2.GetUrl() == "http://www.example2.com/"); + + REQUIRE_THROWS_CODE_MSG(SFSUrlBuilder(SFSCustomUrl("http://www.+.com"), c_instanceId, c_nameSpace, handler), + ConnectionUrlSetupFailed, + "Curl URL error: Bad hostname"); + + REQUIRE_THROWS_CODE_MSG(SFSUrlBuilder(SFSCustomUrl("example"), c_instanceId, c_nameSpace, handler), + ConnectionUrlSetupFailed, + "Curl URL error: Bad scheme"); + } +} diff --git a/src/SfsClient/sfs-client/client/tests/unit/details/TestOverrideTests.cpp b/src/SfsClient/sfs-client/client/tests/unit/details/TestOverrideTests.cpp @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "Env.h" +#include "TestOverride.h" + +#include <catch2/catch_test_macros.hpp> + +#define TEST(...) TEST_CASE("[TestOverrideTests] " __VA_ARGS__) + +using namespace SFS::test; +using namespace SFS::details::env; + +TEST("Testing AreTestOverridesAllowed()") +{ + bool areTestOverridesAllowed = AreTestOverridesAllowed(); +#ifdef SFS_ENABLE_TEST_OVERRIDES + REQUIRE(areTestOverridesAllowed); +#else + REQUIRE_FALSE(areTestOverridesAllowed); +#endif +} + +TEST("Testing GetEnvVarNameFromOverride") +{ + REQUIRE(GetEnvVarNameFromOverride(TestOverride::BaseUrl) == "SFS_TEST_OVERRIDE_BASE_URL"); +} + +TEST("Testing GetTestOverride()") +{ + if (AreTestOverridesAllowed()) + { + SECTION("Testing GetTestOverride() on a non-existing environment variable") + { + auto env = GetTestOverride(TestOverride::BaseUrl); + REQUIRE(!env.has_value()); + + SECTION("Testing GetTestOverride() on an existing environment variable") + { + const std::string varName = GetEnvVarNameFromOverride(TestOverride::BaseUrl); + REQUIRE(SetEnv(varName, "override")); + + env = GetTestOverride(TestOverride::BaseUrl); + REQUIRE(env.has_value()); + REQUIRE(*env == "override"); + + INFO("Unsetting the environment variable"); + REQUIRE(UnsetEnv(varName)); + } + } + } + else + { + SECTION("GetTestOverride() returns std::nullopt when test overrides are not allowed") + { + auto env = GetTestOverride(TestOverride::BaseUrl); + REQUIRE(!env.has_value()); + } + } +} + +TEST("Testing ScopedTestOverride") +{ + SECTION("Testing ScopedTestOverride on a non-existing override") + { + const std::string varName = GetEnvVarNameFromOverride(TestOverride::BaseUrl); + auto env = GetTestOverride(TestOverride::BaseUrl); + REQUIRE(!env.has_value()); + + { + ScopedTestOverride scopedOverride(TestOverride::BaseUrl, "dummyValue"); + + INFO("Variable exists within scope"); + env = GetEnv(varName); + REQUIRE(env.has_value()); + REQUIRE(*env == "dummyValue"); + + if (AreTestOverridesAllowed()) + { + INFO("Checking test override within scope"); + env = GetTestOverride(TestOverride::BaseUrl); + REQUIRE(env.has_value()); + REQUIRE(*env == "dummyValue"); + } + + SECTION("Testing ScopedEnv on an existing variable overwrites it") + { + { + ScopedTestOverride scopedOverride2(TestOverride::BaseUrl, "dummyValue2"); + + INFO("Variable has value overwritten within scope"); + env = GetEnv(varName); + REQUIRE(env.has_value()); + REQUIRE(*env == "dummyValue2"); + + if (AreTestOverridesAllowed()) + { + INFO("Checking test override has been overwritten within scope"); + env = GetTestOverride(TestOverride::BaseUrl); + REQUIRE(env.has_value()); + REQUIRE(*env == "dummyValue2"); + } + } + + INFO("Variable goes back to previous value"); + env = GetEnv(varName); + REQUIRE(env.has_value()); + REQUIRE(*env == "dummyValue"); + + if (AreTestOverridesAllowed()) + { + INFO("Checking test override has gone back to previous value"); + env = GetTestOverride(TestOverride::BaseUrl); + REQUIRE(env.has_value()); + REQUIRE(*env == "dummyValue"); + } + } + } + + INFO("Variable should be unset after the scope ends"); + env = GetEnv(varName); + REQUIRE(!env.has_value()); + + if (AreTestOverridesAllowed()) + { + INFO("Test override should be unset after the scope ends"); + env = GetTestOverride(TestOverride::BaseUrl); + REQUIRE(!env.has_value()); + } + } +} diff --git a/src/SfsClient/sfs-client/client/tests/unit/details/UrlBuilderTests.cpp b/src/SfsClient/sfs-client/client/tests/unit/details/UrlBuilderTests.cpp @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "../../util/SFSExceptionMatcher.h" +#include "../../util/TestHelper.h" +#include "ReportingHandler.h" +#include "UrlBuilder.h" + +#include <catch2/catch_test_macros.hpp> + +#define TEST(...) TEST_CASE("[UrlBuilderTests] " __VA_ARGS__) + +using namespace SFS::details; +using namespace SFS::test; + +TEST("UrlBuilder") +{ + ReportingHandler handler; + handler.SetLoggingCallback(LogCallbackToTest); + + UrlBuilder builder(handler); + REQUIRE_THROWS_CODE_MSG(builder.GetUrl(), ConnectionUrlSetupFailed, "Curl URL error: No host part in the URL"); + + builder.SetHost("www.example.com"); + REQUIRE_THROWS_CODE_MSG(builder.GetUrl(), ConnectionUrlSetupFailed, "Curl URL error: No scheme part in the URL"); + + builder.SetScheme(Scheme::Https); + REQUIRE(builder.GetUrl() == "https://www.example.com/"); + + SECTION("SetHost") + { + builder.SetHost("www.example2.com"); + REQUIRE(builder.GetUrl() == "https://www.example2.com/"); + + REQUIRE_THROWS_CODE_MSG(builder.SetHost("+"), ConnectionUrlSetupFailed, "Curl URL error: Bad hostname"); + REQUIRE_THROWS_CODE_MSG(builder.SetHost("a&b"), ConnectionUrlSetupFailed, "Curl URL error: Bad hostname"); + REQUIRE_THROWS_CODE_MSG(builder.SetHost("a\nb"), ConnectionUrlSetupFailed, "Curl URL error: Bad hostname"); + REQUIRE_THROWS_CODE_MSG(builder.SetHost("a\tb"), ConnectionUrlSetupFailed, "Curl URL error: Bad hostname"); + + REQUIRE_THROWS_CODE_MSG(builder.SetHost(""), InvalidArg, "Host must not empty"); + } + + SECTION("SetPath") + { + builder.SetPath("index.html"); + REQUIRE(builder.GetUrl() == "https://www.example.com/index.html"); + + REQUIRE_THROWS_CODE_MSG(builder.SetPath(""), InvalidArg, "Path must not empty"); + + builder.ResetPath(); + REQUIRE(builder.GetUrl() == "https://www.example.com/"); + } + + SECTION("AppendPath") + { + builder.SetPath("index.html"); + REQUIRE(builder.GetUrl() == "https://www.example.com/index.html"); + + builder.AppendPathEncoded("index.html"); + REQUIRE(builder.GetUrl() == "https://www.example.com/index.html/index.html"); + + builder.AppendPath("a/"); + REQUIRE(builder.GetUrl() == "https://www.example.com/index.html/index.html/a/"); + + builder.AppendPath("b/"); + REQUIRE(builder.GetUrl() == "https://www.example.com/index.html/index.html/a/b/"); + + INFO("Encoding for append includes the / character"); + builder.AppendPathEncoded("c/"); + REQUIRE(builder.GetUrl() == "https://www.example.com/index.html/index.html/a/b/c%2f"); + + INFO("Calling SetPath() resets the path"); + builder.SetPath("index"); + REQUIRE(builder.GetUrl() == "https://www.example.com/index"); + + REQUIRE_THROWS_CODE_MSG(builder.AppendPath(""), InvalidArg, "Path must not empty"); + } + + SECTION("SetQuery, AppendQuery") + { + builder.SetQuery("key", "value"); + REQUIRE(builder.GetUrl() == "https://www.example.com/?key=value"); + + builder.AppendQuery("key2", "value2"); + REQUIRE(builder.GetUrl() == "https://www.example.com/?key=value&key2=value2"); + + builder.SetQuery("key2", "value2"); + REQUIRE(builder.GetUrl() == "https://www.example.com/?key2=value2"); + + builder.AppendQuery("key3", "valu/e@2"); + REQUIRE(builder.GetUrl() == "https://www.example.com/?key2=value2&key3=valu%2fe%402"); + + builder.SetQuery("ke$y4", "valu/e@3"); + REQUIRE(builder.GetUrl() == "https://www.example.com/?ke%24y4=valu%2fe%403"); + + REQUIRE_THROWS_CODE_MSG(builder.SetQuery("", "value"), InvalidArg, "Query key and value must not empty"); + REQUIRE_THROWS_CODE_MSG(builder.SetQuery("key", ""), InvalidArg, "Query key and value must not empty"); + REQUIRE_THROWS_CODE_MSG(builder.SetQuery("", ""), InvalidArg, "Query key and value must not empty"); + + REQUIRE_THROWS_CODE_MSG(builder.AppendQuery("", "value"), InvalidArg, "Query key and value must not empty"); + REQUIRE_THROWS_CODE_MSG(builder.AppendQuery("key", ""), InvalidArg, "Query key and value must not empty"); + REQUIRE_THROWS_CODE_MSG(builder.AppendQuery("", ""), InvalidArg, "Query key and value must not empty"); + + builder.ResetQuery(); + REQUIRE(builder.GetUrl() == "https://www.example.com/"); + } + + SECTION("SetUrl") + { + builder.SetUrl("https://www.example.com/index.html?key=value"); + REQUIRE(builder.GetUrl() == "https://www.example.com/index.html?key=value"); + REQUIRE_THROWS_CODE_MSG(builder.SetUrl("https://www.+.com"), + ConnectionUrlSetupFailed, + "Curl URL error: Bad hostname"); + + REQUIRE_THROWS_CODE_MSG(builder.SetUrl(""), InvalidArg, "Url must not empty"); + } + + SECTION("SetScheme, SetHost, SetPath, SetQuery") + { + builder.SetScheme(Scheme::Https).SetHost("www.example.com").SetPath("index.html").SetQuery("key", "value"); + REQUIRE(builder.GetUrl() == "https://www.example.com/index.html?key=value"); + } + + SECTION("SetScheme, SetHost, AppendPathEncoded, SetQuery") + { + builder.SetScheme(Scheme::Https) + .SetHost("www.example.com") + .AppendPathEncoded("index@.html") + .SetQuery("key", "value"); + REQUIRE(builder.GetUrl() == "https://www.example.com/index%40.html?key=value"); + } + + SECTION("Constructor with URL") + { + UrlBuilder builder2("https://www.example.com/index.html?key=value", handler); + REQUIRE(builder2.GetUrl() == "https://www.example.com/index.html?key=value"); + + REQUIRE_THROWS_CODE_MSG(UrlBuilder("https://www.+.com", handler), + ConnectionUrlSetupFailed, + "Curl URL error: Bad hostname"); + } +} diff --git a/src/SfsClient/sfs-client/client/tests/unit/details/UtilTests.cpp b/src/SfsClient/sfs-client/client/tests/unit/details/UtilTests.cpp @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "Util.h" + +#include <catch2/catch_test_macros.hpp> + +#define TEST(...) TEST_CASE("[UtilTests] " __VA_ARGS__) + +using namespace SFS::details::util; + +TEST("Testing AreEqualI") +{ + SECTION("ASCII strings") + { + REQUIRE(AreEqualI("abc", "abc")); + REQUIRE(AreEqualI("abc", "ABC")); + REQUIRE(AreEqualI("abc", "Abc")); + REQUIRE(AreEqualI("abc", "aBc")); + REQUIRE(AreEqualI("abc", "abC")); + REQUIRE(AreEqualI("abc", "ABC")); + REQUIRE(AreEqualI("abc", "AbC")); + REQUIRE(AreEqualI("abc", "aBC")); + } +} + +TEST("Testing AreNotEqualI") +{ + REQUIRE(AreNotEqualI("abc", "ab")); + REQUIRE(AreNotEqualI("ab", "abc")); + REQUIRE(AreNotEqualI("abc", "abd")); +} diff --git a/src/SfsClient/sfs-client/client/tests/unit/details/entity/FileEntityTests.cpp b/src/SfsClient/sfs-client/client/tests/unit/details/entity/FileEntityTests.cpp @@ -0,0 +1,639 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "../../../util/SFSExceptionMatcher.h" +#include "../../../util/TestHelper.h" +#include "ReportingHandler.h" +#include "entity/FileEntity.h" +#include "sfsclient/AppFile.h" +#include "sfsclient/File.h" + +#include <catch2/catch_test_macros.hpp> +#include <nlohmann/json.hpp> + +#define TEST(...) TEST_CASE("[FileEntityTests] " __VA_ARGS__) + +using namespace SFS; +using namespace SFS::details; +using namespace SFS::test; +using json = nlohmann::json; + +// GenericFileEntity constants +const std::string c_fileId = "fileId"; +const std::string c_url = "url"; +const uint64_t c_size = 123; +const std::string c_sha1 = "sha1"; +const std::string c_sha256 = "sha256"; + +// AppFileEntity constants +const std::string c_fileMoniker = "fileMoniker"; +const std::string c_arch = "amd64"; +const std::string c_applicability = "app"; +ApplicabilityDetailsEntity c_appDetailsEntity{{c_arch}, {c_applicability}}; + +TEST("Testing FileEntity::FromJson()") +{ + ReportingHandler handler; + handler.SetLoggingCallback(LogCallbackToTest); + + SECTION("Generic File Entity") + { + std::unique_ptr<FileEntity> entity; + SECTION("Correct") + { + const json fileEntity = {{"FileId", c_fileId}, + {"Url", c_url}, + {"SizeInBytes", c_size}, + {"Hashes", {{"Sha1", c_sha1}, {"Sha256", c_sha256}}}}; + + REQUIRE_NOTHROW(entity = FileEntity::FromJson(fileEntity, handler)); + REQUIRE(entity != nullptr); + REQUIRE(entity->GetContentType() == ContentType::Generic); + REQUIRE(entity->fileId == c_fileId); + REQUIRE(entity->url == c_url); + REQUIRE(entity->sizeInBytes == c_size); + REQUIRE(entity->hashes.size() == 2); + REQUIRE(entity->hashes.at("Sha1") == c_sha1); + REQUIRE(entity->hashes.at("Sha256") == c_sha256); + } + + SECTION("Missing fields") + { + SECTION("Missing FileId") + { + const json fileEntity = {{"Url", c_url}, + {"SizeInBytes", c_size}, + {"Hashes", {{"Sha1", c_sha1}, {"Sha256", c_sha256}}}}; + REQUIRE_THROWS_CODE_MSG(FileEntity::FromJson(fileEntity, handler), + ServiceInvalidResponse, + "Missing File.FileId in response"); + } + + SECTION("Missing Url") + { + const json fileEntity = {{"FileId", c_fileId}, + {"SizeInBytes", c_size}, + {"Hashes", {{"Sha1", c_sha1}, {"Sha256", c_sha256}}}}; + REQUIRE_THROWS_CODE_MSG(FileEntity::FromJson(fileEntity, handler), + ServiceInvalidResponse, + "Missing File.Url in response"); + } + + SECTION("Missing SizeInBytes") + { + const json fileEntity = {{"FileId", c_fileId}, + {"Url", c_url}, + {"Hashes", {{"Sha1", c_sha1}, {"Sha256", c_sha256}}}}; + REQUIRE_THROWS_CODE_MSG(FileEntity::FromJson(fileEntity, handler), + ServiceInvalidResponse, + "Missing File.SizeInBytes in response"); + } + + SECTION("Missing Hashes") + { + const json fileEntity = {{"FileId", c_fileId}, {"Url", c_url}, {"SizeInBytes", c_size}}; + REQUIRE_THROWS_CODE_MSG(FileEntity::FromJson(fileEntity, handler), + ServiceInvalidResponse, + "Missing File.Hashes in response"); + } + } + + SECTION("Wrong types") + { + SECTION("FileId") + { + const json fileEntity = {{"FileId", 1}, + {"Url", c_url}, + {"SizeInBytes", c_size}, + {"Hashes", {{"Sha1", c_sha1}, {"Sha256", c_sha256}}}}; + REQUIRE_THROWS_CODE_MSG(FileEntity::FromJson(fileEntity, handler), + ServiceInvalidResponse, + "File.FileId is not a string"); + } + + SECTION("Url") + { + const json fileEntity = {{"FileId", c_fileId}, + {"Url", 1}, + {"SizeInBytes", c_size}, + {"Hashes", {{"Sha1", c_sha1}, {"Sha256", c_sha256}}}}; + REQUIRE_THROWS_CODE_MSG(FileEntity::FromJson(fileEntity, handler), + ServiceInvalidResponse, + "File.Url is not a string"); + } + + SECTION("SizeInBytes") + { + const json fileEntity = {{"FileId", c_fileId}, + {"Url", c_url}, + {"SizeInBytes", "size"}, + {"Hashes", {{"Sha1", c_sha1}, {"Sha256", c_sha256}}}}; + REQUIRE_THROWS_CODE_MSG(FileEntity::FromJson(fileEntity, handler), + ServiceInvalidResponse, + "File.SizeInBytes is not an unsigned number"); + } + + SECTION("Hashes not an object") + { + const json fileEntity = {{"FileId", c_fileId}, {"Url", c_url}, {"SizeInBytes", c_size}, {"Hashes", 1}}; + REQUIRE_THROWS_CODE_MSG(FileEntity::FromJson(fileEntity, handler), + ServiceInvalidResponse, + "File.Hashes is not an object"); + } + + SECTION("Hashes.Sha1") + { + const json fileEntity = {{"FileId", c_fileId}, + {"Url", c_url}, + {"SizeInBytes", c_size}, + {"Hashes", {{"Sha1", 1}}}}; + REQUIRE_THROWS_CODE_MSG(FileEntity::FromJson(fileEntity, handler), + ServiceInvalidResponse, + "File.Hashes object value is not a string"); + } + } + } + + SECTION("App File Entity") + { + std::unique_ptr<FileEntity> entity; + const json applicabilityDetails = {{"Architectures", json::array({c_arch})}, + {"PlatformApplicabilityForPackage", json::array({c_applicability})}}; + + SECTION("Correct") + { + const json fileEntity = {{"FileId", c_fileId}, + {"Url", c_url}, + {"SizeInBytes", c_size}, + {"Hashes", {{"Sha1", c_sha1}, {"Sha256", c_sha256}}}, + {"FileMoniker", c_fileMoniker}, + {"ApplicabilityDetails", applicabilityDetails}}; + + REQUIRE_NOTHROW(entity = FileEntity::FromJson(fileEntity, handler)); + REQUIRE(entity != nullptr); + REQUIRE(entity->GetContentType() == ContentType::App); + REQUIRE(entity->fileId == c_fileId); + REQUIRE(entity->url == c_url); + REQUIRE(entity->sizeInBytes == c_size); + REQUIRE(entity->hashes.size() == 2); + REQUIRE(entity->hashes.at("Sha1") == c_sha1); + REQUIRE(entity->hashes.at("Sha256") == c_sha256); + + AppFileEntity* appEntity = dynamic_cast<AppFileEntity*>(entity.get()); + REQUIRE(appEntity->fileMoniker == c_fileMoniker); + REQUIRE(appEntity->applicabilityDetails.architectures.size() == 1); + REQUIRE(appEntity->applicabilityDetails.architectures[0] == c_arch); + REQUIRE(appEntity->applicabilityDetails.platformApplicabilityForPackage.size() == 1); + REQUIRE(appEntity->applicabilityDetails.platformApplicabilityForPackage[0] == c_applicability); + } + + SECTION("Missing fields") + { + SECTION("Missing ApplicabilityDetails") + { + const json fileEntity = {{"FileId", c_fileId}, + {"Url", c_url}, + {"SizeInBytes", c_size}, + {"Hashes", {{"Sha1", c_sha1}, {"Sha256", c_sha256}}}, + {"FileMoniker", c_fileMoniker}}; + REQUIRE_THROWS_CODE_MSG(FileEntity::FromJson(fileEntity, handler), + ServiceInvalidResponse, + "Missing File.ApplicabilityDetails in response"); + } + + SECTION("Missing ApplicabilityDetails.Architectures") + { + const json wrongApplicabilityDetails = { + {"PlatformApplicabilityForPackage", json::array({c_applicability})}}; + const json fileEntity = {{"FileId", c_fileId}, + {"Url", c_url}, + {"SizeInBytes", c_size}, + {"Hashes", {{"Sha1", c_sha1}, {"Sha256", c_sha256}}}, + {"FileMoniker", c_fileMoniker}, + {"ApplicabilityDetails", wrongApplicabilityDetails}}; + REQUIRE_THROWS_CODE_MSG(FileEntity::FromJson(fileEntity, handler), + ServiceInvalidResponse, + "Missing File.ApplicabilityDetails.Architectures in response"); + } + + SECTION("Missing ApplicabilityDetails.PlatformApplicabilityForPackage") + { + const json wrongApplicabilityDetails = {{"Architectures", json::array({c_arch})}}; + const json fileEntity = {{"FileId", c_fileId}, + {"Url", c_url}, + {"SizeInBytes", c_size}, + {"Hashes", {{"Sha1", c_sha1}, {"Sha256", c_sha256}}}, + {"FileMoniker", c_fileMoniker}, + {"ApplicabilityDetails", wrongApplicabilityDetails}}; + REQUIRE_THROWS_CODE_MSG( + FileEntity::FromJson(fileEntity, handler), + ServiceInvalidResponse, + "Missing File.ApplicabilityDetails.PlatformApplicabilityForPackage in response"); + } + } + + SECTION("Wrong types") + { + SECTION("FileMoniker") + { + const json fileEntity = {{"FileId", c_fileId}, + {"Url", c_url}, + {"SizeInBytes", c_size}, + {"Hashes", {{"Sha1", c_sha1}, {"Sha256", c_sha256}}}, + {"FileMoniker", 1}, + {"ApplicabilityDetails", applicabilityDetails}}; + REQUIRE_THROWS_CODE_MSG(FileEntity::FromJson(fileEntity, handler), + ServiceInvalidResponse, + "File.FileMoniker is not a string"); + } + + SECTION("ApplicabilityDetails") + { + SECTION("Not an object") + { + const json fileEntity = {{"FileId", c_fileId}, + {"Url", c_url}, + {"SizeInBytes", c_size}, + {"Hashes", {{"Sha1", c_sha1}, {"Sha256", c_sha256}}}, + {"FileMoniker", c_fileMoniker}, + {"ApplicabilityDetails", c_fileId}}; + REQUIRE_THROWS_CODE_MSG(FileEntity::FromJson(fileEntity, handler), + ServiceInvalidResponse, + "File.ApplicabilityDetails is not an object"); + } + + SECTION("Architectures is not an array") + { + const json wrongApplicabilityDetails = { + {"Architectures", c_fileId}, + {"PlatformApplicabilityForPackage", json::array({c_applicability})}}; + const json fileEntity = {{"FileId", c_fileId}, + {"Url", c_url}, + {"SizeInBytes", c_size}, + {"Hashes", {{"Sha1", c_sha1}, {"Sha256", c_sha256}}}, + {"FileMoniker", c_fileMoniker}, + {"ApplicabilityDetails", wrongApplicabilityDetails}}; + REQUIRE_THROWS_CODE_MSG(FileEntity::FromJson(fileEntity, handler), + ServiceInvalidResponse, + "File.ApplicabilityDetails.Architectures is not an array"); + } + + SECTION("Architectures array value is not a string") + { + const json wrongApplicabilityDetails = { + {"Architectures", json::array({1})}, + {"PlatformApplicabilityForPackage", json::array({c_applicability})}}; + const json fileEntity = {{"FileId", c_fileId}, + {"Url", c_url}, + {"SizeInBytes", c_size}, + {"Hashes", {{"Sha1", c_sha1}, {"Sha256", c_sha256}}}, + {"FileMoniker", c_fileMoniker}, + {"ApplicabilityDetails", wrongApplicabilityDetails}}; + REQUIRE_THROWS_CODE_MSG(FileEntity::FromJson(fileEntity, handler), + ServiceInvalidResponse, + "File.ApplicabilityDetails.Architectures array value is not a string"); + } + + SECTION("PlatformApplicabilityForPackage is not an array") + { + const json wrongApplicabilityDetails = {{"Architectures", json::array({c_arch})}, + {"PlatformApplicabilityForPackage", c_fileId}}; + const json fileEntity = {{"FileId", c_fileId}, + {"Url", c_url}, + {"SizeInBytes", c_size}, + {"Hashes", {{"Sha1", c_sha1}, {"Sha256", c_sha256}}}, + {"FileMoniker", c_fileMoniker}, + {"ApplicabilityDetails", wrongApplicabilityDetails}}; + REQUIRE_THROWS_CODE_MSG( + FileEntity::FromJson(fileEntity, handler), + ServiceInvalidResponse, + "File.ApplicabilityDetails.PlatformApplicabilityForPackage is not an array"); + } + + SECTION("PlatformApplicabilityForPackage array value is not a string") + { + const json wrongApplicabilityDetails = {{"Architectures", json::array({c_arch})}, + {"PlatformApplicabilityForPackage", json::array({1})}}; + const json fileEntity = {{"FileId", c_fileId}, + {"Url", c_url}, + {"SizeInBytes", c_size}, + {"Hashes", {{"Sha1", c_sha1}, {"Sha256", c_sha256}}}, + {"FileMoniker", c_fileMoniker}, + {"ApplicabilityDetails", wrongApplicabilityDetails}}; + REQUIRE_THROWS_CODE_MSG( + FileEntity::FromJson(fileEntity, handler), + ServiceInvalidResponse, + "File.ApplicabilityDetails.PlatformApplicabilityForPackage array value is not a string"); + } + } + } + } +} + +TEST("Testing GenericFileEntity conversions") +{ + ReportingHandler handler; + handler.SetLoggingCallback(LogCallbackToTest); + + auto CheckFile = [&](const File& file) { + REQUIRE(file.GetFileId() == c_fileId); + REQUIRE(file.GetUrl() == c_url); + REQUIRE(file.GetSizeInBytes() == c_size); + REQUIRE(file.GetHashes().size() == 2); + REQUIRE(file.GetHashes().at(HashType::Sha1) == c_sha1); + REQUIRE(file.GetHashes().at(HashType::Sha256) == c_sha256); + }; + + SECTION("GenericFileEntity::ToFile()") + { + SECTION("Success") + { + std::unique_ptr<FileEntity> entity = std::make_unique<GenericFileEntity>(); + entity->fileId = c_fileId; + entity->url = c_url; + entity->sizeInBytes = c_size; + entity->hashes = {{"Sha1", c_sha1}, {"Sha256", c_sha256}}; + + auto file = GenericFileEntity::ToFile(std::move(*entity), handler); + CheckFile(*file); + } + + SECTION("Wrong type") + { + std::unique_ptr<FileEntity> wrongEntity = std::make_unique<AppFileEntity>(); + wrongEntity->fileId = c_fileId; + wrongEntity->url = c_url; + wrongEntity->sizeInBytes = c_size; + wrongEntity->hashes = {{"Sha1", c_sha1}, {"Sha256", c_sha256}}; + + REQUIRE_THROWS_CODE_MSG( + GenericFileEntity::ToFile(std::move(*wrongEntity), handler), + ServiceUnexpectedContentType, + "Unexpected content type [App] returned by the service does not match the expected [Generic]"); + } + } + + SECTION("GenericFileEntity::FileEntitiesToFileVector()") + { + SECTION("Success") + { + std::unique_ptr<FileEntity> entity = std::make_unique<GenericFileEntity>(); + entity->fileId = c_fileId; + entity->url = c_url; + entity->sizeInBytes = c_size; + entity->hashes = {{"Sha1", c_sha1}, {"Sha256", c_sha256}}; + + FileEntities entities; + entities.push_back(std::move(entity)); + + std::vector<File> files; + SECTION("1 file") + { + REQUIRE_NOTHROW(files = GenericFileEntity::FileEntitiesToFileVector(std::move(entities), handler)); + REQUIRE(files.size() == 1); + CheckFile(files[0]); + } + + SECTION("2 files") + { + std::unique_ptr<FileEntity> entity2 = std::make_unique<GenericFileEntity>(); + entity2->fileId = c_fileId; + entity2->url = c_url; + entity2->sizeInBytes = c_size; + entity2->hashes = {{"Sha1", c_sha1}, {"Sha256", c_sha256}}; + + entities.push_back(std::move(entity2)); + REQUIRE_NOTHROW(files = GenericFileEntity::FileEntitiesToFileVector(std::move(entities), handler)); + REQUIRE(files.size() == 2); + CheckFile(files[0]); + CheckFile(files[1]); + } + } + + SECTION("Wrong type, fails") + { + std::unique_ptr<FileEntity> wrongEntity = std::make_unique<AppFileEntity>(); + wrongEntity->fileId = c_fileId; + wrongEntity->url = c_url; + wrongEntity->sizeInBytes = c_size; + wrongEntity->hashes = {{"Sha1", c_sha1}, {"Sha256", c_sha256}}; + + FileEntities wrongEntities; + wrongEntities.push_back(std::move(wrongEntity)); + REQUIRE_THROWS_CODE_MSG( + GenericFileEntity::FileEntitiesToFileVector(std::move(wrongEntities), handler), + ServiceUnexpectedContentType, + "Unexpected content type [App] returned by the service does not match the expected [Generic]"); + } + } +} + +TEST("Testing AppFileEntity conversions") +{ + ReportingHandler handler; + handler.SetLoggingCallback(LogCallbackToTest); + + auto CheckAppFile = [&](const AppFile& file) { + REQUIRE(file.GetFileId() == c_fileId); + REQUIRE(file.GetUrl() == c_url); + REQUIRE(file.GetSizeInBytes() == c_size); + REQUIRE(file.GetHashes().size() == 2); + REQUIRE(file.GetHashes().at(HashType::Sha1) == c_sha1); + REQUIRE(file.GetHashes().at(HashType::Sha256) == c_sha256); + REQUIRE(file.GetFileMoniker() == c_fileMoniker); + REQUIRE(file.GetApplicabilityDetails().GetArchitectures().size() == 1); + REQUIRE(file.GetApplicabilityDetails().GetArchitectures()[0] == Architecture::Amd64); + REQUIRE(file.GetApplicabilityDetails().GetPlatformApplicabilityForPackage().size() == 1); + REQUIRE(file.GetApplicabilityDetails().GetPlatformApplicabilityForPackage()[0] == c_applicability); + }; + + SECTION("AppFileEntity::ToAppFile()") + { + SECTION("Success") + { + std::unique_ptr<FileEntity> entity = std::make_unique<AppFileEntity>(); + auto appEntity = dynamic_cast<AppFileEntity*>(entity.get()); + appEntity->fileId = c_fileId; + appEntity->url = c_url; + appEntity->sizeInBytes = c_size; + appEntity->hashes = {{"Sha1", c_sha1}, {"Sha256", c_sha256}}; + appEntity->fileMoniker = c_fileMoniker; + appEntity->applicabilityDetails = c_appDetailsEntity; + + auto file = AppFileEntity::ToAppFile(std::move(*entity), handler); + CheckAppFile(*file); + } + + SECTION("Wrong type") + { + std::unique_ptr<FileEntity> wrongEntity = std::make_unique<GenericFileEntity>(); + wrongEntity->fileId = c_fileId; + wrongEntity->url = c_url; + wrongEntity->sizeInBytes = c_size; + wrongEntity->hashes = {{"Sha1", c_sha1}, {"Sha256", c_sha256}}; + + REQUIRE_THROWS_CODE_MSG( + AppFileEntity::ToAppFile(std::move(*wrongEntity), handler), + ServiceUnexpectedContentType, + "Unexpected content type [Generic] returned by the service does not match the expected [App]"); + } + } + + SECTION("Testing AppFileEntity::FileEntitiesToAppFileVector()") + { + SECTION("Success") + { + std::unique_ptr<FileEntity> entity = std::make_unique<AppFileEntity>(); + auto appEntity = dynamic_cast<AppFileEntity*>(entity.get()); + appEntity->fileId = c_fileId; + appEntity->url = c_url; + appEntity->sizeInBytes = c_size; + appEntity->hashes = {{"Sha1", c_sha1}, {"Sha256", c_sha256}}; + appEntity->fileMoniker = c_fileMoniker; + appEntity->applicabilityDetails = c_appDetailsEntity; + + FileEntities entities; + entities.push_back(std::move(entity)); + + std::vector<AppFile> files; + SECTION("1 file") + { + REQUIRE_NOTHROW(files = AppFileEntity::FileEntitiesToAppFileVector(std::move(entities), handler)); + REQUIRE(files.size() == 1); + CheckAppFile(files[0]); + } + + SECTION("2 files") + { + std::unique_ptr<FileEntity> entity2 = std::make_unique<AppFileEntity>(); + auto appEntity2 = dynamic_cast<AppFileEntity*>(entity2.get()); + appEntity2->fileId = c_fileId; + appEntity2->url = c_url; + appEntity2->sizeInBytes = c_size; + appEntity2->hashes = {{"Sha1", c_sha1}, {"Sha256", c_sha256}}; + appEntity2->fileMoniker = c_fileMoniker; + appEntity2->applicabilityDetails = c_appDetailsEntity; + + entities.push_back(std::move(entity2)); + REQUIRE_NOTHROW(files = AppFileEntity::FileEntitiesToAppFileVector(std::move(entities), handler)); + REQUIRE(files.size() == 2); + CheckAppFile(files[0]); + CheckAppFile(files[1]); + } + } + + SECTION("Wrong type, fails") + { + std::unique_ptr<FileEntity> wrongEntity = std::make_unique<GenericFileEntity>(); + wrongEntity->fileId = c_fileId; + wrongEntity->url = c_url; + wrongEntity->sizeInBytes = c_size; + wrongEntity->hashes = {{"Sha1", c_sha1}, {"Sha256", c_sha256}}; + + FileEntities wrongEntities; + wrongEntities.push_back(std::move(wrongEntity)); + REQUIRE_THROWS_CODE_MSG( + AppFileEntity::FileEntitiesToAppFileVector(std::move(wrongEntities), handler), + ServiceUnexpectedContentType, + "Unexpected content type [Generic] returned by the service does not match the expected [App]"); + } + } +} + +TEST("Testing FileEntity::DownloadInfoResponseToFileEntities()") +{ + ReportingHandler handler; + handler.SetLoggingCallback(LogCallbackToTest); + + SECTION("GenericFileEntity") + { + auto CheckEntity = [&](const FileEntity& entity) { + REQUIRE(entity.GetContentType() == ContentType::Generic); + REQUIRE(entity.fileId == c_fileId); + REQUIRE(entity.url == c_url); + REQUIRE(entity.sizeInBytes == c_size); + REQUIRE(entity.hashes.size() == 2); + REQUIRE(entity.hashes.at("Sha1") == c_sha1); + REQUIRE(entity.hashes.at("Sha256") == c_sha256); + }; + + SECTION("Correct") + { + const json fileJson = {{"FileId", c_fileId}, + {"Url", c_url}, + {"SizeInBytes", c_size}, + {"Hashes", {{"Sha1", c_sha1}, {"Sha256", c_sha256}}}}; + + SECTION("1 file") + { + const json downloadInfoResponse = json::array({fileJson}); + auto entities = FileEntity::DownloadInfoResponseToFileEntities(downloadInfoResponse, handler); + REQUIRE(entities.size() == 1); + REQUIRE(entities[0] != nullptr); + CheckEntity(*entities[0]); + } + + SECTION("2 files") + { + const json downloadInfoResponse = json::array({fileJson, fileJson}); + auto entities = FileEntity::DownloadInfoResponseToFileEntities(downloadInfoResponse, handler); + REQUIRE(entities.size() == 2); + REQUIRE(entities[0] != nullptr); + CheckEntity(*entities[0]); + REQUIRE(entities[1] != nullptr); + CheckEntity(*entities[1]); + } + } + } + + SECTION("AppFileEntity") + { + const json applicabilityDetails = {{"Architectures", json::array({c_arch})}, + {"PlatformApplicabilityForPackage", json::array({c_applicability})}}; + + auto CheckEntity = [&](const AppFileEntity& entity) { + REQUIRE(entity.GetContentType() == ContentType::App); + REQUIRE(entity.fileId == c_fileId); + REQUIRE(entity.url == c_url); + REQUIRE(entity.sizeInBytes == c_size); + REQUIRE(entity.hashes.size() == 2); + REQUIRE(entity.hashes.at("Sha1") == c_sha1); + REQUIRE(entity.hashes.at("Sha256") == c_sha256); + REQUIRE(entity.fileMoniker == c_fileMoniker); + REQUIRE(entity.applicabilityDetails.architectures.size() == 1); + REQUIRE(entity.applicabilityDetails.architectures[0] == c_arch); + REQUIRE(entity.applicabilityDetails.platformApplicabilityForPackage.size() == 1); + REQUIRE(entity.applicabilityDetails.platformApplicabilityForPackage[0] == c_applicability); + }; + + SECTION("Correct") + { + const json fileJson = {{"FileId", c_fileId}, + {"Url", c_url}, + {"SizeInBytes", c_size}, + {"Hashes", {{"Sha1", c_sha1}, {"Sha256", c_sha256}}}, + {"FileMoniker", c_fileMoniker}, + {"ApplicabilityDetails", applicabilityDetails}}; + + SECTION("1 file") + { + const json downloadInfoResponse = json::array({fileJson}); + auto entities = FileEntity::DownloadInfoResponseToFileEntities(downloadInfoResponse, handler); + REQUIRE(entities.size() == 1); + REQUIRE(entities[0] != nullptr); + CheckEntity(dynamic_cast<AppFileEntity&>(*entities[0])); + } + + SECTION("2 files") + { + const json downloadInfoResponse = json::array({fileJson, fileJson}); + auto entities = FileEntity::DownloadInfoResponseToFileEntities(downloadInfoResponse, handler); + REQUIRE(entities.size() == 2); + REQUIRE(entities[0] != nullptr); + CheckEntity(dynamic_cast<AppFileEntity&>(*entities[0])); + REQUIRE(entities[1] != nullptr); + CheckEntity(dynamic_cast<AppFileEntity&>(*entities[1])); + } + } + } +} diff --git a/src/SfsClient/sfs-client/client/tests/unit/details/entity/VersionEntityTests.cpp b/src/SfsClient/sfs-client/client/tests/unit/details/entity/VersionEntityTests.cpp @@ -0,0 +1,305 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "../../../util/SFSExceptionMatcher.h" +#include "../../../util/TestHelper.h" +#include "ReportingHandler.h" +#include "entity/VersionEntity.h" +#include "sfsclient/ContentId.h" + +#include <catch2/catch_test_macros.hpp> +#include <nlohmann/json.hpp> + +#define TEST(...) TEST_CASE("[VersionEntityTests] " __VA_ARGS__) + +using namespace SFS; +using namespace SFS::details; +using namespace SFS::test; +using json = nlohmann::json; + +// GenericVersionEntity constants +const std::string c_ns = "namespace"; +const std::string c_name = "name"; +const std::string c_version = "version"; + +// AppVersionEntity constants +const std::string c_updateId = "updateId"; + +TEST("Testing VersionEntity::FromJson()") +{ + ReportingHandler handler; + handler.SetLoggingCallback(LogCallbackToTest); + + SECTION("Generic Version Entity") + { + std::unique_ptr<VersionEntity> entity; + SECTION("Correct") + { + const json versionEntity = {{"ContentId", {{"Namespace", c_ns}, {"Name", c_name}, {"Version", c_version}}}}; + + REQUIRE_NOTHROW(entity = VersionEntity::FromJson(versionEntity, handler)); + REQUIRE(entity != nullptr); + REQUIRE(entity->GetContentType() == ContentType::Generic); + REQUIRE(entity->contentId.nameSpace == c_ns); + REQUIRE(entity->contentId.name == c_name); + REQUIRE(entity->contentId.version == c_version); + } + + SECTION("Missing fields") + { + SECTION("Missing ContentId") + { + const json versionEntity = {{"Namespace", c_ns}, {"Name", c_name}, {"Version", c_version}}; + REQUIRE_THROWS_CODE_MSG(VersionEntity::FromJson(versionEntity, handler), + ServiceInvalidResponse, + "Missing ContentId in response"); + } + + SECTION("Missing ContentId.Namespace") + { + const json versionEntity = {{"ContentId", {{"Name", c_name}, {"Version", c_version}}}}; + REQUIRE_THROWS_CODE_MSG(VersionEntity::FromJson(versionEntity, handler), + ServiceInvalidResponse, + "Missing ContentId.Namespace in response"); + } + + SECTION("Missing ContentId.Name") + { + const json versionEntity = {{"ContentId", {{"Namespace", c_ns}, {"Version", c_version}}}}; + REQUIRE_THROWS_CODE_MSG(VersionEntity::FromJson(versionEntity, handler), + ServiceInvalidResponse, + "Missing ContentId.Name in response"); + } + + SECTION("Missing ContentId.Version") + { + const json versionEntity = {{"ContentId", {{"Namespace", c_ns}, {"Name", c_name}}}}; + REQUIRE_THROWS_CODE_MSG(VersionEntity::FromJson(versionEntity, handler), + ServiceInvalidResponse, + "Missing ContentId.Version in response"); + } + } + + SECTION("Wrong types") + { + SECTION("ContentId not an object") + { + const json versionEntity = json::array({{"Namespace", 1}, {"Name", c_name}, {"Version", c_version}}); + REQUIRE_THROWS_CODE_MSG(VersionEntity::FromJson(versionEntity, handler), + ServiceInvalidResponse, + "Response is not a JSON object"); + } + + SECTION("ContentId.Namespace") + { + const json versionEntity = { + {"ContentId", {{"Namespace", 1}, {"Name", c_name}, {"Version", c_version}}}}; + REQUIRE_THROWS_CODE_MSG(VersionEntity::FromJson(versionEntity, handler), + ServiceInvalidResponse, + "ContentId.Namespace is not a string"); + } + + SECTION("ContentId.Name") + { + const json versionEntity = {{"ContentId", {{"Namespace", c_ns}, {"Name", 1}, {"Version", c_version}}}}; + REQUIRE_THROWS_CODE_MSG(VersionEntity::FromJson(versionEntity, handler), + ServiceInvalidResponse, + "ContentId.Name is not a string"); + } + + SECTION("ContentId.Version") + { + const json versionEntity = {{"ContentId", {{"Namespace", c_ns}, {"Name", c_name}, {"Version", 1}}}}; + REQUIRE_THROWS_CODE_MSG(VersionEntity::FromJson(versionEntity, handler), + ServiceInvalidResponse, + "ContentId.Version is not a string"); + } + } + } + + SECTION("App Version Entity") + { + std::unique_ptr<VersionEntity> entity; + const json contentId = {{"Namespace", c_ns}, {"Name", c_name}, {"Version", c_version}}; + + SECTION("Correct") + { + const json versionEntity = {{"ContentId", contentId}, + {"UpdateId", c_updateId}, + {"Prerequisites", json::array({contentId})}}; + + REQUIRE_NOTHROW(entity = VersionEntity::FromJson(versionEntity, handler)); + REQUIRE(entity != nullptr); + REQUIRE(entity->GetContentType() == ContentType::App); + REQUIRE(entity->contentId.nameSpace == c_ns); + REQUIRE(entity->contentId.name == c_name); + REQUIRE(entity->contentId.version == c_version); + + AppVersionEntity* appEntity = dynamic_cast<AppVersionEntity*>(entity.get()); + REQUIRE(appEntity->updateId == c_updateId); + REQUIRE(appEntity->prerequisites.size() == 1); + REQUIRE(appEntity->prerequisites[0].contentId.nameSpace == c_ns); + REQUIRE(appEntity->prerequisites[0].contentId.name == c_name); + REQUIRE(appEntity->prerequisites[0].contentId.version == c_version); + } + + SECTION("Missing fields") + { + SECTION("Missing Prerequisites") + { + const json versionEntity = {{"ContentId", contentId}, {"UpdateId", c_updateId}}; + REQUIRE_THROWS_CODE_MSG(VersionEntity::FromJson(versionEntity, handler), + ServiceInvalidResponse, + "Missing Prerequisites in response"); + } + + SECTION("Missing Prerequisite.Namespace") + { + const json wrongPrerequisite = {{"Name", c_name}, {"Version", c_version}}; + const json versionEntity = {{"ContentId", contentId}, + {"UpdateId", c_updateId}, + {"Prerequisites", json::array({wrongPrerequisite})}}; + REQUIRE_THROWS_CODE_MSG(VersionEntity::FromJson(versionEntity, handler), + ServiceInvalidResponse, + "Missing Prerequisite.Namespace in response"); + } + + SECTION("Missing Prerequisite.Name") + { + const json wrongPrerequisite = {{"Namespace", c_ns}, {"Version", c_version}}; + const json versionEntity = {{"ContentId", contentId}, + {"UpdateId", c_updateId}, + {"Prerequisites", json::array({wrongPrerequisite})}}; + REQUIRE_THROWS_CODE_MSG(VersionEntity::FromJson(versionEntity, handler), + ServiceInvalidResponse, + "Missing Prerequisite.Name in response"); + } + + SECTION("Missing Prerequisite.Version") + { + const json wrongPrerequisite = {{"Namespace", c_ns}, {"Name", c_name}}; + const json versionEntity = {{"ContentId", contentId}, + {"UpdateId", c_updateId}, + {"Prerequisites", json::array({wrongPrerequisite})}}; + REQUIRE_THROWS_CODE_MSG(VersionEntity::FromJson(versionEntity, handler), + ServiceInvalidResponse, + "Missing Prerequisite.Version in response"); + } + } + + SECTION("Wrong types") + { + SECTION("UpdateId") + { + const json versionEntity = {{"ContentId", contentId}, + {"UpdateId", 1}, + {"Prerequisites", json::array({contentId})}}; + REQUIRE_THROWS_CODE_MSG(VersionEntity::FromJson(versionEntity, handler), + ServiceInvalidResponse, + "UpdateId is not a string"); + } + + SECTION("Prerequisites") + { + SECTION("Not an array") + { + const json versionEntity = {{"ContentId", contentId}, + {"UpdateId", c_updateId}, + {"Prerequisites", contentId}}; + REQUIRE_THROWS_CODE_MSG(VersionEntity::FromJson(versionEntity, handler), + ServiceInvalidResponse, + "Prerequisites is not an array"); + } + + SECTION("Element is not an object") + { + const json versionEntity = {{"ContentId", contentId}, + {"UpdateId", c_updateId}, + {"Prerequisites", json::array({1})}}; + REQUIRE_THROWS_CODE_MSG(VersionEntity::FromJson(versionEntity, handler), + ServiceInvalidResponse, + "Prerequisite element is not a JSON object"); + } + + SECTION("Prerequisite.Namespace") + { + const json wrongPrerequisite = {{"Namespace", 1}, {"Name", c_name}, {"Version", c_version}}; + const json versionEntity = {{"ContentId", contentId}, + {"UpdateId", c_updateId}, + {"Prerequisites", json::array({wrongPrerequisite})}}; + REQUIRE_THROWS_CODE_MSG(VersionEntity::FromJson(versionEntity, handler), + ServiceInvalidResponse, + "Prerequisite.Namespace is not a string"); + } + + SECTION("Prerequisite.Name") + { + const json wrongPrerequisite = {{"Namespace", c_ns}, {"Name", 1}, {"Version", c_version}}; + const json versionEntity = {{"ContentId", contentId}, + {"UpdateId", c_updateId}, + {"Prerequisites", json::array({wrongPrerequisite})}}; + REQUIRE_THROWS_CODE_MSG(VersionEntity::FromJson(versionEntity, handler), + ServiceInvalidResponse, + "Prerequisite.Name is not a string"); + } + + SECTION("Prerequisite.Version") + { + const json wrongPrerequisite = {{"Namespace", c_ns}, {"Name", c_name}, {"Version", 1}}; + const json versionEntity = {{"ContentId", contentId}, + {"UpdateId", c_updateId}, + {"Prerequisites", json::array({wrongPrerequisite})}}; + REQUIRE_THROWS_CODE_MSG(VersionEntity::FromJson(versionEntity, handler), + ServiceInvalidResponse, + "Prerequisite.Version is not a string"); + } + } + } + } +} + +TEST("Testing VersionEntity conversions") +{ + ReportingHandler handler; + handler.SetLoggingCallback(LogCallbackToTest); + + auto CheckContentId = [&](const ContentId& contentId) { + REQUIRE(contentId.GetNameSpace() == c_ns); + REQUIRE(contentId.GetName() == c_name); + REQUIRE(contentId.GetVersion() == c_version); + }; + + SECTION("VersionEntity::ToContentId()") + { + SECTION("Success with GenericVersionEntity") + { + std::unique_ptr<VersionEntity> entity = std::make_unique<GenericVersionEntity>(); + entity->contentId.nameSpace = c_ns; + entity->contentId.name = c_name; + entity->contentId.version = c_version; + + auto contentId = VersionEntity::ToContentId(std::move(*entity), handler); + CheckContentId(*contentId); + } + + SECTION("Success with AppVersionEntity") + { + std::unique_ptr<VersionEntity> entity = std::make_unique<AppVersionEntity>(); + auto appEntity = AppVersionEntity::GetAppVersionEntityPtr(entity, handler); + appEntity->contentId.nameSpace = c_ns; + appEntity->contentId.name = c_name; + appEntity->contentId.version = c_version; + appEntity->updateId = c_updateId; + + std::unique_ptr<GenericVersionEntity> prereqEntity = std::make_unique<GenericVersionEntity>(); + prereqEntity->contentId.nameSpace = c_ns; + prereqEntity->contentId.name = c_name; + prereqEntity->contentId.version = c_version; + + appEntity->prerequisites.push_back(std::move(*prereqEntity)); + + auto contentId = VersionEntity::ToContentId(std::move(*entity), handler); + CheckContentId(*contentId); + } + } +} diff --git a/src/SfsClient/sfs-client/client/tests/util/SFSExceptionMatcher.cpp b/src/SfsClient/sfs-client/client/tests/util/SFSExceptionMatcher.cpp @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "SFSExceptionMatcher.h" + +#include <catch2/matchers/catch_matchers_string.hpp> + +using namespace SFS; +using namespace SFS::details; +using namespace SFS::test; + +SFSExceptionMatcher::SFSExceptionMatcher(const Result::Code& code) : m_code(code) +{ +} + +SFSExceptionMatcher::SFSExceptionMatcher(const Result::Code& code, std::string message) + : m_code(code) + , m_message(std::move(message)) +{ +} + +bool SFSExceptionMatcher::match(const SFSException& other) const +{ + if (m_message) + { + return other.GetResult() == m_code && other.what() == m_message; + } + return other.GetResult() == m_code; +} + +std::string SFSExceptionMatcher::describe() const +{ + return "Equals: " + std::string(ToString(m_code)) + (m_message ? " with message: " + *m_message : ""); +} + +template <typename Matcher> +SFSExceptionGenericMatcher<Matcher>::SFSExceptionGenericMatcher(const Result::Code& code, Matcher messageMatcher) + : m_code(code) + , m_messageMatcher(std::move(messageMatcher)) +{ +} + +template <typename Matcher> +bool SFSExceptionGenericMatcher<Matcher>::match(const SFSException& other) const +{ + return other.GetResult() == m_code && m_messageMatcher.match(other.what()); +} + +template <typename Matcher> +std::string SFSExceptionGenericMatcher<Matcher>::describe() const +{ + return "Equals: " + std::string(ToString(m_code)) + m_messageMatcher.describe(); +} + +template struct SFS::test::SFSExceptionGenericMatcher<Catch::Matchers::StringEqualsMatcher>; +template struct SFS::test::SFSExceptionGenericMatcher<Catch::Matchers::StringContainsMatcher>; +template struct SFS::test::SFSExceptionGenericMatcher<Catch::Matchers::EndsWithMatcher>; +template struct SFS::test::SFSExceptionGenericMatcher<Catch::Matchers::StartsWithMatcher>; +template struct SFS::test::SFSExceptionGenericMatcher<Catch::Matchers::RegexMatcher>; diff --git a/src/SfsClient/sfs-client/client/tests/util/SFSExceptionMatcher.h b/src/SfsClient/sfs-client/client/tests/util/SFSExceptionMatcher.h @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "Result.h" +#include "SFSException.h" + +#include <catch2/catch_test_macros.hpp> +#include <catch2/matchers/catch_matchers_templated.hpp> + +#include <optional> +#include <string> + +#define REQUIRE_THROWS_CODE(call, code) \ + REQUIRE_THROWS_MATCHES(call, SFS::details::SFSException, SFS::test::SFSExceptionMatcher(SFS::Result::code)) +#define REQUIRE_THROWS_CODE_MSG(call, code, message) \ + REQUIRE_THROWS_MATCHES(call, SFS::details::SFSException, SFS::test::SFSExceptionMatcher(SFS::Result::code, message)) +#define REQUIRE_THROWS_CODE_MSG_MATCHES(call, code, matcher) \ + REQUIRE_THROWS_MATCHES(call, \ + SFS::details::SFSException, \ + SFS::test::SFSExceptionGenericMatcher(SFS::Result::code, matcher)) + +namespace SFS::test +{ +struct SFSExceptionMatcher : Catch::Matchers::MatcherGenericBase +{ + SFSExceptionMatcher(const Result::Code& code); + SFSExceptionMatcher(const Result::Code& code, std::string message); + + bool match(const SFS::details::SFSException& other) const; + std::string describe() const override; + + private: + Result::Code m_code{Result::Success}; + std::optional<std::string> m_message; +}; + +template <typename Matcher> +struct SFSExceptionGenericMatcher : Catch::Matchers::MatcherGenericBase +{ + SFSExceptionGenericMatcher(const Result::Code& code, Matcher messageMatcher); + + bool match(const SFS::details::SFSException& other) const; + std::string describe() const override; + + private: + Result::Code m_code{Result::Success}; + Matcher m_messageMatcher; +}; +} // namespace SFS::test diff --git a/src/SfsClient/sfs-client/client/tests/util/TestHelper.cpp b/src/SfsClient/sfs-client/client/tests/util/TestHelper.cpp @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "TestHelper.h" + +#include <chrono> +#include <filesystem> +#include <iomanip> +#include <mutex> +#include <sstream> +#include <time.h> + +#include <catch2/catch_test_macros.hpp> + +static std::mutex s_logMutex; + +namespace +{ +std::string TimestampToString(std::chrono::time_point<std::chrono::system_clock> time) +{ + using namespace std::chrono; + + // get number of milliseconds for the current second + // (remainder after division into seconds) + auto ms = duration_cast<milliseconds>(time.time_since_epoch()) % 1000; + + auto timer = system_clock::to_time_t(time); + + std::stringstream timeStream; + struct tm gmTime; +#ifdef _WIN32 + gmtime_s(&gmTime, &timer); // gmtime_s is the safe version of gmtime, not available on Linux +#else + gmTime = (*std::gmtime(&timer)); +#endif + timeStream << std::put_time(&gmTime, "%F %X"); // yyyy-mm-dd HH:MM:SS + timeStream << '.' << std::setfill('0') << std::setw(3) << ms.count(); + + return timeStream.str(); +} +} // namespace + +void SFS::test::LogCallbackToTest(const SFS::LogData& logData) +{ + std::lock_guard guard(s_logMutex); + UNSCOPED_INFO("Log: " << TimestampToString(logData.time) << " [" << ToString(logData.severity) << "]" << " " + << std::filesystem::path(logData.file).filename().string() << ":" << logData.line << " " + << logData.message); +} diff --git a/src/SfsClient/sfs-client/client/tests/util/TestHelper.h b/src/SfsClient/sfs-client/client/tests/util/TestHelper.h @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "sfsclient/Logging.h" + +#include <chrono> + +#define TEST_UNSCOPED_INFO(message) \ + do \ + { \ + const std::string __message = (message); \ + SFS::test::LogCallbackToTest({SFS::LogSeverity::Info, \ + __message.c_str(), \ + __FILE__, \ + __LINE__, \ + __FUNCTION__, \ + std::chrono::system_clock::now()}); \ + } while ((void)0, 0) + +namespace SFS::test +{ +// Use this method to redirect the library logging to the Catch2 logging system +void LogCallbackToTest(const SFS::LogData& logData); +} // namespace SFS::test diff --git a/src/SfsClient/sfs-client/cmake/SFSOptions.cmake b/src/SfsClient/sfs-client/cmake/SFSOptions.cmake @@ -0,0 +1,15 @@ +# Define custom options for CMake. +# Options are passed to cmake through command line, -Doption=value +option(SFS_BUILD_TESTS "Indicates if tests should be built." OFF) + +option(SFS_BUILD_SAMPLES "Indicates if samples should be built." OFF) + +option( + SFS_ENABLE_TEST_OVERRIDES + "Set SFS_ENABLE_OVERRIDES to ON to enable certain test overrides through environment variables." + OFF) + +option( + SFS_WINDOWS_STATIC_ONLY + "Indicates if only static libraries and dependencies should be built on Windows." + OFF) diff --git a/src/SfsClient/sfs-client/cmake/sfsclient-config.cmake.in b/src/SfsClient/sfs-client/cmake/sfsclient-config.cmake.in @@ -0,0 +1,16 @@ +# Generates a macro to auto-configure everything +@PACKAGE_INIT@ + +set(SFS_BUILD_TESTS @SFS_BUILD_TESTS@) + +include(CMakeFindDependencyMacro) +find_dependency(CURL) +find_dependency(nlohmann_json) +find_dependency(correlation_vector) + +if(SFS_BUILD_TESTS) + find_dependency(Catch2) + find_dependency(cpp-httplib) +endif() + +include("${CMAKE_CURRENT_LIST_DIR}/sfsclient-targets.cmake")+ \ No newline at end of file diff --git a/src/SfsClient/sfs-client/pre-commit-wrapper.sh b/src/SfsClient/sfs-client/pre-commit-wrapper.sh @@ -0,0 +1,18 @@ +#!/bin/sh +# +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# +# A wrapper script to call the pre-commit.sh script +# +# Since git hooks are not committed to the server, existing only +# in the .git folder, this will be copied there and call the actual +# hook script only in case it exists. This should prevent issues +# with the user having to work on an earlier commit that does not +# have the hook or its dependencies installed. + +root_dir=$(git rev-parse --show-toplevel) +hook_script="$root_dir/pre-commit.sh" +if [[ -f $hook_script ]]; then + . "$hook_script" +fi diff --git a/src/SfsClient/sfs-client/pre-commit.sh b/src/SfsClient/sfs-client/pre-commit.sh @@ -0,0 +1,152 @@ +#!/bin/sh +# +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# +# A hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# To avoid having this script called, use "git commit -n" + +# Get files that need to be checked against clang-format into an array + +# To understand the regex format in bash, take a look at https://tldp.org/LDP/abs/html/x17129.html +# For example, this is how the regex (\.cpp|\.h)$ matches the example file client/include/SFSClient.h: +# (\.cpp|\.h): Match either .cpp or .h. The . character is escaped because it has a special meaning in regex. +# $: Match end of string +pattern_to_include_in_clang_format='(\.cpp|\.h)$' +pattern_to_include_in_cmake_format='CMakeLists.txt' + +# Get the names of staged files (--cached) that are not deleted (--diff-filter=d) +all_staged_files=$(git diff --cached --name-only --diff-filter=d) +mapfile -t filtered_files <<<"$all_staged_files" + +declare -A files_with_unstaged_changes + +clang_format_files=() +cmake_format_files=() + +# Only those that match a pattern will be analyzed +max_index=${#filtered_files[@]} +for ((i = 0; i < max_index; i++)); do + if [[ "${filtered_files[$i]}" =~ $pattern_to_include_in_clang_format || "${filtered_files[$i]}" =~ $pattern_to_include_in_cmake_format ]]; then + if [[ "${filtered_files[$i]}" =~ $pattern_to_include_in_clang_format ]]; then + clang_format_files+=("${filtered_files[$i]}") + elif [[ "${filtered_files[$i]}" =~ $pattern_to_include_in_cmake_format ]]; then + cmake_format_files+=("${filtered_files[$i]}") + fi + + # Collect files that will be analyzed but have unstaged changes, as we don't want to overwrite these changes + # `git diff` is empty if a file has no unstaged changes + if [[ -n "$(git diff "${filtered_files[$i]}")" ]]; then + files_with_unstaged_changes["${filtered_files[$i]}"]=1 + fi + fi +done + +# Early stop if no files to check +if [[ ${clang_format_files[*]} == "" && ${cmake_format_files[*]} == "" ]]; then + exit 0 +fi + +RED='\033[0;31m' +YELLOW='\033[0;33m' +NC='\033[0m' # No color + +# +# clang-format +# + +# Check that clang-format is installed +python_dir="$(python -c 'import os,sysconfig;print(sysconfig.get_path("scripts",f"{os.name}_user"))')" +clang_format="$python_dir/clang-format.exe" + +if [[ ! -x "$(command -v "$clang_format")" ]]; then + echo -e "${RED}There are staged changes that need to be checked by the formatter." + echo -e "${RED}clang-format toolset was not found. Re-run scripts\Setup.ps1 to install it.${NC}" + exit 1 +fi + +formatted_files=() +unformatted_files=() +for file in "${clang_format_files[@]}"; do + # Check if file will be modified so we can properly inform the user of modified files + # Use `clang-format -n` which does a dry run and outputs a non-empty response if there's something to format + # Pass contents through stdin to get only the staged changes. --assume-filename is used to find the closest .clang-format + # `git show :<file>` outputs the staged version of a file + if [[ -n $(git show ":$file" | "$clang_format" -n --assume-filename "$file" 2>&1) ]]; then + + # If the file has unstaged changes, we can't format it + if [[ -v "files_with_unstaged_changes["$file"]" ]] ; then + unformatted_files+=("$file") + else + # Collect file that will be formatted and then actually format in-place + formatted_files+=("$file") + "$clang_format" -i "$file" + fi + fi +done + + +# Report modified files at end so we can run both formatters and report all changes at once + +# +# cmake-format +# +cmake_format="$python_dir/cmake-format.exe" + +if [[ ! -x "$(command -v "$cmake_format")" ]]; then + echo -e "${RED}There are staged changes that need to be checked by the formatter." + echo -e "${RED}cmake-format toolset was not found. Re-run scripts\Setup.ps1 to install it.${NC}" + exit 1 +fi + +tmp_formatted_name="tmp_formatted" + +for file in "${cmake_format_files[@]}"; do + # Check if file will be modified so we can properly inform the user of modified files + # First create a temporary file with only staged changes since cmake-format does not support stdin + git show ":$file" > $tmp_formatted_name + # Use `cmake-format --check` which does a dry run and returns 1 if there's something to format + if ! "$cmake_format" --check "$tmp_formatted_name" > /dev/null 2>&1; then + + # If the file has unstaged changes, we can't format it + if [[ -v "files_with_unstaged_changes["$file"]" ]] ; then + unformatted_files+=("$file") + else + # Collect file that will be formatted and then actually format in-place + formatted_files+=("$file") + "$cmake_format" -i "$file" + fi + fi + rm $tmp_formatted_name +done + +IFS=$'\n' sorted_formatted_files=($(sort <<<"${formatted_files[*]}")) +IFS=$'\n' sorted_unformatted_files=($(sort <<<"${unformatted_files[*]}")) +unset IFS + +exit_code=0 + +# If there were formatted files, we report the changes +if [[ ${sorted_formatted_files[*]} != "" ]]; then + echo -e "${YELLOW}There are files that have been automatically formatted. Review and stage the changes before committing:" + for file in "${sorted_formatted_files[@]}"; do + echo -e "M\t$file" + done + echo -e "${NC}" + exit_code=1 +fi + +# If there were files that could not be formatted, we report them +if [[ ${unformatted_files[*]} != "" ]]; then + echo -e "${RED}There are files that need to be formatted but have unstaged changes. Either stage the files or stash/discard the unstaged changes:" + for file in "${unformatted_files[@]}"; do + echo -e "M\t$file" + done + echo -e "${NC}" + exit_code=1 +fi + +exit $exit_code diff --git a/src/SfsClient/sfs-client/samples/CMakeLists.txt b/src/SfsClient/sfs-client/samples/CMakeLists.txt @@ -0,0 +1,5 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +add_subdirectory(integration-do-client) +add_subdirectory(tool) diff --git a/src/SfsClient/sfs-client/samples/README.md b/src/SfsClient/sfs-client/samples/README.md @@ -0,0 +1,9 @@ +# Samples + +This folder contains a few samples that show how to use the SFS Client library. + +## integration-do-client + +This sample shows how to use the SFSClient to retrieva metadata and URLs from the SFS Service and the [Delivery Optimization SDK](https://github.com/microsoft/do-client) to perform the download of the retrieved URLs. + +In Windows, the DO SDK uses COM APIs. In Linux, it will require the DO Agent, which is built from the same repository. diff --git a/src/SfsClient/sfs-client/samples/integration-do-client/CMakeLists.txt b/src/SfsClient/sfs-client/samples/integration-do-client/CMakeLists.txt @@ -0,0 +1,62 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +project(IntegrationDOClient LANGUAGES CXX) + +# Settings for do-client library +set(DO_INCLUDE_SDK + ON + CACHE BOOL "" FORCE) +set(DO_BUILD_TESTS + OFF + CACHE BOOL "" FORCE) +if(NOT DEFINED CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE debug) +endif() + +if(MSVC) + set(DO_PATCH_COMMAND "") +else() + # Populate LSB_RELEASE_ID_SHORT with the linux distro if available + find_program(LSB_RELEASE_EXEC lsb_release) + execute_process( + COMMAND ${LSB_RELEASE_EXEC} -is + OUTPUT_VARIABLE LSB_RELEASE_ID_SHORT + OUTPUT_STRIP_TRAILING_WHITESPACE) + + if(LSB_RELEASE_ID_SHORT STREQUAL "Ubuntu") + # 1. Run bootstrap script to install Linux dependencies + # 2. Build & install DO Agent, which acts as a service for the REST API + set(DO_PATCH_COMMAND + cd <SOURCE_DIR> && sudo ./build/scripts/bootstrap.sh --install + build && python3 ./build/build.py --project agent --package-for deb + && cd /tmp/build-deliveryoptimization-agent/linux-debug && sudo + dpkg -i deliveryoptimization-agent_1.1.0_amd64.deb) + else() + message(FATAL_ERROR "Unsupported platform ${LSB_RELEASE_ID_SHORT}") + endif() +endif() + +# Include the capability of fetching external content +include(FetchContent) + +FetchContent_Declare( + do-client + GIT_REPOSITORY https://github.com/microsoft/do-client.git + GIT_TAG d71ade6f692dd8bc319ec3228c956517e9b29292 # v1.1.0 + PATCH_COMMAND "${DO_PATCH_COMMAND}") +FetchContent_MakeAvailable(do-client) + +add_executable(${PROJECT_NAME}) + +target_sources(${PROJECT_NAME} PRIVATE IntegrationDOClient.cpp) + +set(SFS_CLIENT_LIB_NAME "Microsoft::${CMAKE_PROJECT_NAME}") +add_dependencies(${PROJECT_NAME} ${SFS_CLIENT_LIB_NAME}) +target_link_libraries(${PROJECT_NAME} PRIVATE ${SFS_CLIENT_LIB_NAME}) + +set(DO_SDK_LIB_NAME "Microsoft::deliveryoptimization") +add_dependencies(${PROJECT_NAME} ${DO_SDK_LIB_NAME}) +target_link_libraries(${PROJECT_NAME} PRIVATE ${DO_SDK_LIB_NAME}) + +target_include_directories(${PROJECT_NAME} PUBLIC ../../client/include) diff --git a/src/SfsClient/sfs-client/samples/integration-do-client/IntegrationDOClient.cpp b/src/SfsClient/sfs-client/samples/integration-do-client/IntegrationDOClient.cpp @@ -0,0 +1,332 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include <do_download.h> +#include <sfsclient/SFSClient.h> + +#include <filesystem> +#include <iomanip> +#include <iostream> +#include <sstream> +#include <time.h> +#include <vector> + +#ifdef _WIN32 +#include <combaseapi.h> +#endif + +namespace +{ +void DisplayUsage() +{ + std::cout << "Usage: ExampleIntegrationDOClient --product <identifier> [options]" << std::endl + << std::endl + << "Required:" << std::endl + << " --product <identifier>\t\tName or GUID of the product to be retrieved" << std::endl + << std::endl + << "Options:" << std::endl + << " -h, --help\t\t\tDisplay this help message" << std::endl + << " --accountId <id>\t\tAccount ID of the SFS service, used to identify the caller" << std::endl + << " --outDir <id>\t\tLocation where download will be placed. Defaults to a tmp dir" << std::endl + << std::endl + << std::endl + << "Example:" << std::endl + << " ExampleIntegrationDOClient --product \"msedge-stable-win-x64\" --accountId msedge" << std::endl; +} + +void DisplayHelp() +{ + std::cout + << "ExampleIntegrationDOClient" << std::endl + << "Copyright (c) Microsoft Corporation. All rights reserved." << std::endl + << std::endl + << "Use to interact with the SFS service through the SFS Client library and download using the Delivery Optimization SDK." + << std::endl + << std::endl; + DisplayUsage(); +} + +const std::string c_boldRedStart = "\033[1;31m"; +const std::string c_cyanStart = "\033[0;36m"; +const std::string c_darkGreyStart = "\033[0;90m"; +const std::string c_colorEnd = "\033[0m"; + +void PrintError(std::string_view message) +{ + std::cout << c_boldRedStart << message << c_colorEnd << std::endl; +} + +void PrintLog(std::string_view message) +{ + std::cout << c_cyanStart << message << c_colorEnd << std::endl; +} + +struct Settings +{ + bool displayHelp{true}; + std::string product; + std::string accountId; + std::string outDir; +}; + +int ParseArguments(const std::vector<std::string_view>& args, Settings& settings) +{ + settings = {}; + settings.displayHelp = args.size() == 1; + + const size_t argsSize = args.size(); + auto validateArg = + [&argsSize](const size_t index, const std::string& switchName, const std::string_view& argValue) -> bool { + if (argsSize <= index + 1) + { + PrintError("Missing argument of --" + switchName + "."); + return false; + } + if (!argValue.empty()) + { + PrintError("--" + switchName + " can only be specified once."); + return false; + } + return true; + }; + + for (size_t i = 1; i < args.size(); ++i) + { + if (args[i].compare("-h") == 0 || args[i].compare("--help") == 0) + { + settings.displayHelp = true; + } + else if (args[i].compare("--product") == 0) + { + if (!validateArg(i, "product", settings.product)) + { + return 1; + } + settings.product = args[++i]; + } + else if (args[i].compare("--accountId") == 0) + { + if (!validateArg(i, "accountId", settings.accountId)) + { + return 1; + } + settings.accountId = args[++i]; + } + else if (args[i].compare("--outDir") == 0) + { + if (!validateArg(i, "outDir", settings.outDir)) + { + return 1; + } + settings.outDir = args[++i]; + } + else + { + PrintError("Unknown option " + std::string(args[i]) + ".\n"); + return 1; + } + } + + if (settings.product.empty()) + { + return 1; + } + + return 0; +} + +void LogResult(const SFS::Result& result) +{ + std::cout << " Result code: " << ToString(result.GetCode()); + if (!result.GetMsg().empty()) + { + std::cout << ". Message: " << result.GetMsg(); + } + std::cout << std::endl; +} + +std::string TimestampToString(std::chrono::time_point<std::chrono::system_clock> time) +{ + using namespace std::chrono; + + // get number of milliseconds for the current second + // (remainder after division into seconds) + auto ms = duration_cast<milliseconds>(time.time_since_epoch()) % 1000; + + auto timer = system_clock::to_time_t(time); + + std::stringstream timeStream; + struct tm gmTime; +#ifdef _WIN32 + gmtime_s(&gmTime, &timer); // gmtime_s is the safe version of gmtime, not available on Linux +#else + gmTime = (*std::gmtime(&timer)); +#endif + timeStream << std::put_time(&gmTime, "%F %X"); // yyyy-mm-dd HH:MM:SS + timeStream << '.' << std::setfill('0') << std::setw(3) << ms.count(); + + return timeStream.str(); +} + +void LoggingCallback(const SFS::LogData& logData) +{ + std::cout << c_darkGreyStart << "Log: " << TimestampToString(logData.time) << " [" << ToString(logData.severity) + << "]" << " " << std::filesystem::path(logData.file).filename().string() << ":" << logData.line << " " + << logData.message << c_colorEnd << std::endl; +} + +std::filesystem::path GetOutDir(const std::string& outDir) +{ + if (outDir.empty()) + { + auto tmpDir = std::filesystem::temp_directory_path(); + auto outDir = tmpDir / "SFSDownload"; + std::filesystem::create_directories(outDir); + + // Set write permissions to the download dir since the download happens from a different process, but only in + // non-Windows +#ifndef _WIN32 + std::filesystem::permissions(outDir, + std::filesystem::perms::owner_all | std::filesystem::perms::group_all | + std::filesystem::perms::others_all, + std::filesystem::perm_options::add); +#endif + return outDir.string(); + } + return outDir; +} + +std::vector<SFS::Content> GetLatestDownloadInfo(const Settings& settings) +{ + // Initialize SFSClient + PrintLog("Initializing SFSClient with accountId: " + settings.accountId); + + SFS::ClientConfig config; + config.accountId = settings.accountId; + config.logCallbackFn = LoggingCallback; + + std::unique_ptr<SFS::SFSClient> sfsClient; + auto result = SFS::SFSClient::Make(config, sfsClient); + if (!result) + { + PrintError("Failed to initialize SFSClient."); + LogResult(result); + return {}; + } + + // Perform operations using SFSClient + PrintLog("Getting latest download info for product: " + settings.product); + std::vector<SFS::Content> contents; + SFS::RequestParams params; + params.productRequests = {{settings.product, {}}}; + result = sfsClient->GetLatestDownloadInfo(params, contents); + if (!result) + { + PrintError("Failed to get latest download info."); + LogResult(result); + return {}; + } + + return contents; +} + +#ifdef _WIN32 +class CoInitializeWrapper +{ + public: + CoInitializeWrapper() + { + CoInitializeEx(nullptr, COINIT_MULTITHREADED); + } + + ~CoInitializeWrapper() + { + CoUninitialize(); + } +}; +#endif + +std::string ErrorCodeToHexString(std::error_code error) +{ + std::stringstream stream; + stream << "0x" << std::hex << error.value(); + return stream.str(); +} + +int Download(const SFS::Content& content, const std::string& baseOutDir) +{ + PrintLog("Found content: " + content.GetContentId().GetNameSpace() + "/" + content.GetContentId().GetName() + "/" + + content.GetContentId().GetVersion()); + + const auto outDir = GetOutDir(baseOutDir); + PrintLog("Downloading files to: " + outDir.string()); + + for (const auto& file : content.GetFiles()) + { + PrintLog("Downloading file " + file.GetFileId() + " from " + file.GetUrl()); + auto outFilePath = outDir / file.GetFileId(); + + if (std::filesystem::exists(outFilePath)) + { + // In a real implementation, one would check the file sha1 or sha256 hash to see if the file is the same as + // the current one + PrintLog("File already exists, cannot download again. Skipping."); + continue; + } + + // Download the file using Delivery Optimization SDK + std::unique_ptr<microsoft::deliveryoptimization::download> download; + auto error = microsoft::deliveryoptimization::download::make(file.GetUrl(), outFilePath.string(), download); + if (error) + { + PrintError("Failed to create download object with error " + ErrorCodeToHexString(error)); + return 1; + } + + error = download->start_and_wait_until_completion(); + if (error) + { + PrintError("Failed to download file with error " + ErrorCodeToHexString(error)); + return 1; + } + + PrintLog("File successfully downloaded"); + } + + return 0; +} +} // namespace + +int main(int argc, char* argv[]) +{ + Settings settings; + if (ParseArguments(std::vector<std::string_view>(argv, argv + argc), settings) != 0) + { + DisplayUsage(); + return 1; + } + + if (settings.displayHelp) + { + DisplayHelp(); + return 0; + } + + auto contents = GetLatestDownloadInfo(settings); + if (contents.size() != 1) + { + return 1; + } + + // In Windows the DO SDK uses COM, so we must call CoInitializeEx before using it +#ifdef _WIN32 + CoInitializeWrapper coInitWrapper; +#endif + + if (Download(contents[0], settings.outDir) != 0) + { + return 1; + } + + return 0; +} diff --git a/src/SfsClient/sfs-client/samples/tool/CMakeLists.txt b/src/SfsClient/sfs-client/samples/tool/CMakeLists.txt @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# JSON library +find_package(nlohmann_json CONFIG REQUIRED) + +project(SFSClientTool LANGUAGES CXX) + +add_executable(${PROJECT_NAME}) + +target_sources(${PROJECT_NAME} PRIVATE SFSClientTool.cpp) + +set(SFS_CLIENT_LIB_NAME "Microsoft::${CMAKE_PROJECT_NAME}") +add_dependencies(${PROJECT_NAME} ${SFS_CLIENT_LIB_NAME}) + +target_link_libraries(${PROJECT_NAME} PRIVATE ${SFS_CLIENT_LIB_NAME}) +target_link_libraries(${PROJECT_NAME} PRIVATE nlohmann_json::nlohmann_json) + +target_include_directories(${PROJECT_NAME} PUBLIC ../../client/include) + +set_compile_options_for_target(${PROJECT_NAME}) diff --git a/src/SfsClient/sfs-client/samples/tool/SFSClientTool.cpp b/src/SfsClient/sfs-client/samples/tool/SFSClientTool.cpp @@ -0,0 +1,475 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include <sfsclient/SFSClient.h> + +#include <exception> +#include <filesystem> +#include <iomanip> +#include <iostream> +#include <sstream> +#include <time.h> +#include <vector> + +#include <nlohmann/json.hpp> + +using namespace SFS; +using json = nlohmann::json; + +namespace +{ +void DisplayUsage() +{ + std::cout + << "Usage: SFSClientTool --product <identifier> --accountId <id> [options]" << std::endl + << std::endl + << "Required:" << std::endl + << " --product <identifier>\tName or GUID of the product to be retrieved" << std::endl + << " --accountId <id>\t\tAccount ID of the SFS service, used to identify the caller" << std::endl + << std::endl + << "Options:" << std::endl + << " -h, --help\t\t\tDisplay this help message" << std::endl + << " -v, --version\t\t\tDisplay the library version" << std::endl + << " --isApp\t\tIndicates the specific product is an App" << std::endl + << " --instanceId <id>\t\tA custom SFS instance ID" << std::endl + << " --namespace <ns>\t\tA custom SFS namespace" << std::endl + << " --customUrl <url>\t\tA custom URL for the SFS service. Library must have been built with SFS_ENABLE_OVERRIDES" + << std::endl + << std::endl + << "Example:" << std::endl + << " SFSClientTool --product msedge-stable-win-x64 --accountId msedge" << std::endl; +} + +void DisplayHelp() +{ + std::cout << "SFSClient Tool" << std::endl + << "Copyright (c) Microsoft Corporation. All rights reserved." << std::endl + << std::endl + << "Use to interact with the SFS service through the SFS Client library." << std::endl + << std::endl; + DisplayUsage(); +} + +void DisplayVersion() +{ + std::cout << "SFSClient Tool " << SFSClient::GetVersion() << std::endl; +} + +const std::string c_boldRedStart = "\033[1;31m"; +const std::string c_cyanStart = "\033[0;36m"; +const std::string c_darkGreyStart = "\033[0;90m"; +const std::string c_colorEnd = "\033[0m"; + +void PrintError(std::string_view message) +{ + std::cout << c_boldRedStart << message << c_colorEnd << std::endl; +} + +void PrintLog(std::string_view message) +{ + std::cout << c_cyanStart << message << c_colorEnd << std::endl; +} + +bool AreEqualI(std::string_view a, std::string_view b) +{ + return std::equal(a.begin(), a.end(), b.begin(), b.end(), [](char a, char b) { + return std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b)); + }); +} + +struct Settings +{ + bool displayHelp{true}; + bool displayVersion{false}; + bool isApp{false}; + std::string product; + std::string accountId; + std::string instanceId; + std::string nameSpace; + std::string customUrl; +}; + +void ParseArguments(const std::vector<std::string_view>& args, Settings& settings) +{ + settings = {}; + settings.displayHelp = args.size() == 1; + + const size_t argsSize = args.size(); + auto validateArg = + [&argsSize](const size_t index, const std::string& switchName, const std::string_view& argValue) -> void { + if (argsSize <= index + 1) + { + throw std::runtime_error("Missing argument of --" + switchName); + } + if (!argValue.empty()) + { + throw std::runtime_error("--" + switchName + " can only be specified once"); + } + }; + + auto matchArg = + [](std::string_view arg, const std::string& shortSwitchName, const std::string& longSwitchName) -> bool { + return (!shortSwitchName.empty() && AreEqualI(arg, shortSwitchName)) || AreEqualI(arg, longSwitchName); + }; + + auto matchLongArg = [](std::string_view arg, const std::string& longSwitchName) -> bool { + return AreEqualI(arg, longSwitchName); + }; + + for (size_t i = 1; i < args.size(); ++i) + { + if (matchArg(args[i], "-h", "--help")) + { + settings.displayHelp = true; + } + else if (matchArg(args[i], "-v", "--version")) + { + settings.displayVersion = true; + } + else if (matchLongArg(args[i], "--isApp")) + { + settings.isApp = true; + } + else if (matchLongArg(args[i], "--product")) + { + validateArg(i, "product", settings.product); + settings.product = args[++i]; + } + else if (matchLongArg(args[i], "--accountId")) + { + validateArg(i, "accountId", settings.accountId); + settings.accountId = args[++i]; + } + else if (matchLongArg(args[i], "--instanceId")) + { + validateArg(i, "instanceId", settings.instanceId); + settings.instanceId = args[++i]; + } + else if (matchLongArg(args[i], "--namespace")) + { + validateArg(i, "namespace", settings.nameSpace); + settings.nameSpace = args[++i]; + } + else if (matchLongArg(args[i], "--customUrl")) + { + validateArg(i, "customUrl", settings.customUrl); + settings.customUrl = args[++i]; + } + else + { + throw std::runtime_error("Unknown option " + std::string(args[i])); + } + } + + if (!settings.displayHelp && (settings.product.empty() || settings.accountId.empty())) + { + throw std::runtime_error("--product and --accountId are required and cannot be empty"); + } +} + +constexpr std::string_view ToString(HashType type) +{ + switch (type) + { + case HashType::Sha1: + return "Sha1"; + case HashType::Sha256: + return "Sha256"; + } + return ""; +} + +constexpr std::string_view ToString(Architecture type) +{ + switch (type) + { + case Architecture::None: + return "None"; + case Architecture::Amd64: + return "amd64"; + case Architecture::Arm: + return "arm"; + case Architecture::Arm64: + return "arm64"; + case Architecture::x86: + return "x86"; + } + return ""; +} + +void DisplayResults(const std::vector<Content>& contents) +{ + if (contents.empty()) + { + PrintError("No results found"); + return; + } + + PrintLog("Content found:"); + + json out = json::array(); + for (const auto& content : contents) + { + json j = json::object(); + j["ContentId"]["Namespace"] = content.GetContentId().GetNameSpace(); + j["ContentId"]["Name"] = content.GetContentId().GetName(); + j["ContentId"]["Version"] = content.GetContentId().GetVersion(); + + j["Files"] = json::array(); + for (const auto& file : content.GetFiles()) + { + json fileJson = json::object(); + fileJson["FileId"] = file.GetFileId(); + fileJson["Url"] = file.GetUrl(); + fileJson["SizeInBytes"] = file.GetSizeInBytes(); + json hashes = json::object(); + for (const auto& hash : file.GetHashes()) + { + hashes[ToString(hash.first)] = hash.second; + } + fileJson["Hashes"] = hashes; + j["Files"].push_back(fileJson); + } + out.push_back(j); + } + + PrintLog(out.dump(2 /*indent*/)); +} + +json AppFileToJson(const AppFile& file) +{ + json fileJson = json::object(); + fileJson["FileId"] = file.GetFileId(); + fileJson["Url"] = file.GetUrl(); + fileJson["SizeInBytes"] = file.GetSizeInBytes(); + fileJson["FileMoniker"] = file.GetFileMoniker(); + json hashes = json::object(); + for (const auto& hash : file.GetHashes()) + { + hashes[ToString(hash.first)] = hash.second; + } + fileJson["Hashes"] = hashes; + + fileJson["ApplicabilityDetails"] = json::object(); + fileJson["ApplicabilityDetails"]["Architectures"] = json::array(); + for (const auto& arch : file.GetApplicabilityDetails().GetArchitectures()) + { + fileJson["ApplicabilityDetails"]["Architectures"].push_back(ToString(arch)); + } + fileJson["ApplicabilityDetails"]["PlatformApplicabilityForPackage"] = json::array(); + for (const auto& app : file.GetApplicabilityDetails().GetPlatformApplicabilityForPackage()) + { + fileJson["ApplicabilityDetails"]["PlatformApplicabilityForPackage"].push_back(app); + } + + return fileJson; +} + +void DisplayResults(const std::vector<AppContent>& contents) +{ + if (contents.empty()) + { + std::cout << "No results found." << std::endl; + return; + } + + PrintLog("Content found:"); + + json out = json::array(); + for (const auto& content : contents) + { + json j = json::object(); + j["ContentId"]["Namespace"] = content.GetContentId().GetNameSpace(); + j["ContentId"]["Name"] = content.GetContentId().GetName(); + j["ContentId"]["Version"] = content.GetContentId().GetVersion(); + j["UpdateId"] = content.GetUpdateId(); + + j["Files"] = json::array(); + for (const auto& file : content.GetFiles()) + { + j["Files"].push_back(AppFileToJson(file)); + } + + j["Prerequisites"] = json::array(); + for (const auto& prereq : content.GetPrerequisites()) + { + json prereqJson = json::object(); + prereqJson["ContentId"]["Namespace"] = prereq.GetContentId().GetNameSpace(); + prereqJson["ContentId"]["Name"] = prereq.GetContentId().GetName(); + prereqJson["ContentId"]["Version"] = prereq.GetContentId().GetVersion(); + + prereqJson["Files"] = json::array(); + for (const auto& file : prereq.GetFiles()) + { + prereqJson["Files"].push_back(AppFileToJson(file)); + } + + j["Prerequisites"].push_back(prereqJson); + } + out.push_back(j); + } + + PrintLog(out.dump(2 /*indent*/)); +} + +void LogResult(const SFS::Result& result) +{ + std::cout << " Result code: " << ToString(result.GetCode()); + if (!result.GetMsg().empty()) + { + std::cout << ". Message: " << result.GetMsg(); + } + std::cout << std::endl; +} + +std::string TimestampToString(std::chrono::time_point<std::chrono::system_clock> time) +{ + using namespace std::chrono; + + // get number of milliseconds for the current second + // (remainder after division into seconds) + auto ms = duration_cast<milliseconds>(time.time_since_epoch()) % 1000; + + auto timer = system_clock::to_time_t(time); + + std::stringstream timeStream; + struct tm gmTime; +#ifdef _WIN32 + gmtime_s(&gmTime, &timer); // gmtime_s is the safe version of gmtime, not available on Linux +#else + gmTime = (*std::gmtime(&timer)); +#endif + timeStream << std::put_time(&gmTime, "%F %X"); // yyyy-mm-dd HH:MM:SS + timeStream << '.' << std::setfill('0') << std::setw(3) << ms.count(); + + return timeStream.str(); +} + +void LoggingCallback(const SFS::LogData& logData) +{ + std::cout << c_darkGreyStart << "Log: " << TimestampToString(logData.time) << " [" << ToString(logData.severity) + << "]" << " " << std::filesystem::path(logData.file).filename().string() << ":" << logData.line << " " + << logData.message << c_colorEnd << std::endl; +} + +bool SetEnv(const std::string& varName, const std::string& value) +{ + if (varName.empty() || value.empty()) + { + return false; + } +#ifdef _WIN32 + return _putenv_s(varName.c_str(), value.c_str()) == 0; +#else + return setenv(varName.c_str(), value.c_str(), 1 /*overwrite*/) == 0; +#endif +} + +Result GetLatestDownloadInfo(const SFSClient& sfsClient, const Settings& settings) +{ + PrintLog("Getting latest download info for product: " + settings.product); + RequestParams params; + params.productRequests = {{settings.product, {}}}; + if (settings.isApp) + { + std::vector<AppContent> appContents; + auto result = sfsClient.GetLatestAppDownloadInfo(params, appContents); + if (!result) + { + PrintError("Failed to get latest download info for app."); + LogResult(result); + return result.GetCode(); + } + + // Display results + DisplayResults(appContents); + } + else + { + std::vector<Content> contents; + auto result = sfsClient.GetLatestDownloadInfo(params, contents); + + if (!result) + { + PrintError("Failed to get latest download info."); + LogResult(result); + return result.GetCode(); + } + + // Display results + DisplayResults(contents); + } + + return Result::Success; +} +} // namespace + +int main(int argc, char* argv[]) +{ + Settings settings; + try + { + ParseArguments(std::vector<std::string_view>(argv, argv + argc), settings); + } + catch (const std::runtime_error& e) + { + PrintError(std::string(e.what()) + "\n"); + DisplayUsage(); + return 1; + } + + if (settings.displayVersion) + { + DisplayVersion(); + return 0; + } + + if (settings.displayHelp) + { + DisplayHelp(); + return 0; + } + + if (!settings.customUrl.empty()) + { + SetEnv("SFS_TEST_OVERRIDE_BASE_URL", settings.customUrl); + PrintLog("Using custom URL: " + settings.customUrl); + PrintLog("Note that the library must have been built with SFS_ENABLE_OVERRIDES to use a custom URL."); + } + + // Initialize SFSClient + PrintLog("Initializing SFSClient with accountId: " + settings.accountId + + (settings.instanceId.empty() ? "" : ", instanceId: " + settings.instanceId) + + (settings.nameSpace.empty() ? "" : ", namespace: " + settings.nameSpace)); + + ClientConfig config; + config.accountId = settings.accountId; + if (!settings.instanceId.empty()) + { + config.instanceId = settings.instanceId; + } + if (!settings.nameSpace.empty()) + { + config.nameSpace = settings.nameSpace; + } + config.logCallbackFn = LoggingCallback; + + std::unique_ptr<SFSClient> sfsClient; + auto result = SFSClient::Make(config, sfsClient); + if (!result) + { + PrintError("Failed to initialize SFSClient."); + LogResult(result); + return result.GetCode(); + } + + // Perform operations using SFSClient + result = GetLatestDownloadInfo(*sfsClient, settings); + if (!result) + { + LogResult(result); + return result.GetCode(); + } + + return 0; +} diff --git a/src/SfsClient/sfs-client/scripts/Build.ps1 b/src/SfsClient/sfs-client/scripts/Build.ps1 @@ -0,0 +1,90 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# +.SYNOPSIS +Simplifies build commands for the SFS Client. + +.PARAMETER Clean +Use this to clean the build folder before building. + +.PARAMETER BuildType +Use this to define the build type between "Debug" and "Release". The default is "Debug". + +.PARAMETER EnableTestOverrides +Use this to enable test overrides. + +.PARAMETER BuildTests +Use this to enable building tests. On by default. + +.PARAMETER BuildSamples +Use this to enable building samples. On by default. + +.DESCRIPTION +This script will contain the build commands for the SFS Client. The default build folder will be "<git_root>/build". +Use this on Windows platforms in a PowerShell session. + +.EXAMPLE +PS> ./scripts/Build.ps1 +#> +param ( + [switch] $Clean = $false, + # BuildType is a build time parameter for Visual Studio generator in CMake + [ValidateSet("Debug", "Release")] + [string] $BuildType = "Debug", + # Make sure when adding a new switch below to check if it requires CMake regeneration + [switch] $EnableTestOverrides = $false, + [bool] $BuildTests = $true, + [bool] $BuildSamples = $true +) + +$GitRoot = (Resolve-Path (&git -C $PSScriptRoot rev-parse --show-toplevel)).Path +if (!(Test-Path "$GitRoot\vcpkg")) +{ + throw "vcpkg not found at $GitRoot\vcpkg. Run the Setup.ps1 script first." +} + +$BuildFolder = "$GitRoot/build" +if ($Clean -and (Test-Path $BuildFolder)) +{ + Write-Host -ForegroundColor Yellow "Cleaning build folder before build..." + Remove-Item -Recurse -Force $BuildFolder +} + +$Regenerate = $false +$CMakeCacheFile = "$BuildFolder\CMakeCache.txt" +$EnableTestOverridesStr = if ($EnableTestOverrides) {"ON"} else {"OFF"} +$BuildTestsOverridesStr = if ($BuildTests) {"ON"} else {"OFF"} +$BuildSamplesOverridesStr = if ($BuildSamples) {"ON"} else {"OFF"} + +function Test-CMakeCacheValueNoMatch($CMakeCacheFile, $Pattern, $ExpectedValue) +{ + $Match = Select-String -Path $CMakeCacheFile -Pattern $Pattern + if ($null -ne $Match -and $null -ne $Match.Matches.Groups[1]) + { + return $ExpectedValue -ne $Match.Matches.Groups[1].Value + } + return $true +} + +if (Test-Path $CMakeCacheFile) +{ + # Regenerate if one of the build options is set to a different value than the one passed in + $Regenerate = Test-CMakeCacheValueNoMatch $CMakeCacheFile "^SFS_ENABLE_TEST_OVERRIDES:BOOL=(.*)$" $EnableTestOverridesStr + $Regenerate = Test-CMakeCacheValueNoMatch $CMakeCacheFile "^SFS_BUILD_TESTS:BOOL=(.*)$" $BuildTestsOverridesStr + $Regenerate = Test-CMakeCacheValueNoMatch $CMakeCacheFile "^SFS_BUILD_SAMPLES:BOOL=(.*)$" $BuildSamplesOverridesStr +} + +# Configure cmake if build folder doesn't exist or if the build must be regenerated. +# This creates build targets that will be used by the build command +if (!(Test-Path $BuildFolder) -or $Regenerate) +{ + $Options = "-DSFS_ENABLE_TEST_OVERRIDES=$EnableTestOverridesStr"; + $Options += " -DSFS_BUILD_TESTS=$BuildTestsOverridesStr"; + $Options += " -DSFS_BUILD_SAMPLES=$BuildSamplesOverridesStr"; + $Options += " -DSFS_WINDOWS_STATIC_ONLY=ON"; + Invoke-Expression "cmake -S $GitRoot -B $BuildFolder $Options" +} + +# This is the build command. If any CMakeLists.txt files change, this will also reconfigure before building +cmake --build $BuildFolder --config $BuildType diff --git a/src/SfsClient/sfs-client/scripts/Setup.ps1 b/src/SfsClient/sfs-client/scripts/Setup.ps1 @@ -0,0 +1,156 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# +.SYNOPSIS +Sets up dependencies required to build and work with the SFS Client. + +.DESCRIPTION +This script will install all of the dependencies required to build and work with the SFS Client. +Use this on Windows platforms in a PowerShell session. + +.EXAMPLE +PS> .\scripts\Setup.ps1 +#> +param ( + [switch] $NoBuildTools +) + +$ErrorActionPreference = "Stop" + +$GitRoot = (Resolve-Path (&git -C $PSScriptRoot rev-parse --show-toplevel)).Path + +function Update-Env { + $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") +} + +function Install-Python { + Write-Host -ForegroundColor Cyan "Installing latest Python if it's not installed" + + # We use the "python --version" command to check if it exists because by default Windows now comes + # with a python.exe from the Store that simply opens the Store when someone tries to use python.exe + python --version 2>&1 | Out-Null + if (!$?) { + winget install python + if (!$?) { + Write-Host -ForegroundColor Red "Failed to install Python" + exit 1 + } + } +} + +function Install-PipDependencies { + Write-Host -ForegroundColor Cyan "`nInstalling dependencies using pip" + + # Upgrade pip and install requirements. Filter out output for dependencies that are already installed + $PipInstalledPackageString = "Requirement already satisfied" + python -m pip install --upgrade pip | Select-String -Pattern $PipInstalledPackageString -NotMatch + + # Installing to user site packages + $PipReqs = Join-Path $PSScriptRoot "pip.requirements.txt" -Resolve + pip install --user -r $PipReqs | Select-String -Pattern $PipInstalledPackageString -NotMatch +} + +function Install-CMake { + Write-Host -ForegroundColor Cyan "`nInstalling cmake if it's not installed" + + # Installing cmake from winget because it adds to PATH + try { + cmake --version + } + catch { + winget install cmake + if (!$?) { + Write-Host -ForegroundColor Red "Failed to install cmake" + exit 1 + } + + # Update env with newly installed cmake + Update-Env + } +} + +function Install-CppBuildTools { + if ($NoBuildTools) { + Write-Host -ForegroundColor Yellow "`nSkipping C++ Build Tools installation" + return + } + + Write-Host -ForegroundColor Cyan "`nInstalling C++ Build Tools" + + # - Microsoft.VisualStudio.Workload.VCTools is the C++ workload in the Visual Studio Build Tools + # --wait makes the install synchronous + winget install Microsoft.VisualStudio.2022.BuildTools --silent --override "--wait --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended --remove Microsoft.VisualStudio.Component.VC.CMake.Project" + if (!$?) { + Write-Host -ForegroundColor Red "Failed to install build tools" + exit 1 + } +} + +function Install-Vcpkg { + Write-Host -ForegroundColor Cyan "`nSetting up vcpkg" + + if (!(Test-Path vcpkg)) { + Write-Host "Cloning vcpkg repo" + git clone https://github.com/microsoft/vcpkg $GitRoot\vcpkg + & "$GitRoot\vcpkg\bootstrap-vcpkg.bat" + } + else { + Write-Host "Checking if vcpkg repo has new commits" + $NoUpdatesString = "Already up to date." + git -C "$GitRoot\vcpkg" pull --show-forced-updates | Select-String -Pattern $NoUpdatesString -NotMatch + } +} + +function Set-GitHooks { + Write-Host -ForegroundColor Cyan "`nSetting Git hooks" + + $HookDestDir = Join-Path $GitRoot "\.git\hooks" -Resolve + $GitHooks = @{"pre-commit-wrapper.sh" = "pre-commit" } + foreach ($i in $GitHooks.GetEnumerator()) { + $HookSrc = Join-Path $GitRoot $i.Name -Resolve + $HookDest = Join-Path $HookDestDir $i.Value + + # If the destination doesn't exist or is different than the one in the source, we'll copy it over. + if (-not (Test-Path $HookDest) -or (Get-FileHash $HookDest).Hash -ne (Get-FileHash $HookSrc).Hash) { + Copy-Item -Path $HookSrc -Destination $HookDest -Force | Out-Null + Write-Host -ForegroundColor Cyan "Setup git $($i.Value) hook with $HookSrc" + } + } +} + +function Set-Aliases { + Write-Host -ForegroundColor Cyan "`nSetting aliases" + + $PythonScriptsDir = python -c "import os,sysconfig;print(sysconfig.get_path('scripts',f'{os.name}_user'))" + + $Aliases = [ordered]@{ + "build" = "$GitRoot\scripts\Build.ps1" + "clang-format" = "$PythonScriptsDir\clang-format.exe" + "cmake-format" = "$PythonScriptsDir\cmake-format.exe" + "test" = "$GitRoot\scripts\Test.ps1" + "vcpkg" = "$GitRoot\vcpkg\vcpkg.exe" + } + foreach ($i in $Aliases.GetEnumerator()) { + $Alias = $i.Name + $Target = $i.Value + + # If the alias doesn't exist or is set to something different than the one we want to target, we'll add it. + $AliasDoesNotExist = !(Get-Alias $Alias -ErrorAction SilentlyContinue) + if ($AliasDoesNotExist -or (Get-Alias $Alias).Definition -ne $Target) { + if (!$AliasDoesNotExist) { + Remove-Alias $Alias -Scope Global + } + New-Alias -Name $Alias -Value $Target -Scope Global + Write-Host "Setup alias $Alias" + } + } +} + +Install-Python +Install-PipDependencies +Install-CMake +Install-CppBuildTools +Install-Vcpkg +Set-GitHooks +Set-Aliases diff --git a/src/SfsClient/sfs-client/scripts/Test.ps1 b/src/SfsClient/sfs-client/scripts/Test.ps1 @@ -0,0 +1,28 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# +.SYNOPSIS +Simplifies test commands for the SFS Client. + +.DESCRIPTION +This script will contain the test commands for the SFS Client. +Use this on Windows platforms in a PowerShell session. + +.EXAMPLE +PS> ./scripts/Test.ps1 +#> +param ( + [switch] $OutputOnFailure = $false +) + +$GitRoot = (Resolve-Path (&git -C $PSScriptRoot rev-parse --show-toplevel)).Path +$BuildFolder = "$GitRoot/build" + +$cmd = "ctest --test-dir ""$BuildFolder/client""" +if ($OutputOnFailure) +{ + $cmd += " --output-on-failure" +} + +Invoke-Expression $cmd diff --git a/src/SfsClient/sfs-client/scripts/build.sh b/src/SfsClient/sfs-client/scripts/build.sh @@ -0,0 +1,163 @@ +#!/bin/bash + +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Synopsis: Simplifies build commands for the SFS Client. +# +# Description: This script will contain the build commands for the SFS Client. The default build folder will be "<git_root>/build". +# Use this on non-Windows platforms in a bash session. +# +# Example: +# $ ./scripts/build.sh +# + +# Ensures script stops on errors +set -e + +if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then + error "Script is being sourced, it should be executed instead." + return 1 +fi + +COLOR_RED="\033[1;31m" +COLOR_YELLOW="\033[1;33m" +BOLD_DEFAULT_COLOR="\033[1m" +NO_COLOR="\033[0m" + +error() { echo -e "${COLOR_RED}$*${NO_COLOR}" >&2; exit 1; } +warn() { echo -e "${COLOR_YELLOW}$*${NO_COLOR}"; } + +clean=false +enable_test_overrides="OFF" +build_tests="ON" +build_samples="ON" +build_type="Debug" + +usage() { echo -e "Usage: $0 [-c|--clean] [-b|--build-type {Debug,Release}] [-t|--enable-test-overrides] + [--build-tests {${BOLD_DEFAULT_COLOR}ON${NO_COLOR}, OFF}] [--build-samples {${BOLD_DEFAULT_COLOR}ON${NO_COLOR}, OFF}]" 1>&2; exit 1; } + +# Make sure when adding a new option to check if it requires CMake regeneration + +if ! opts=$(getopt \ + --longoptions "clean,build-type:,enable-test-overrides,build-tests:,build-samples:" \ + --name "$(basename "$0")" \ + --options "cb:t" \ + -- "$@" +); then + usage +fi + +eval set "--$opts" + +while [ $# -gt 0 ]; do + case "$1" in + -c|--clean) + clean=true + shift 1 + ;; + -b|--build-type) + shift 1 + build_type=$1 + case "$build_type" in + Debug|Release) + ;; + *) + usage + ;; + esac + shift 1 + ;; + -t|--enable-test-overrides) + enable_test_overrides="ON" + shift 1 + ;; + --build-tests) + shift 1 + build_tests=$1 + case "$build_tests" in + ON|OFF) + ;; + *) + usage + ;; + esac + shift 1 + ;; + --build-samples) + shift 1 + build_samples=$1 + case "$build_samples" in + ON|OFF) + ;; + *) + usage + ;; + esac + shift 1 + ;; + *) + break + ;; + esac +done + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null 2>&1 && pwd)" +git_root=$(git -C "$script_dir" rev-parse --show-toplevel) + +vcpkg_dir="$git_root/vcpkg" +if [ ! -d "$vcpkg_dir" ]; then + error "vcpkg not found at $git_root/vcpkg. Source the setup.sh script first." +fi + +build_folder="$git_root/build" +if $clean && [ -d "$build_folder" ]; then + warn "Cleaning build folder before build..." + rm -r -f "$build_folder" +fi + +regenerate=false +cmake_cache_file="$build_folder/CMakeCache.txt" + +test_cmake_cache_value_no_match() { + local cmake_cache_file=$1 + local pattern=$2 + local expected_value=$3 + + value=$(sed -nr "s/$pattern/\1/p" "$cmake_cache_file") + if [ -n "$value" ] && [ "$value" == "$expected_value" ]; then + return 1 + fi + return 0 +} + +if [ -f "$cmake_cache_file" ]; then + # Regenerate if one of the build options is set to a different value than the one passed in + if test_cmake_cache_value_no_match "$cmake_cache_file" "^CMAKE_BUILD_TYPE:STRING=(.*)$" "$build_type"; then + regenerate=true + fi + if test_cmake_cache_value_no_match "$cmake_cache_file" "^SFS_ENABLE_TEST_OVERRIDES:BOOL=(.*)$" "$enable_test_overrides"; then + regenerate=true + fi + if test_cmake_cache_value_no_match "$cmake_cache_file" "^SFS_BUILD_TESTS:BOOL=(.*)$" "$build_tests"; then + regenerate=true + fi + if test_cmake_cache_value_no_match "$cmake_cache_file" "^SFS_BUILD_SAMPLES:BOOL=(.*)$" "$build_samples"; then + regenerate=true + fi +fi + +# Configure cmake if build folder doesn't exist or if the build must be regenerated. +# This creates build targets that will be used by the build command +if [ ! -d "$build_folder" ] || $regenerate ; then + cmake \ + -S "$git_root" \ + -B "$build_folder" \ + -DCMAKE_BUILD_TYPE="$build_type" \ + -DSFS_ENABLE_TEST_OVERRIDES="$enable_test_overrides" \ + -DSFS_BUILD_TESTS="$build_tests" \ + -DSFS_BUILD_SAMPLES="$build_samples" +fi + +# This is the build command. If any CMakeLists.txt files change, this will also reconfigure before building +cmake --build "$build_folder" diff --git a/src/SfsClient/sfs-client/scripts/check-format.py b/src/SfsClient/sfs-client/scripts/check-format.py @@ -0,0 +1,66 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Checks the formatting of the codebase and fails if it is not correct.""" + +import os +import subprocess +import sys +import sysconfig + +script_dir = os.path.dirname(os.path.realpath(__file__)) +root_dir = os.path.join(script_dir, "..") +os.chdir(root_dir) + +user_dir = sysconfig.get_path("scripts",f"{os.name}_user") +if os.name == 'nt': + clang_format = "{}\clang-format.exe".format(user_dir) + cmake_format = "{}\cmake-format.exe".format(user_dir) +else: + clang_format = "{}/clang-format".format(user_dir) + cmake_format = "{}/cmake-format".format(user_dir) + +if not os.path.exists(clang_format): + print("clang-format not found at: {}".format(clang_format)) + sys.exit(1) + +if not os.path.exists(cmake_format): + print("cmake-format not found at: {}".format(cmake_format)) + sys.exit(1) + +cmake_filename='CMakeLists.txt' + +# Find all the interesting files in the repository +clang_files = [] +cmake_files = [] + +all_files = subprocess.check_output('git ls-files', shell=True, text=True).strip('\n').split('\n') + +for file in all_files: + if file.endswith('.cpp') or file.endswith('.h'): + clang_files.append(file) + if file.endswith(cmake_filename): + cmake_files.append(file) + +# Run clang and cmake-format on all the interesting files +unformatted_files = [] +for file in clang_files: + # When the file is unformatted, clang-format returns a zero exit code, but a non-empty stderr + result = subprocess.check_output("{} {} -n".format(clang_format, file), shell=True, text=True, stderr=subprocess.STDOUT) + if len(result) > 0: + unformatted_files.append(file) +for file in cmake_files: + # When the file is unformatted, cmake-format returns a non-zero exit code + result = subprocess.run("{} {} --check".format(cmake_format, file), stderr=subprocess.DEVNULL, shell=True) + if result.returncode != 0: + unformatted_files.append(file) + +unformatted_files.sort() + +if unformatted_files: + print("The following files have incorrect formatting:\n") + for file in unformatted_files: + print(file) + sys.exit(1) + +print("All files have correct formatting.") diff --git a/src/SfsClient/sfs-client/scripts/pip.requirements.txt b/src/SfsClient/sfs-client/scripts/pip.requirements.txt @@ -0,0 +1,2 @@ +clang-format==18.1.4 +cmake-format==0.6.13+ \ No newline at end of file diff --git a/src/SfsClient/sfs-client/scripts/setup.sh b/src/SfsClient/sfs-client/scripts/setup.sh @@ -0,0 +1,190 @@ +#!/bin/bash + +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Synopsis: Sets up dependencies required to build and work with the SFS Client. +# +# Description: This script will install all of the dependencies required to build and work with the SFS Client. +# Use this on non-Windows platforms in a bash session. It must be sourced. +# +# Example: +# $ source ./scripts/setup.sh +# + +COLOR_RED="\033[1;31m" +COLOR_CYAN="\033[1;36m" +NO_COLOR="\033[0m" + +header() { echo -e "${COLOR_CYAN}$*${NO_COLOR}"; } +error() { echo -e "${COLOR_RED}$*${NO_COLOR}" >&2; } + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + error "Script is being run directly, it should be sourced instead." + exit 1 +fi + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null 2>&1 && pwd)" +git_root=$(git -C "$script_dir" rev-parse --show-toplevel) + +sudo apt-get -qq update + +install_with_apt() { + program_to_install=$1 + + if ! sudo apt-get -qq install "$program_to_install"; then + error "Failed to install $program_to_install" + return 1 + fi + return 0 +} + +install_python() { + header "Installing latest Python if it's not installed" + + if ! install_with_apt "python3"; then + return + fi + if ! install_with_apt "python3-pip"; then + return + fi + return 0 +} + +install_pip_dependencies() { + header "\nInstalling dependencies using pip" + + # Upgrade pip and install requirements. Filter out output for dependencies that are already installed + python3 -m pip install --upgrade pip | grep -v -e "already satisfied" -e "Defaulting to user installation" + ret="${PIPESTATUS[0]}" + if [ "$ret" -ne 0 ]; then + return "$ret" + fi + + pip_reqs="$script_dir/pip.requirements.txt" + + pip install -r "$pip_reqs" | grep -v -e "already satisfied" -e "Defaulting to user installation" + ret="${PIPESTATUS[0]}" + if [ "$ret" -ne 0 ]; then + return "$ret" + fi + + return 0 +} + +install_cmake() { + header "\nInstalling cmake if it's not installed" + + if ! install_with_apt "cmake"; then + return + fi + return 0 +} + +install_vcpkg() { + header "\nSetting up vcpkg" + + vcpkg_dir="$git_root/vcpkg" + if [ -d "$vcpkg_dir" ]; then + echo "Checking if vcpkg repo has new commits" + git -C "$vcpkg_dir" pull --show-forced-updates | grep -v 'Already up to date' + else + echo "Cloning vcpkg repo" + git clone https://github.com/microsoft/vcpkg "$vcpkg_dir" + # Needed for the bootstrap and for other vcpkg packages + if ! sudo apt-get -qq install curl zip unzip tar pkg-config; then + return + fi + "$vcpkg_dir/bootstrap-vcpkg.sh" + fi + return 0 +} + +# Dependency for compiling CorrelationVector C++ library +install_uuid() { + header "\nInstalling uuid-dev if it's not installed" + + if ! install_with_apt "pkg-config"; then + return + fi + if ! install_with_apt "uuid-dev"; then + return + fi + return 0 +} + +set_git_hooks() { + header "\nSetting Git hooks" + + hook_dest_dir="$git_root/.git/hooks" + declare -A git_hooks=( ["pre-commit-wrapper.sh"]="pre-commit") + for src in "${!git_hooks[@]}"; do + hook_src="$git_root/$src" + hook_dest="$hook_dest_dir/${git_hooks[$src]}" + + # If the destination doesn't exist or is different than the one in the source, we'll copy it over. + if [ ! -f "$hook_dest" ] || ! cmp -s "$hook_src" "$hook_dest"; then + cp -f "$hook_src" "$hook_dest" + echo "Setup git ${git_hooks[$src]} hook with $hook_src" + fi + done +} + +set_aliases() { + header "\nSetting aliases" + + python_scripts_dir=$(python3 -c 'import os,sysconfig;print(sysconfig.get_path("scripts",f"{os.name}_user"))') + + declare -A aliases=( ["build"]="$git_root/scripts/build.sh" + ["clang-format"]="$python_scripts_dir/clang-format" + ["cmake-format"]="$python_scripts_dir/cmake-format" + ["test"]="$git_root/scripts/test.sh" + ["vcpkg"]="$git_root/vcpkg/vcpkg") + + for alias in "${!aliases[@]}"; do + target="${aliases[$alias]}" + + # If the alias doesn't exist or is set to something different than the one we want to target, we'll add it. + + # If alias doesn't exist, the alias command returns a non-zero result + alias_does_not_exist=false + alias_exists_not_the_same=false + if ! alias "$alias" > /dev/null 2>&1; then + alias_does_not_exist=true + else + output=$(alias "$alias") + expected_output="alias $alias='$target'" + if [ "$output" != "$expected_output" ]; then + alias_exists_not_the_same=true + fi + fi + + if $alias_does_not_exist || $alias_exists_not_the_same; then + eval "alias $alias=$target" + echo "Setup alias $alias" + fi + done +} + +if ! install_python; then + return +fi + +if ! install_pip_dependencies; then + return +fi + +if ! install_cmake; then + return +fi + +if ! install_vcpkg; then + return +fi + +if ! install_uuid; then + return +fi + +set_git_hooks +set_aliases diff --git a/src/SfsClient/sfs-client/scripts/test.sh b/src/SfsClient/sfs-client/scripts/test.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Synopsis: Simplifies test commands for the SFS Client. +# +# Description: This script will contain the test commands for the SFS Client. +# Use this on non-Windows platforms in a bash session. +# +# Example: +# $ ./scripts/test.sh +# + +# Ensures script stops on errors +set -e + +if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then + error "Script is being sourced, it should be executed instead." + return 1 +fi + +output_on_failure=false + +usage() { echo "Usage: $0 [--output-on-failure]" 1>&2; exit 1; } + +if ! opts=$(getopt \ + --longoptions "output-on-failure" \ + --name "$(basename "$0")" \ + --options "" \ + -- "$@" +); then + usage +fi + +eval set "--$opts" + +while [ $# -gt 0 ]; do + case "$1" in + --output-on-failure) + output_on_failure=true + shift 1 + ;; + *) + break + ;; + esac +done + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null 2>&1 && pwd)" +git_root=$(git -C "$script_dir" rev-parse --show-toplevel) +build_folder="$git_root/build" + +cmd="ctest --test-dir \"$build_folder/client\"" + +if $output_on_failure ; then + cmd+=" --output-on-failure" +fi + +eval "$cmd" diff --git a/src/SfsClient/sfs-client/sfs-client-vcpkg-port/sfs-client/portfile.cmake b/src/SfsClient/sfs-client/sfs-client-vcpkg-port/sfs-client/portfile.cmake @@ -0,0 +1,37 @@ +# This file is a template portfile to be used to consume the sfs-client library using the vcpkg infrastructure + +# Choose one of the consumption formats below. SOURCE_PATH must be defined after either step. + +# 1. Get from Github +# vcpkg_from_github( +# OUT_SOURCE_PATH SOURCE_PATH +# REPO microsoft/sfs-client +# REF <commit-id> +# SHA512 0 # Run vcpkg the first time and this value will be shown in the logs for you to replace +# HEAD_REF main +# ) + +# 2. Use a locally available path, like a subtree'd repository +# set(SOURCE_PATH D:/sfs-client) + +# Preferring static linkage in Windows +if(VCPKG_TARGET_IS_WINDOWS) + vcpkg_check_linkage(ONLY_STATIC_LIBRARY) +endif() + +# Configure and install the library +vcpkg_cmake_configure(SOURCE_PATH "${SOURCE_PATH}") +vcpkg_cmake_install() +vcpkg_fixup_pkgconfig() + +# Exposes the package targets like other vcpkg packages do +vcpkg_cmake_config_fixup(PACKAGE_NAME sfsclient CONFIG_PATH lib/cmake/sfsclient) + +# Handle copyright +vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE") + +# Handle usage +file(COPY "${CMAKE_CURRENT_LIST_DIR}/usage" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") + +# Remove duplicated include files +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") diff --git a/src/SfsClient/sfs-client/sfs-client-vcpkg-port/sfs-client/usage b/src/SfsClient/sfs-client/sfs-client-vcpkg-port/sfs-client/usage @@ -0,0 +1,4 @@ +The package sfs-client provides CMake targets: + + find_package(sfsclient CONFIG REQUIRED) + target_link_libraries(main PRIVATE microsoft::sfsclient) diff --git a/src/SfsClient/sfs-client/sfs-client-vcpkg-port/sfs-client/vcpkg.json b/src/SfsClient/sfs-client/sfs-client-vcpkg-port/sfs-client/vcpkg.json @@ -0,0 +1,30 @@ +{ + "name": "sfs-client", + "version": "<VERSION>", + "description": "Simple File Solution (SFS) Client", + "homepage": "https://github.com/microsoft/sfs-client", + "license": "MIT", + "dependencies": [ + { + "name": "curl", + "features": [ + "c-ares", + { + "name": "openssl", + "platform": "!windows", + "$comment": "TODO #42: OpenSSL is not needed on Windows yet while we don't have cert pinning. Default is schannel, which uses Windows store" + } + ] + }, + "nlohmann-json", + "correlation-vector-cpp", + { + "name": "vcpkg-cmake", + "host": true + }, + { + "name": "vcpkg-cmake-config", + "host": true + } + ] +}+ \ No newline at end of file diff --git a/src/SfsClient/sfs-client/vcpkg-custom-triplets/x64-windows-static-custom.cmake b/src/SfsClient/sfs-client/vcpkg-custom-triplets/x64-windows-static-custom.cmake @@ -0,0 +1,6 @@ +set(VCPKG_TARGET_ARCHITECTURE x64) + +message(STATUS "PORTs are preferred with static linkage...") + +set(VCPKG_CRT_LINKAGE static) +set(VCPKG_LIBRARY_LINKAGE static) diff --git a/src/SfsClient/sfs-client/vcpkg.json b/src/SfsClient/sfs-client/vcpkg.json @@ -0,0 +1,64 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", + "$in-case-of-update": "If updating this file, please also update the cgmanifest.json file. See DEVELOPMENT.md for more", + "features": { + "tests": { + "description": "Build tests", + "dependencies": [ + "catch2", + "cpp-httplib" + ] + } + }, + "dependencies": [ + "correlation-vector-cpp", + { + "name": "curl", + "features": [ + "c-ares", + { + "name": "openssl", + "platform": "!windows", + "$comment": "TODO #42: OpenSSL is not needed on Windows yet while we don't have cert pinning. Default is schannel, which uses Windows store" + } + ] + }, + "nlohmann-json" + ], + "overrides": [ + { + "name": "c-ares", + "version": "1.19.1" + }, + { + "name": "catch2", + "version": "3.4.0" + }, + { + "name": "cpp-httplib", + "version": "0.15.3" + }, + { + "name": "correlation-vector-cpp", + "version": "1.0" + }, + { + "name": "curl", + "version": "8.4.0" + }, + { + "name": "nlohmann-json", + "version": "3.11.3" + }, + { + "name": "openssl", + "version": "3.1.4#1" + } + ], + "vcpkg-configuration": { + "overlay-triplets": [ + "./vcpkg-custom-triplets" + ] + }, + "builtin-baseline": "f56238700757aa05975e41fa835739c632810f3f" +}+ \ No newline at end of file diff --git a/src/VcpkgCustomTriplets/arm-release-static.cmake b/src/VcpkgCustomTriplets/arm-release-static.cmake @@ -0,0 +1,6 @@ +set(VCPKG_TARGET_ARCHITECTURE arm) +set(VCPKG_CRT_LINKAGE static) +set(VCPKG_LIBRARY_LINKAGE static) +set(VCPKG_C_FLAGS "/Qspectre") +set(VCPKG_CXX_FLAGS "/Qspectre") +set(VCPKG_BUILD_TYPE release) diff --git a/src/VcpkgCustomTriplets/arm-release.cmake b/src/VcpkgCustomTriplets/arm-release.cmake @@ -0,0 +1,6 @@ +set(VCPKG_TARGET_ARCHITECTURE arm) +set(VCPKG_CRT_LINKAGE dynamic) +set(VCPKG_LIBRARY_LINKAGE static) +set(VCPKG_C_FLAGS "/Qspectre") +set(VCPKG_CXX_FLAGS "/Qspectre") +set(VCPKG_BUILD_TYPE release) diff --git a/src/VcpkgCustomTriplets/arm.cmake b/src/VcpkgCustomTriplets/arm.cmake @@ -0,0 +1,3 @@ +set(VCPKG_TARGET_ARCHITECTURE arm) +set(VCPKG_CRT_LINKAGE dynamic) +set(VCPKG_LIBRARY_LINKAGE static) diff --git a/src/VcpkgCustomTriplets/arm64-release-static.cmake b/src/VcpkgCustomTriplets/arm64-release-static.cmake @@ -0,0 +1,6 @@ +set(VCPKG_TARGET_ARCHITECTURE arm64) +set(VCPKG_CRT_LINKAGE static) +set(VCPKG_LIBRARY_LINKAGE static) +set(VCPKG_C_FLAGS "/Qspectre") +set(VCPKG_CXX_FLAGS "/Qspectre") +set(VCPKG_BUILD_TYPE release) diff --git a/src/VcpkgCustomTriplets/arm64-release.cmake b/src/VcpkgCustomTriplets/arm64-release.cmake @@ -0,0 +1,6 @@ +set(VCPKG_TARGET_ARCHITECTURE arm64) +set(VCPKG_CRT_LINKAGE dynamic) +set(VCPKG_LIBRARY_LINKAGE static) +set(VCPKG_C_FLAGS "/Qspectre") +set(VCPKG_CXX_FLAGS "/Qspectre") +set(VCPKG_BUILD_TYPE release) diff --git a/src/VcpkgCustomTriplets/arm64.cmake b/src/VcpkgCustomTriplets/arm64.cmake @@ -0,0 +1,3 @@ +set(VCPKG_TARGET_ARCHITECTURE arm64) +set(VCPKG_CRT_LINKAGE dynamic) +set(VCPKG_LIBRARY_LINKAGE static) diff --git a/src/VcpkgCustomTriplets/x64-release-static.cmake b/src/VcpkgCustomTriplets/x64-release-static.cmake @@ -0,0 +1,6 @@ +set(VCPKG_TARGET_ARCHITECTURE x64) +set(VCPKG_CRT_LINKAGE static) +set(VCPKG_LIBRARY_LINKAGE static) +set(VCPKG_C_FLAGS "/Qspectre") +set(VCPKG_CXX_FLAGS "/Qspectre") +set(VCPKG_BUILD_TYPE release) diff --git a/src/VcpkgCustomTriplets/x64-release.cmake b/src/VcpkgCustomTriplets/x64-release.cmake @@ -0,0 +1,6 @@ +set(VCPKG_TARGET_ARCHITECTURE x64) +set(VCPKG_CRT_LINKAGE dynamic) +set(VCPKG_LIBRARY_LINKAGE static) +set(VCPKG_C_FLAGS "/Qspectre") +set(VCPKG_CXX_FLAGS "/Qspectre") +set(VCPKG_BUILD_TYPE release) diff --git a/src/VcpkgCustomTriplets/x64.cmake b/src/VcpkgCustomTriplets/x64.cmake @@ -0,0 +1,3 @@ +set(VCPKG_TARGET_ARCHITECTURE x64) +set(VCPKG_CRT_LINKAGE dynamic) +set(VCPKG_LIBRARY_LINKAGE static) diff --git a/src/VcpkgCustomTriplets/x86-release-static.cmake b/src/VcpkgCustomTriplets/x86-release-static.cmake @@ -0,0 +1,6 @@ +set(VCPKG_TARGET_ARCHITECTURE x86) +set(VCPKG_CRT_LINKAGE static) +set(VCPKG_LIBRARY_LINKAGE static) +set(VCPKG_C_FLAGS "/Qspectre") +set(VCPKG_CXX_FLAGS "/Qspectre") +set(VCPKG_BUILD_TYPE release) diff --git a/src/VcpkgCustomTriplets/x86-release.cmake b/src/VcpkgCustomTriplets/x86-release.cmake @@ -0,0 +1,6 @@ +set(VCPKG_TARGET_ARCHITECTURE x86) +set(VCPKG_CRT_LINKAGE dynamic) +set(VCPKG_LIBRARY_LINKAGE static) +set(VCPKG_C_FLAGS "/Qspectre") +set(VCPKG_CXX_FLAGS "/Qspectre") +set(VCPKG_BUILD_TYPE release) diff --git a/src/VcpkgCustomTriplets/x86.cmake b/src/VcpkgCustomTriplets/x86.cmake @@ -0,0 +1,3 @@ +set(VCPKG_TARGET_ARCHITECTURE x86) +set(VCPKG_CRT_LINKAGE dynamic) +set(VCPKG_LIBRARY_LINKAGE static) diff --git a/src/WindowsPackageManager/WindowsPackageManager.vcxproj b/src/WindowsPackageManager/WindowsPackageManager.vcxproj @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="utf-8"?> <Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.props')" /> <PropertyGroup Label="Globals"> @@ -215,6 +215,60 @@ <RunCodeAnalysis>false</RunCodeAnalysis> <CodeAnalysisRuleSet>..\CodeAnalysis.ruleset</CodeAnalysisRuleSet> </PropertyGroup> + <PropertyGroup Label="Vcpkg"> + <VcpkgEnableManifest>true</VcpkgEnableManifest> + <VcpkgApplocalDeps>false</VcpkgApplocalDeps> + <VcpkgInstalledDir>$(ProjectDir)..\vcpkg_installed</VcpkgInstalledDir> + <VcpkgUseStatic></VcpkgUseStatic> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|x64'"> + <VcpkgTriplet>x64-release-static</VcpkgTriplet> + <VcpkgConfiguration>Release</VcpkgConfiguration> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> + <VcpkgTriplet>x64</VcpkgTriplet> + <VcpkgConfiguration>Debug</VcpkgConfiguration> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> + <VcpkgTriplet>x64-release</VcpkgTriplet> + <VcpkgConfiguration>Release</VcpkgConfiguration> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'"> + <VcpkgTriplet>arm</VcpkgTriplet> + <VcpkgConfiguration>Debug</VcpkgConfiguration> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'"> + <VcpkgTriplet>arm64</VcpkgTriplet> + <VcpkgConfiguration>Debug</VcpkgConfiguration> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> + <VcpkgTriplet>x86</VcpkgTriplet> + <VcpkgConfiguration>Debug</VcpkgConfiguration> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Release|ARM'"> + <VcpkgTriplet>arm-release</VcpkgTriplet> + <VcpkgConfiguration>Release</VcpkgConfiguration> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'"> + <VcpkgTriplet>arm64-release</VcpkgTriplet> + <VcpkgConfiguration>Release</VcpkgConfiguration> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> + <VcpkgTriplet>x86-release</VcpkgTriplet> + <VcpkgConfiguration>Release</VcpkgConfiguration> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|ARM'"> + <VcpkgTriplet>arm-release-static</VcpkgTriplet> + <VcpkgConfiguration>Release</VcpkgConfiguration> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|ARM64'"> + <VcpkgTriplet>arm64-release-static</VcpkgTriplet> + <VcpkgConfiguration>Release</VcpkgConfiguration> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='ReleaseStatic|Win32'"> + <VcpkgTriplet>x86-release-static</VcpkgTriplet> + <VcpkgConfiguration>Release</VcpkgConfiguration> + </PropertyGroup> <ItemDefinitionGroup> <ClCompile> <PrecompiledHeader>NotUsing</PrecompiledHeader> @@ -474,6 +528,9 @@ <ProjectReference Include="..\Microsoft.Management.Deployment\Microsoft.Management.Deployment.vcxproj"> <Project>{1cc41a9a-ae66-459d-9210-1e572dd7be69}</Project> </ProjectReference> + <ProjectReference Include="..\SfsClient\SfsClient.vcxproj"> + <Project>{1b9077b3-8923-4ecd-8fc9-b3190fcbe4d4}</Project> + </ProjectReference> <ProjectReference Include="..\YamlCppLib\YamlCppLib.vcxproj"> <Project>{8bb94bb8-374f-4294-bca1-c7811514a6b7}</Project> </ProjectReference> @@ -494,4 +551,4 @@ <Error Condition="!Exists('$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.props'))" /> <Error Condition="!Exists('$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.targets'))" /> </Target> -</Project>- \ No newline at end of file +</Project> diff --git a/src/vcpkg.json b/src/vcpkg.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", + "dependencies": [ + "correlation-vector-cpp", + { + "name": "curl", + "features": [ + "c-ares" + ] + }, + "nlohmann-json" + ], + "overrides": [ + { + "name": "c-ares", + "version": "1.19.1" + }, + { + "name": "correlation-vector-cpp", + "version": "1.0" + }, + { + "name": "curl", + "version": "8.4.0" + }, + { + "name": "nlohmann-json", + "version": "3.11.3" + } + ], + "vcpkg-configuration": { + "overlay-triplets": [ + "./VcpkgCustomTriplets" + ] + }, + "builtin-baseline": "f56238700757aa05975e41fa835739c632810f3f" +}