commit ffa142f19e61172ebe9edbfe1a7eca7250b1cf1b parent ff65c79dab1bcbd7329e9507814894142e67c406 Author: JohnMcPMS <johnmcp@microsoft.com> Date: Wed, 19 Feb 2020 11:32:50 -0800 Create source management functionality (#38) Diffstat:
33 files changed, 986 insertions(+), 89 deletions(-)
diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -122,9 +122,9 @@ <ClCompile> <Optimization>Disabled</Optimization> <PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\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> @@ -139,7 +139,7 @@ <ItemDefinitionGroup Condition="'$(Platform)'=='Win32'"> <ClCompile> <PreprocessorDefinitions>WIN32;%(PreprocessorDefinitions);CLICOREDLLBUILD</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</TreatWarningAsError> </ClCompile> <Link> @@ -152,10 +152,10 @@ <FunctionLevelLinking>true</FunctionLevelLinking> <IntrinsicFunctions>true</IntrinsicFunctions> <PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> </ClCompile> <Link> <EnableCOMDATFolding>true</EnableCOMDATFolding> @@ -173,6 +173,7 @@ <ClInclude Include="Commands\DescribeCommand.h" /> <ClInclude Include="Commands\InstallCommand.h" /> <ClInclude Include="Commands\RootCommand.h" /> + <ClInclude Include="Commands\SourceCommand.h" /> <ClInclude Include="Invocation.h" /> <ClInclude Include="Localization.h" /> <ClInclude Include="pch.h" /> @@ -191,6 +192,7 @@ <ClCompile Include="Commands\DescribeCommand.cpp" /> <ClCompile Include="Commands\InstallCommand.cpp" /> <ClCompile Include="Commands\RootCommand.cpp" /> + <ClCompile Include="Commands\SourceCommand.cpp" /> <ClCompile Include="Core.cpp" /> <ClCompile Include="pch.cpp"> <PrecompiledHeader>Create</PrecompiledHeader> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -75,6 +75,9 @@ <ClInclude Include="Workflows\ShellExecuteInstallerHandler.h"> <Filter>Workflows</Filter> </ClInclude> + <ClInclude Include="Commands\SourceCommand.h"> + <Filter>Commands</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -113,6 +116,9 @@ <ClCompile Include="Workflows\ShellExecuteInstallerHandler.cpp"> <Filter>Workflows</Filter> </ClCompile> + <ClCompile Include="Commands\SourceCommand.cpp"> + <Filter>Commands</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLICore/Command.cpp b/src/AppInstallerCLICore/Command.cpp @@ -187,10 +187,8 @@ namespace AppInstaller::CLI } } - void Command::ExecuteInternal(Invocation&, std::ostream& out, std::istream& in) const + void Command::ExecuteInternal(Invocation&, std::ostream& out, std::istream&) const { - UNREFERENCED_PARAMETER(in); - out << LOCME("Oops, we forgot to do this...") << std::endl; THROW_HR(E_NOTIMPL); } diff --git a/src/AppInstallerCLICore/Commands/RootCommand.cpp b/src/AppInstallerCLICore/Commands/RootCommand.cpp @@ -6,6 +6,7 @@ #include "InstallCommand.h" #include "DescribeCommand.h" +#include "SourceCommand.h" namespace AppInstaller::CLI { @@ -14,6 +15,7 @@ namespace AppInstaller::CLI return InitializeFromMoveOnly<std::vector<std::unique_ptr<Command>>>({ std::make_unique<InstallCommand>(), std::make_unique<DescribeCommand>(), + std::make_unique<SourceCommand>(), }); } diff --git a/src/AppInstallerCLICore/Commands/SourceCommand.cpp b/src/AppInstallerCLICore/Commands/SourceCommand.cpp @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "SourceCommand.h" +#include "Localization.h" + + +namespace AppInstaller::CLI +{ + using namespace std::string_view_literals; + + constexpr std::string_view s_SourceCommand_ArgName_Name = "name"; + constexpr std::string_view s_SourceCommand_ArgName_Type = "type"; + constexpr std::string_view s_SourceCommand_ArgName_Arg = "arg"; + + std::vector<std::unique_ptr<Command>> SourceCommand::GetCommands() const + { + return InitializeFromMoveOnly<std::vector<std::unique_ptr<Command>>>({ + std::make_unique<SourceAddCommand>(), + std::make_unique<SourceListCommand>(), + }); + } + + std::string SourceCommand::ShortDescription() const + { + return LOCME("Manage sources of applications"); + } + + std::vector<std::string> SourceCommand::GetLongDescription() const + { + return { + LOCME("Manage sources of applications"), + }; + } + + void SourceCommand::ExecuteInternal(Invocation&, std::ostream& out, std::istream&) const + { + OutputHelp(out); + } + + std::vector<Argument> SourceAddCommand::GetArguments() const + { + return { + Argument{ s_SourceCommand_ArgName_Name, LOCME("Name of the source for future reference"), ArgumentType::Positional, true }, + Argument{ s_SourceCommand_ArgName_Type, LOCME("Type of the source"), ArgumentType::Positional, true }, + Argument{ s_SourceCommand_ArgName_Arg, LOCME("Argument given to the source"), ArgumentType::Positional, true }, + }; + } + + std::string SourceAddCommand::ShortDescription() const + { + return LOCME("Add a new source"); + } + + std::vector<std::string> SourceAddCommand::GetLongDescription() const + { + return { + LOCME("Add a new source"), + }; + } + + void SourceAddCommand::ExecuteInternal(Invocation& inv, std::ostream& out, std::istream&) const + { + std::string name = *inv.GetArg(s_SourceCommand_ArgName_Name); + std::string type = *inv.GetArg(s_SourceCommand_ArgName_Type); + std::string arg = *inv.GetArg(s_SourceCommand_ArgName_Arg); + + out << LOCME("Adding source:") << std::endl; + out << " " << name << std::endl; + out << " " << type << std::endl; + out << " " << arg << std::endl; + + // TODO: Needs to be hooked up to a reporter when real source construction happens. + Repository::AddSource(std::move(name), std::move(type), std::move(arg)); + + out << LOCME("Done") << std::endl; + } + + std::vector<Argument> SourceListCommand::GetArguments() const + { + return { + Argument{ s_SourceCommand_ArgName_Name, LOCME("Name of the source to list full details for"), ArgumentType::Positional, false }, + }; + } + + std::string SourceListCommand::ShortDescription() const + { + return LOCME("List current sources"); + } + + std::vector<std::string> SourceListCommand::GetLongDescription() const + { + return { + LOCME("List current sources"), + }; + } + + void SourceListCommand::ExecuteInternal(Invocation& inv, std::ostream& out, std::istream&) const + { + std::vector<Repository::SourceDetails> sources = Repository::GetSources(); + + if (inv.Contains(s_SourceCommand_ArgName_Name)) + { + const std::string& name = *inv.GetArg(s_SourceCommand_ArgName_Name); + auto itr = std::find_if(sources.begin(), sources.end(), [name](const Repository::SourceDetails& sd) { return Utility::CaseInsensitiveEquals(sd.Name, name); }); + + if (itr == sources.end()) + { + out << LOCME("No source with the given name was found: ") << name << std::endl; + } + else + { + out << LOCME("Name") << ": " << itr->Name << std::endl; + out << LOCME("Type") << ": " << itr->Type << std::endl; + out << LOCME("Arg") << ": " << itr->Arg << std::endl; + out << LOCME("Data") << ": " << itr->Data << std::endl; + if (itr->LastUpdateTime == Utility::ConvertUnixEpochToSystemClock(0)) + { + out << LOCME("Last Update") << ": <never>" << std::endl; + } + else + { + out << LOCME("Last Update") << ": " << itr->LastUpdateTime << std::endl; + } + } + } + else + { + out << LOCME("Current sources:") << std::endl; + + if (sources.empty()) + { + out << LOCME(" <none>") << std::endl; + } + else + { + for (const auto& source : sources) + { + out << " " << source.Name << " => " << source.Arg << std::endl; + } + } + } + } +} diff --git a/src/AppInstallerCLICore/Commands/SourceCommand.h b/src/AppInstallerCLICore/Commands/SourceCommand.h @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Command.h" + +namespace AppInstaller::CLI +{ + struct SourceCommand final : public Command + { + SourceCommand() : Command("source") {} + + virtual std::vector<std::unique_ptr<Command>> GetCommands() const override; + + virtual std::string ShortDescription() const override; + virtual std::vector<std::string> GetLongDescription() const override; + + protected: + virtual void ExecuteInternal(Invocation& inv, std::ostream& out, std::istream& in) const; + }; + + struct SourceAddCommand final : public Command + { + SourceAddCommand() : Command("add") {} + + virtual std::vector<Argument> GetArguments() const override; + + virtual std::string ShortDescription() const override; + virtual std::vector<std::string> GetLongDescription() const override; + + protected: + virtual void ExecuteInternal(Invocation& inv, std::ostream& out, std::istream& in) const; + }; + + struct SourceListCommand final : public Command + { + SourceListCommand() : Command("list") {} + + virtual std::vector<Argument> GetArguments() const override; + + virtual std::string ShortDescription() const override; + virtual std::vector<std::string> GetLongDescription() const override; + + protected: + virtual void ExecuteInternal(Invocation& inv, std::ostream& out, std::istream& in) const; + }; +} diff --git a/src/AppInstallerCLICore/pch.h b/src/AppInstallerCLICore/pch.h @@ -24,13 +24,15 @@ #include <wrl/client.h> #include <AppxPackaging.h> -#include "AppInstallerLogging.h" -#include "AppInstallerTelemetry.h" -#include "AppInstallerStrings.h" -#include "AppInstallerRuntime.h" -#include "AppInstallerSHA256.h" -#include "AppInstallerDownloader.h" -#include "AppInstallerErrors.h" -#include "Manifest/ManifestInstaller.h" -#include "Manifest/Manifest.h" -#include "AppInstallerMsixInfo.h" +#include <AppInstallerDateTime.h> +#include <AppInstallerDownloader.h> +#include <AppInstallerErrors.h> +#include <AppInstallerLogging.h> +#include <AppInstallerMsixInfo.h> +#include <AppInstallerRepositorySource.h> +#include <AppInstallerRuntime.h> +#include <AppInstallerSHA256.h> +#include <AppInstallerStrings.h> +#include <AppInstallerTelemetry.h> +#include <Manifest/ManifestInstaller.h> +#include <Manifest/Manifest.h> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -162,6 +162,8 @@ <ClCompile Include="pch.cpp"> <PrecompiledHeader>Create</PrecompiledHeader> </ClCompile> + <ClCompile Include="Settings.cpp" /> + <ClCompile Include="Sources.cpp" /> <ClCompile Include="SQLiteIndex.cpp" /> <ClCompile Include="SQLiteWrapper.cpp" /> <ClCompile Include="TestCommon.cpp" /> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -53,13 +53,16 @@ <ClCompile Include="LanguageUtilities.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="Settings.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="Sources.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> <None Include="packages.config" /> - <None Include="TestData\InstallerArgTest_Msi_WithSwitches.yml"> - <Filter>TestData</Filter> - </None> </ItemGroup> <ItemGroup> <CopyFileToFolders Include="TestData\BadManifest-MissingName.yml"> @@ -89,5 +92,6 @@ <CopyFileToFolders Include="TestData\InstallerArgTest_Inno_WithSwitches.yml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallerArgTest_Msi_WithSwitches.yml" /> </ItemGroup> </Project> \ No newline at end of file diff --git a/src/AppInstallerCLITests/Settings.cpp b/src/AppInstallerCLITests/Settings.cpp @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" + +#include <AppInstallerRuntime.h> +#include <AppInstallerStrings.h> + +using namespace AppInstaller::Runtime; +using namespace AppInstaller::Utility; + +TEST_CASE("ReadEmptySetting", "[settings]") +{ + std::string name = "nonexistentsetting"; + + auto result = GetSettingStream(name); + REQUIRE(!result); +} + +TEST_CASE("SetAndReadSetting", "[settings]") +{ + std::string name = "testsettingname"; + std::string value = "This is the test setting value"; + + SetSetting(name, value); + + auto result = GetSettingStream(name); + REQUIRE(static_cast<bool>(result)); + + std::string settingValue = ReadEntireStream(*result); + REQUIRE(value == settingValue); +} + +TEST_CASE("SetAndReadSettingInContainer", "[settings]") +{ + std::string name = "testcontainer/testsettingname"; + std::string value = "This is the test setting value from inside a container"; + + SetSetting(name, value); + + auto result = GetSettingStream(name); + REQUIRE(static_cast<bool>(result)); + + std::string settingValue = ReadEntireStream(*result); + REQUIRE(value == settingValue); +} + +TEST_CASE("RemoveSetting", "[settings]") +{ + std::string name = "testsettingname"; + std::string value = "This is the test setting value to be removed"; + + SetSetting(name, value); + + { + auto result = GetSettingStream(name); + REQUIRE(static_cast<bool>(result)); + + std::string settingValue = ReadEntireStream(*result); + REQUIRE(value == settingValue); + } + + RemoveSetting(name); + + auto result = GetSettingStream(name); + REQUIRE(!static_cast<bool>(result)); +} diff --git a/src/AppInstallerCLITests/Sources.cpp b/src/AppInstallerCLITests/Sources.cpp @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" + +#include <Public/AppInstallerRepositorySource.h> +#include <AppInstallerDateTime.h> +#include <AppInstallerRuntime.h> +#include <AppInstallerStrings.h> + +using namespace AppInstaller::Runtime; +using namespace AppInstaller::Repository; +using namespace AppInstaller::Utility; + +// Duplicating here because a change to these values in the product *REALLY* needs to be thought through. +using namespace std::string_literals; +using namespace std::string_view_literals; +constexpr std::string_view s_RepositorySettings_UserSources = "usersources"sv; + +constexpr std::string_view s_SourcesYaml_Sources = "Sources"sv; +constexpr std::string_view s_SourcesYaml_Source_Name = "Name"sv; +constexpr std::string_view s_SourcesYaml_Source_Type = "Type"sv; +constexpr std::string_view s_SourcesYaml_Source_Arg = "Arg"sv; +constexpr std::string_view s_SourcesYaml_Source_Data = "Data"sv; +constexpr std::string_view s_SourcesYaml_Source_LastUpdate = "LastUpdate"sv; + +constexpr std::string_view s_EmptySources = R"( +Sources: +)"sv; + +constexpr std::string_view s_SingleSource = R"( +Sources: + - Name: testName + Type: testType + Arg: testArg + Data: testData + LastUpdate: 0 +)"sv; + +constexpr std::string_view s_ThreeSources = R"( +Sources: + - Name: testName + Type: testType + Arg: testArg + Data: testData + LastUpdate: 0 + - Name: testName2 + Type: testType2 + Arg: testArg2 + Data: testData2 + LastUpdate: 1 + - Name: testName3 + Type: testType3 + Arg: testArg3 + Data: testData3 + LastUpdate: 2 +)"sv; + +constexpr std::string_view s_SingleSource_MissingArg = R"( +Sources: + - Name: testName + Type: testType + Data: testData + LastUpdate: 0 +)"sv; + +TEST_CASE("RepoSources_UserSettingDoesNotExist", "[sources]") +{ + RemoveSetting(s_RepositorySettings_UserSources); + + std::vector<SourceDetails> sources = GetSources(); + REQUIRE(sources.empty()); +} + +TEST_CASE("RepoSources_EmptySourcesList", "[sources]") +{ + SetSetting(s_RepositorySettings_UserSources, s_EmptySources); + + std::vector<SourceDetails> sources = GetSources(); + REQUIRE(sources.empty()); +} + +TEST_CASE("RepoSources_SingleSource", "[sources]") +{ + SetSetting(s_RepositorySettings_UserSources, s_SingleSource); + + std::vector<SourceDetails> sources = GetSources(); + REQUIRE(sources.size() == 1); + + REQUIRE(sources[0].Name == "testName"); + REQUIRE(sources[0].Type == "testType"); + REQUIRE(sources[0].Arg == "testArg"); + REQUIRE(sources[0].Data == "testData"); + REQUIRE(sources[0].LastUpdateTime == ConvertUnixEpochToSystemClock(0)); +} + +TEST_CASE("RepoSources_ThreeSources", "[sources]") +{ + SetSetting(s_RepositorySettings_UserSources, s_ThreeSources); + + std::vector<SourceDetails> sources = GetSources(); + REQUIRE(sources.size() == 3); + + const char* suffix[3] = { "", "2", "3" }; + + for (size_t i = 0; i < 3; ++i) + { + INFO("Source #" << i); + REQUIRE(sources[i].Name == "testName"s + suffix[i]); + REQUIRE(sources[i].Type == "testType"s + suffix[i]); + REQUIRE(sources[i].Arg == "testArg"s + suffix[i]); + REQUIRE(sources[i].Data == "testData"s + suffix[i]); + REQUIRE(sources[i].LastUpdateTime == ConvertUnixEpochToSystemClock(i)); + } +} + +TEST_CASE("RepoSources_InvalidYAML", "[sources]") +{ + SetSetting(s_RepositorySettings_UserSources, "Name: Value : BAD"); + + REQUIRE_THROWS_HR(GetSources(), APPINSTALLER_CLI_ERROR_SOURCES_INVALID); +} + +TEST_CASE("RepoSources_MissingField", "[sources]") +{ + SetSetting(s_RepositorySettings_UserSources, s_SingleSource_MissingArg); + + REQUIRE_THROWS_HR(GetSources(), APPINSTALLER_CLI_ERROR_SOURCES_INVALID); +} + +TEST_CASE("RepoSources_AddSource", "[sources]") +{ + RemoveSetting(s_RepositorySettings_UserSources); + + std::string name = "thisIsTheName"; + std::string type = "thisIsTheType"; + std::string arg = "thisIsTheArg"; + + auto source = AddSource(name, type, arg); + + std::vector<SourceDetails> sources = GetSources(); + REQUIRE(sources.size() == 1); + + REQUIRE(sources[0].Name == name); + REQUIRE(sources[0].Type == type); + REQUIRE(sources[0].Arg == arg); + REQUIRE(sources[0].Data == ""); + REQUIRE(sources[0].LastUpdateTime == ConvertUnixEpochToSystemClock(0)); +} + +TEST_CASE("RepoSources_AddMultipleSources", "[sources]") +{ + RemoveSetting(s_RepositorySettings_UserSources); + + std::string name = "thisIsTheName"; + std::string type = "thisIsTheType"; + std::string arg = "thisIsTheArg"; + + auto source = AddSource(name, type, arg); + + std::vector<SourceDetails> sources = GetSources(); + REQUIRE(sources.size() == 1); + + REQUIRE(sources[0].Name == name); + REQUIRE(sources[0].Type == type); + REQUIRE(sources[0].Arg == arg); + REQUIRE(sources[0].Data == ""); + REQUIRE(sources[0].LastUpdateTime == ConvertUnixEpochToSystemClock(0)); + + const char* suffix[2] = { "", "2" }; + + source = AddSource(name + suffix[1], type + suffix[1], arg + suffix[1]); + + sources = GetSources(); + REQUIRE(sources.size() == 2); + + for (size_t i = 0; i < 2; ++i) + { + INFO("Source #" << i); + REQUIRE(sources[i].Name == name + suffix[i]); + REQUIRE(sources[i].Type == type + suffix[i]); + REQUIRE(sources[i].Arg == arg + suffix[i]); + REQUIRE(sources[i].Data == ""); + REQUIRE(sources[i].LastUpdateTime == ConvertUnixEpochToSystemClock(0)); + } +} diff --git a/src/AppInstallerCLITests/main.cpp b/src/AppInstallerCLITests/main.cpp @@ -15,6 +15,12 @@ using namespace winrt; using namespace Windows::Foundation; using namespace std::string_literals; +namespace AppInstaller::Runtime +{ + void TestHook_ForceContainerPrepend(const std::filesystem::path& prepend); +} + + // Logs the the AppInstaller log target to break up individual tests struct LoggingBreakListener : public Catch::TestEventListenerBase { @@ -102,5 +108,9 @@ int main(int argc, char** argv) AppInstaller::Logging::Log().SetLevel(AppInstaller::Logging::Level::Verbose); AppInstaller::Logging::EnableWilFailureTelemetry(); + // Force all tests to run against settings inside this container. + // This prevents test runs from trashing the users actual settings. + AppInstaller::Runtime::TestHook_ForceContainerPrepend("AutoTestContainer"); + return Catch::Session().run(static_cast<int>(args.size()), args.data()); } diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -168,11 +168,11 @@ </Link> </ItemDefinitionGroup> <ItemGroup> - <ClInclude Include="DateTime.h" /> <ClInclude Include="HttpStream\HttpClientWrapper.h" /> <ClInclude Include="HttpStream\HttpLocalCache.h" /> <ClInclude Include="HttpStream\HttpRandomAccessStream.h" /> <ClInclude Include="pch.h" /> + <ClInclude Include="Public\AppInstallerDateTime.h" /> <ClInclude Include="Public\AppInstallerDownloader.h" /> <ClInclude Include="Public\AppInstallerErrors.h" /> <ClInclude Include="Public\AppInstallerFileLogger.h" /> diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -45,9 +45,6 @@ <ClInclude Include="Public\AppInstallerStrings.h"> <Filter>Public</Filter> </ClInclude> - <ClInclude Include="DateTime.h"> - <Filter>Header Files</Filter> - </ClInclude> <ClInclude Include="Public\AppInstallerRuntime.h"> <Filter>Public</Filter> </ClInclude> @@ -81,6 +78,9 @@ <ClInclude Include="Public\AppInstallerFileLogger.h"> <Filter>Public</Filter> </ClInclude> + <ClInclude Include="Public\AppInstallerDateTime.h"> + <Filter>Public</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> diff --git a/src/AppInstallerCommonCore/AppInstallerLogging.cpp b/src/AppInstallerCommonCore/AppInstallerLogging.cpp @@ -3,9 +3,9 @@ #include "pch.h" #include "Public/AppInstallerLogging.h" -#include "Public/AppInstallerTelemetry.h" #include "Public/AppInstallerFileLogger.h" -#include "DateTime.h" +#include "Public/AppInstallerTelemetry.h" +#include "Public/AppInstallerDateTime.h" namespace AppInstaller::Logging { diff --git a/src/AppInstallerCommonCore/AppInstallerStrings.cpp b/src/AppInstallerCommonCore/AppInstallerStrings.cpp @@ -5,6 +5,12 @@ namespace AppInstaller::Utility { + bool CaseInsensitiveEquals(const std::string& a, const std::string& b) + { + // TODO: When we bring in ICU, do this correctly. + return ToLower(a) == ToLower(b); + } + std::string ConvertToUTF8(std::wstring_view input) { int utf8ByteCount = WideCharToMultiByte(CP_UTF8, 0, input.data(), wil::safe_cast<int>(input.length()), nullptr, 0, nullptr, nullptr); @@ -73,4 +79,18 @@ namespace AppInstaller::Utility pos += value.length(); } } + + std::string ReadEntireStream(std::istream& stream) + { + std::streampos currentPos = stream.tellg(); + stream.seekg(0, std::ios_base::end); + + auto offset = stream.tellg() - currentPos; + stream.seekg(currentPos); + + std::string result(offset, '\0'); + stream.read(&result[0], offset); + + return result; + } } diff --git a/src/AppInstallerCommonCore/Architecture.cpp b/src/AppInstallerCommonCore/Architecture.cpp @@ -35,6 +35,34 @@ namespace AppInstaller::Utility return Architecture::Unknown; } + Architecture GetSystemArchitecture() + { + Architecture systemArchitecture = Architecture::Unknown; + + SYSTEM_INFO systemInfo; + ZeroMemory(&systemInfo, sizeof(SYSTEM_INFO)); + GetNativeSystemInfo(&systemInfo); + + switch (systemInfo.wProcessorArchitecture) + { + case PROCESSOR_ARCHITECTURE_AMD64: + case PROCESSOR_ARCHITECTURE_IA64: + systemArchitecture = Architecture::X64; + break; + case PROCESSOR_ARCHITECTURE_ARM: + systemArchitecture = Architecture::Arm; + break; + case PROCESSOR_ARCHITECTURE_ARM64: + systemArchitecture = Architecture::Arm64; + break; + case PROCESSOR_ARCHITECTURE_INTEL: + systemArchitecture = Architecture::X86; + break; + } + + return systemArchitecture; + } + std::vector<Architecture> GetApplicableArchitectures() { static std::vector<Architecture> applicableArchs; @@ -44,7 +72,7 @@ namespace AppInstaller::Utility return applicableArchs; } - switch (Runtime::GetSystemArchitecture()) + switch (GetSystemArchitecture()) { case Architecture::Arm64: applicableArchs.push_back(Architecture::Arm64); diff --git a/src/AppInstallerCommonCore/DateTime.cpp b/src/AppInstallerCommonCore/DateTime.cpp @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "pch.h" -#include "DateTime.h" +#include "Public/AppInstallerDateTime.h" namespace AppInstaller::Utility { @@ -26,4 +26,23 @@ namespace AppInstaller::Utility stream << std::setw(3) << std::setfill('0') << leftoverMillis.count(); } + + int64_t GetCurrentUnixEpoch() + { + static_assert(std::is_same_v<int64_t, decltype(time(nullptr))>, "time returns a 64-bit integer"); + time_t now = time(nullptr); + return static_cast<int64_t>(now); + } + + int64_t ConvertSystemClockToUnixEpoch(const std::chrono::system_clock::time_point& time) + { + static_assert(std::is_same_v<int64_t, decltype(std::chrono::system_clock::to_time_t(time))>, "to_time_t returns a 64-bit integer"); + time_t timeAsTimeT = std::chrono::system_clock::to_time_t(time); + return static_cast<int64_t>(timeAsTimeT); + } + + std::chrono::system_clock::time_point ConvertUnixEpochToSystemClock(int64_t epoch) + { + return std::chrono::system_clock::from_time_t(static_cast<time_t>(epoch)); + } } diff --git a/src/AppInstallerCommonCore/DateTime.h b/src/AppInstallerCommonCore/DateTime.h @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#pragma once - -#include <chrono> -#include <ostream> - -namespace AppInstaller::Utility -{ - // Writes the given time to the given stream. - // Assumes that system_clock uses Linux epoch (as required by C++20 standard). - // Time is also assumed to be after the epoch. - void OutputTimepoint(std::ostream& stream, const std::chrono::system_clock::time_point& time); -} diff --git a/src/AppInstallerCommonCore/FileLogger.cpp b/src/AppInstallerCommonCore/FileLogger.cpp @@ -4,7 +4,7 @@ #include "Public/AppInstallerFileLogger.h" #include "Public/AppInstallerRuntime.h" -#include "DateTime.h" +#include "Public/AppInstallerDateTime.h" #define AICLI_FILELOGGER_DEFAULT_FILE "AICLI.log" diff --git a/src/AppInstallerCommonCore/Public/AppInstallerArchitecture.h b/src/AppInstallerCommonCore/Public/AppInstallerArchitecture.h @@ -20,6 +20,9 @@ namespace AppInstaller::Utility // Converts a string to corresponding enum Architecture ConvertToArchitectureEnum(const std::string& archStr); + // Gets the system's architecture as Architecture enum + AppInstaller::Utility::Architecture GetSystemArchitecture(); + // Gets a set of architectures that are applicable to the current system std::vector<Architecture> GetApplicableArchitectures(); @@ -27,4 +30,4 @@ namespace AppInstaller::Utility // Returns the priority in the applicable architecture list if the architecture is applicable. 0 has lowest priority. // Returns -1 if the architecture is not applicable. int IsApplicableArchitecture(Architecture arch); -}- \ No newline at end of file +} diff --git a/src/AppInstallerCommonCore/Public/AppInstallerDateTime.h b/src/AppInstallerCommonCore/Public/AppInstallerDateTime.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once + +#include <chrono> +#include <ostream> + +namespace AppInstaller::Utility +{ + // Writes the given time to the given stream. + // Assumes that system_clock uses Linux epoch (as required by C++20 standard). + // Time is also assumed to be after the epoch. + void OutputTimepoint(std::ostream& stream, const std::chrono::system_clock::time_point& time); + + // Gets the current time as a unix epoch value. + int64_t GetCurrentUnixEpoch(); + + // Converts the given unix epoch time to a system_clock::time_point. + int64_t ConvertSystemClockToUnixEpoch(const std::chrono::system_clock::time_point& time); + + // Converts the given unix epoch time to a system_clock::time_point. + std::chrono::system_clock::time_point ConvertUnixEpochToSystemClock(int64_t epoch); +} diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -15,3 +15,5 @@ #define APPINSTALLER_CLI_ERROR_DOWNLOAD_FAILED ((HRESULT)0x8A150008) #define APPINSTALLER_CLI_ERROR_CANNOT_WRITE_TO_UPLEVEL_INDEX ((HRESULT)0x8A150009) #define APPINSTALLER_CLI_ERROR_INDEX_INTEGRITY_COMPROMISED ((HRESULT)0x8A15000A) +#define APPINSTALLER_CLI_ERROR_SOURCES_INVALID ((HRESULT)0x8A15000B) +#define APPINSTALLER_CLI_ERROR_SOURCE_NAME_ALREADY_EXISTS ((HRESULT)0x8A15000C) diff --git a/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h b/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once -#include "AppInstallerArchitecture.h" - #include <filesystem> +#include <memory> #include <string> +#include <string_view> namespace AppInstaller::Runtime { @@ -17,6 +17,13 @@ namespace AppInstaller::Runtime // Gets the path to the temp location. std::filesystem::path GetPathToTemp(); - // Gets the system's architecture as Architecture enum - AppInstaller::Utility::Architecture GetSystemArchitecture(); + // Gets a stream containing the named setting's value, if present. + // If the setting does not exist, returns an empty value. + std::unique_ptr<std::istream> GetSettingStream(std::filesystem::path name); + + // Sets the named setting to the given value. + void SetSetting(std::filesystem::path name, std::string_view value); + + // Deletes the given setting. + void RemoveSetting(std::filesystem::path name); } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerStrings.h b/src/AppInstallerCommonCore/Public/AppInstallerStrings.h @@ -7,6 +7,9 @@ namespace AppInstaller::Utility { + // Compares the two UTF8 strings in a case insensitive manner. + bool CaseInsensitiveEquals(const std::string& a, const std::string& b); + // Converts the given UTF16 string to UTF8 std::string ConvertToUTF8(std::wstring_view input); @@ -24,4 +27,7 @@ namespace AppInstaller::Utility // Find token in the input string and replace with value. void FindAndReplace(std::string& inputStr, const std::string& token, const std::string& value); + + // Reads the entire stream into a string. + std::string ReadEntireStream(std::istream& stream); } diff --git a/src/AppInstallerCommonCore/Runtime.cpp b/src/AppInstallerCommonCore/Runtime.cpp @@ -1,22 +1,106 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "pch.h" -#include "Public/AppInstallerArchitecture.h" #include "Public/AppInstallerRuntime.h" -#include <winrt/Windows.Storage.h> - -using namespace AppInstaller::Utility; +#include "Public/AppInstallerStrings.h" namespace AppInstaller::Runtime { namespace { + // Gets a boolean indicating whether the current process has identity. bool DoesCurrentProcessHaveIdentity() { UINT32 length = 0; LONG result = GetPackageFamilyName(GetCurrentProcess(), &length, nullptr); return (result != APPMODEL_ERROR_NO_PACKAGE); } + + static std::filesystem::path s_Settings_TestHook_ForcedContainerPrepend; + + void ValidateSettingNamePath(std::filesystem::path& name) + { + THROW_HR_IF(E_INVALIDARG, !name.has_relative_path()); + THROW_HR_IF(E_INVALIDARG, name.has_root_path()); + THROW_HR_IF(E_INVALIDARG, !name.has_filename()); + + if (!s_Settings_TestHook_ForcedContainerPrepend.empty()) + { + std::filesystem::path result = s_Settings_TestHook_ForcedContainerPrepend; + result /= name; + name = std::move(result); + } + } + + // Gets the container within LocalSettings for the given path. + auto GetLocalSettingsContainerForPath(const std::filesystem::path& name) + { + auto result = winrt::Windows::Storage::ApplicationData::Current().LocalSettings(); + + for (const auto& part : name.parent_path()) + { + auto partHstring = winrt::to_hstring(part.c_str()); + result = result.CreateContainer(partHstring, winrt::Windows::Storage::ApplicationDataCreateDisposition::Always); + } + + return result; + } + + // Gets the path to the appdata root. + // *Only used by non packaged version!* + std::filesystem::path GetPathToAppDataRoot() + { + THROW_HR_IF(E_NOT_VALID_STATE, IsRunningInPackagedContext()); + + DWORD charCount = ExpandEnvironmentStringsW(L"%LOCALAPPDATA%", nullptr, 0); + THROW_LAST_ERROR_IF(charCount == 0); + + std::wstring localAppDataPath(charCount + 1, L'\0'); + charCount = ExpandEnvironmentStringsW(L"%LOCALAPPDATA%", &localAppDataPath[0], charCount + 1); + THROW_LAST_ERROR_IF(charCount == 0); + + localAppDataPath.resize(charCount - 1); + + std::filesystem::path result = localAppDataPath; + result /= "Microsoft/AppInstaller"; + return result; + } + + // Gets the path to the settings root. + // Creates the directory if it does not already exist. + std::filesystem::path GetPathToSettingsRoot() + { + std::filesystem::path result = GetPathToAppDataRoot(); + result /= "Settings"; + + if (std::filesystem::exists(result)) + { + if (!std::filesystem::is_directory(result)) + { + // STATUS_NOT_A_DIRECTORY: A requested opened file is not a directory. + THROW_NTSTATUS_MSG(0xC0000103, "Settings is not a directory"); + } + } + else + { + std::filesystem::create_directories(result); + } + + return result; + } + + // Gets the path to the settings directory for the given setting. + // Creates the directory if it does not already exist. + std::filesystem::path GetPathToSettings(const std::filesystem::path& name) + { + std::filesystem::path result = GetPathToAppDataRoot(); + if (name.has_parent_path()) + { + result /= name.parent_path(); + std::filesystem::create_directories(result); + } + return result; + } } bool IsRunningInPackagedContext() @@ -71,31 +155,79 @@ namespace AppInstaller::Runtime } } - Architecture GetSystemArchitecture() + std::unique_ptr<std::istream> GetSettingStream(std::filesystem::path name) + { + ValidateSettingNamePath(name); + + if (IsRunningInPackagedContext()) + { + auto container = GetLocalSettingsContainerForPath(name); + auto filenameHstring = winrt::to_hstring(name.filename().c_str()); + auto settingsValues = container.Values(); + if (settingsValues.HasKey(filenameHstring)) + { + auto value = winrt::unbox_value<winrt::hstring>(settingsValues.Lookup(filenameHstring)); + return std::make_unique<std::istringstream>(Utility::ConvertToUTF8(value.c_str())); + } + else + { + return {}; + } + } + else + { + auto settingFileName = GetPathToSettings(name); + settingFileName /= name.filename(); + + if (std::filesystem::exists(settingFileName)) + { + return std::make_unique<std::ifstream>(settingFileName); + } + else + { + return {}; + } + } + } + + void SetSetting(std::filesystem::path name, std::string_view value) + { + ValidateSettingNamePath(name); + + if (IsRunningInPackagedContext()) + { + GetLocalSettingsContainerForPath(name).Values(). + Insert(winrt::to_hstring(name.filename().c_str()), winrt::box_value(winrt::to_hstring(value))); + } + else + { + auto settingFileName = GetPathToSettings(name); + settingFileName /= name.filename(); + + std::ofstream stream(settingFileName, std::ios_base::out | std::ios_base::binary | std::ios_base::trunc); + stream << value << std::flush; + } + } + + void RemoveSetting(std::filesystem::path name) + { + ValidateSettingNamePath(name); + + if (IsRunningInPackagedContext()) + { + GetLocalSettingsContainerForPath(name).Values().Remove(winrt::to_hstring(name.filename().c_str())); + } + else + { + auto settingFileName = GetPathToSettings(name); + settingFileName /= name.filename(); + + std::filesystem::remove(settingFileName); + } + } + + void TestHook_ForceContainerPrepend(const std::filesystem::path& prepend) { - Architecture systemArchitecture = Architecture::Unknown; - - SYSTEM_INFO systemInfo; - ZeroMemory(&systemInfo, sizeof(SYSTEM_INFO)); - GetNativeSystemInfo(&systemInfo); - - switch (systemInfo.wProcessorArchitecture) - { - case PROCESSOR_ARCHITECTURE_AMD64: - case PROCESSOR_ARCHITECTURE_IA64: - systemArchitecture = Architecture::X64; - break; - case PROCESSOR_ARCHITECTURE_ARM: - systemArchitecture = Architecture::Arm; - break; - case PROCESSOR_ARCHITECTURE_ARM64: - systemArchitecture = Architecture::Arm64; - break; - case PROCESSOR_ARCHITECTURE_INTEL: - systemArchitecture = Architecture::X86; - break; - } - - return systemArchitecture; + s_Settings_TestHook_ForcedContainerPrepend = prepend; } } diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -208,6 +208,7 @@ <ClCompile Include="pch.cpp"> <PrecompiledHeader>Create</PrecompiledHeader> </ClCompile> + <ClCompile Include="RepositorySource.cpp" /> <ClCompile Include="SQLiteStatementBuilder.cpp" /> <ClCompile Include="SQLiteWrapper.cpp" /> </ItemGroup> diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -146,6 +146,9 @@ <ClCompile Include="SQLiteStatementBuilder.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="RepositorySource.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp @@ -201,14 +201,12 @@ namespace AppInstaller::Repository::Microsoft // should be consistent across systems. void SQLiteIndex::SetLastWriteTime() { - static_assert(std::is_same_v<int64_t, decltype(time(nullptr))>, "time returns a 64-bit integer"); - time_t now = time(nullptr); - Schema::MetadataTable::SetNamedValue(m_dbconn, Schema::s_MetadataValueName_LastWriteTime, static_cast<int64_t>(now)); + Schema::MetadataTable::SetNamedValue(m_dbconn, Schema::s_MetadataValueName_LastWriteTime, Utility::GetCurrentUnixEpoch()); } std::chrono::system_clock::time_point SQLiteIndex::GetLastWriteTime() { int64_t lastWriteTime = Schema::MetadataTable::GetNamedValue<int64_t>(m_dbconn, Schema::s_MetadataValueName_LastWriteTime); - return std::chrono::system_clock::from_time_t(static_cast<time_t>(lastWriteTime)); + return Utility::ConvertUnixEpochToSystemClock(lastWriteTime); } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/PathPartTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/PathPartTable.cpp @@ -125,6 +125,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 { THROW_HR_IF(E_INVALIDARG, !relativePath.has_relative_path()); THROW_HR_IF(E_INVALIDARG, relativePath.has_root_path()); + THROW_HR_IF(E_INVALIDARG, !relativePath.has_filename()); std::unique_ptr<SQLite::Savepoint> savepoint; if (createIfNotFound) diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h @@ -6,6 +6,7 @@ #include <chrono> #include <memory> #include <string> +#include <string_view> #include <vector> @@ -44,15 +45,15 @@ namespace AppInstaller::Repository }; // Adds a new source for the user. - std::unique_ptr<ISource> AddSource(const std::string& name, const std::string& type, const std::string& arg); + std::unique_ptr<ISource> AddSource(std::string name, std::string type, std::string arg); // Opens an existing source. // Passing an empty string as the name of the source will return a source that aggregates all others. - std::unique_ptr<ISource> OpenSource(const std::string& name); + std::unique_ptr<ISource> OpenSource(std::string_view name); // Gets the details for all sources. std::vector<SourceDetails> GetSources(); // Removes an existing source. - void RemoveSource(const std::string& name); + void RemoveSource(std::string_view name); } diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -0,0 +1,197 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Public/AppInstallerRepositorySource.h" + + +namespace AppInstaller::Repository +{ + using namespace std::string_view_literals; + constexpr std::string_view s_RepositorySettings_UserSources = "usersources"sv; + + constexpr std::string_view s_SourcesYaml_Sources = "Sources"sv; + constexpr std::string_view s_SourcesYaml_Source_Name = "Name"sv; + constexpr std::string_view s_SourcesYaml_Source_Type = "Type"sv; + constexpr std::string_view s_SourcesYaml_Source_Arg = "Arg"sv; + constexpr std::string_view s_SourcesYaml_Source_Data = "Data"sv; + constexpr std::string_view s_SourcesYaml_Source_LastUpdate = "LastUpdate"sv; + + namespace + { + // Attempts to read a single scalar value from the node. + template<typename Value> + bool TryReadScalar(std::string_view settingName, const std::string& settingValue, const YAML::Node& sourceNode, std::string_view name, Value& value) + { + YAML::Node valueNode = sourceNode[std::string{ name }]; + + if (!valueNode || !valueNode.IsScalar()) + { + AICLI_LOG(Repo, Error, << "Setting '" << settingName << "' did not contain the expected format (" << name << " is invalid within a source):\n" << settingValue); + return false; + } + + value = valueNode.as<Value>(); + return true; + } + + // Attempts to read the source details from the given stream. + // Results are all or nothing; if any failures occur, no details are returned. + bool TryReadSourceDetails(std::string_view settingName, std::istream& stream, std::vector<SourceDetails>& sourceDetails) + { + sourceDetails.clear(); + + std::vector<SourceDetails> result; + std::string settingValue = Utility::ReadEntireStream(stream); + + YAML::Node document; + try + { + document = YAML::Load(settingValue); + } + catch (const std::runtime_error& e) + { + AICLI_LOG(YAML, Error, << "Setting '" << settingName << "' contained invalid YAML (" << e.what() << "):\n" << settingValue); + return false; + } + + try + { + YAML::Node sources = document[std::string{ s_SourcesYaml_Sources }]; + if (!sources) + { + AICLI_LOG(Repo, Error, << "Setting '" << settingName << "' did not contain the expected format (missing " << s_SourcesYaml_Sources << "):\n" << settingValue); + return false; + } + + if (sources.IsNull()) + { + // An empty sources is an acceptable thing. + return true; + } + + if (!sources.IsSequence()) + { + AICLI_LOG(Repo, Error, << "Setting '" << settingName << "' did not contain the expected format (" << s_SourcesYaml_Sources << " was not a sequence):\n" << settingValue); + return false; + } + + for (const auto& source : sources) + { + SourceDetails details; + if (!TryReadScalar(settingName, settingValue, source, s_SourcesYaml_Source_Name, details.Name)) { return false; } + if (!TryReadScalar(settingName, settingValue, source, s_SourcesYaml_Source_Type, details.Type)) { return false; } + if (!TryReadScalar(settingName, settingValue, source, s_SourcesYaml_Source_Arg, details.Arg)) { return false; } + if (!TryReadScalar(settingName, settingValue, source, s_SourcesYaml_Source_Data, details.Data)) { return false; } + int64_t lastUpdateInEpoch{}; + if (!TryReadScalar(settingName, settingValue, source, s_SourcesYaml_Source_LastUpdate, lastUpdateInEpoch)) { return false; } + details.LastUpdateTime = Utility::ConvertUnixEpochToSystemClock(lastUpdateInEpoch); + + result.emplace_back(std::move(details)); + } + } + catch (const std::runtime_error& e) + { + AICLI_LOG(YAML, Error, << "Setting '" << settingName << "' contained unexpected YAML (" << e.what() << "):\n" << settingValue); + return false; + } + + sourceDetails = std::move(result); + return true; + } + + // Gets the source details from a particular setting. + std::vector<SourceDetails> GetSourcesFromSetting(std::string_view settingName) + { + auto sourcesStream = Runtime::GetSettingStream(settingName); + if (!sourcesStream) + { + // TODO: Handle first run scenario and configure default source(s). + // Note that this case is different than the one in which all sources have been removed. + return {}; + } + else + { + std::vector<SourceDetails> result; + THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCES_INVALID, !TryReadSourceDetails(settingName, *sourcesStream, result)); + return result; + } + } + + // Make up for the lack of string_view support in YAML CPP. + YAML::Emitter& operator<<(YAML::Emitter& out, std::string_view sv) + { + return (out << std::string(sv)); + } + + // Sets the sources for a particular setting. + void SetSourcesToSetting(std::string_view settingName, const std::vector<SourceDetails>& sources) + { + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << s_SourcesYaml_Sources; + out << YAML::BeginSeq; + + for (const SourceDetails& details : sources) + { + out << YAML::BeginMap; + out << YAML::Key << s_SourcesYaml_Source_Name << YAML::Value << details.Name; + out << YAML::Key << s_SourcesYaml_Source_Type << YAML::Value << details.Type; + out << YAML::Key << s_SourcesYaml_Source_Arg << YAML::Value << details.Arg; + out << YAML::Key << s_SourcesYaml_Source_Data << YAML::Value << details.Data; + out << YAML::Key << s_SourcesYaml_Source_LastUpdate << YAML::Value << Utility::ConvertSystemClockToUnixEpoch(details.LastUpdateTime); + out << YAML::EndMap; + } + + out << YAML::EndSeq; + out << YAML::EndMap; + + Runtime::SetSetting(settingName, out.c_str()); + } + } + + std::unique_ptr<ISource> AddSource(std::string name, std::string type, std::string arg) + { + THROW_HR_IF(E_INVALIDARG, name.empty()); + THROW_HR_IF(E_INVALIDARG, type.empty()); + + // Check all sources for the given name. + std::vector<SourceDetails> currentSources = GetSources(); + + auto itr = std::find_if(currentSources.begin(), currentSources.end(), [&name](const SourceDetails& sd) { return Utility::CaseInsensitiveEquals(sd.Name, name); }); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_NAME_ALREADY_EXISTS, itr != currentSources.end()); + + SourceDetails details; + details.Name = std::move(name); + details.Type = std::move(type); + details.Arg = std::move(arg); + details.LastUpdateTime = Utility::ConvertUnixEpochToSystemClock(0); + + // TODO: Implement actual source creation, for now we just add to the user setting. + // Ex. + //std::unique_ptr<ISource> result = CreateSourceFromDetails(details); + + currentSources = GetSourcesFromSetting(s_RepositorySettings_UserSources); + // NOTE: When implementing creation as above, insert the details that we then get out of the result. + currentSources.push_back(details); + + SetSourcesToSetting(s_RepositorySettings_UserSources, currentSources); + + return {}; + } + + std::unique_ptr<ISource> OpenSource(std::string_view name) + { + UNREFERENCED_PARAMETER(name); + return {}; + } + + std::vector<SourceDetails> GetSources() + { + return GetSourcesFromSetting(s_RepositorySettings_UserSources); + } + + void RemoveSource(std::string_view name) + { + UNREFERENCED_PARAMETER(name); + } +} diff --git a/src/AppInstallerRepositoryCore/pch.h b/src/AppInstallerRepositoryCore/pch.h @@ -5,8 +5,10 @@ #define NOMINMAX #include <windows.h> +#include <AppInstallerDateTime.h> #include <AppInstallerErrors.h> #include <AppInstallerLogging.h> +#include <AppInstallerRuntime.h> #include <AppInstallerSHA256.h> #include <AppInstallerStrings.h> #include <yaml-cpp/yaml.h>