winget-cli

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

commit 69290f9695649fad58aab5c48b2132a0a1cd1072
parent b1538828de7ef3af1f3d4ba9659ed3f414df8d85
Author: JohnMcPMS <johnmcp@microsoft.com>
Date:   Mon, 11 Jan 2021 16:03:19 -0800

Force output paths to use u8string with filesystem::path (#694)

## Issue
When paths with non-ASCII characters are used, logging them is resulting in an exception due to not being able to convert them to ASCII (the default behavior when forced to convert to a narrow char).

## Change
To better handle all situations of output of a path, both the logging and reporting infrastructure have been enlightened to force the use of `u8String()` when they see one.
Diffstat:
Msrc/AppInstallerCLICore/ChannelStreams.cpp | 7+++++++
Msrc/AppInstallerCLICore/ChannelStreams.h | 1+
Msrc/AppInstallerCLIE2ETests/SourceCommand.cs | 16++++++++--------
Msrc/AppInstallerCLITests/Strings.cpp | 17+++++++++++++++++
Msrc/AppInstallerCommonCore/Public/AppInstallerLogging.h | 33++++++++++++++++++++++++++++++++-
Msrc/AppInstallerCommonCore/Public/winget/LocIndependent.h | 10+++++-----
6 files changed, 70 insertions(+), 14 deletions(-)

diff --git a/src/AppInstallerCLICore/ChannelStreams.cpp b/src/AppInstallerCLICore/ChannelStreams.cpp @@ -85,6 +85,13 @@ namespace AppInstaller::CLI::Execution return *this; } + OutputStream& OutputStream::operator<<(const std::filesystem::path& path) + { + ApplyFormat(); + m_out << path.u8string(); + return *this; + } + NoVTStream::NoVTStream(std::ostream& out, bool enabled) : m_out(out, enabled, false) {} diff --git a/src/AppInstallerCLICore/ChannelStreams.h b/src/AppInstallerCLICore/ChannelStreams.h @@ -100,6 +100,7 @@ namespace AppInstaller::CLI::Execution OutputStream& operator<<(std::ostream& (__cdecl* f)(std::ostream&)); OutputStream& operator<<(const VirtualTerminal::Sequence& sequence); OutputStream& operator<<(const VirtualTerminal::ConstructedSequence& sequence); + OutputStream& operator<<(const std::filesystem::path& path); private: // Applies the format for the stream. diff --git a/src/AppInstallerCLIE2ETests/SourceCommand.cs b/src/AppInstallerCLIE2ETests/SourceCommand.cs @@ -29,7 +29,7 @@ namespace AppInstallerCLIE2ETests public void SourceAddWithInvalidURL() { // Add source with invalid url should fail - var result = TestCommon.RunAICLICommand("source add", "AnotherSource https://microsoft.com"); + var result = TestCommon.RunAICLICommand("source add", $"AnotherSource {Constants.TestSourceUrl}/Invalid/Directory/Dont/Add/Me"); Assert.AreEqual(Constants.ErrorCode.HTTP_E_STATUS_NOT_FOUND, result.ExitCode); Assert.True(result.StdOut.Contains("An unexpected error occurred while executing the command")); } @@ -50,7 +50,7 @@ namespace AppInstallerCLIE2ETests // List with no args should list all available sources var result = TestCommon.RunAICLICommand("source list", ""); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains("https://localhost:5001/TestKit")); + Assert.True(result.StdOut.Contains(Constants.TestSourceUrl)); } [Test] @@ -58,8 +58,8 @@ namespace AppInstallerCLIE2ETests { var result = TestCommon.RunAICLICommand("source list", $"-n {Constants.TestSourceName}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains("TestSource")); - Assert.True(result.StdOut.Contains("https://localhost:5001/TestKit")); + Assert.True(result.StdOut.Contains(Constants.TestSourceName)); + Assert.True(result.StdOut.Contains(Constants.TestSourceUrl)); Assert.True(result.StdOut.Contains("Microsoft.PreIndexed.Package")); Assert.True(result.StdOut.Contains("Updated")); } @@ -110,8 +110,8 @@ namespace AppInstallerCLIE2ETests { var result = TestCommon.RunAICLICommand("source reset", ""); Assert.True(result.StdOut.Contains("The following sources will be reset if the --force option is given:")); - Assert.True(result.StdOut.Contains("TestSource")); - Assert.True(result.StdOut.Contains("https://localhost:5001/TestKit")); + Assert.True(result.StdOut.Contains(Constants.TestSourceName)); + Assert.True(result.StdOut.Contains(Constants.TestSourceUrl)); } [Test] @@ -126,8 +126,8 @@ namespace AppInstallerCLIE2ETests result = TestCommon.RunAICLICommand("source list", ""); Assert.True(result.StdOut.Contains("winget")); Assert.True(result.StdOut.Contains("https://winget.azureedge.net/cache")); - Assert.False(result.StdOut.Contains($"{Constants.TestSourceName}")); - Assert.False(result.StdOut.Contains($"{Constants.TestSourceUrl}")); + Assert.False(result.StdOut.Contains(Constants.TestSourceName)); + Assert.False(result.StdOut.Contains(Constants.TestSourceUrl)); ResetTestSource(); } } diff --git a/src/AppInstallerCLITests/Strings.cpp b/src/AppInstallerCLITests/Strings.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "TestCommon.h" #include <AppInstallerStrings.h> +#include <ExecutionReporter.h> using namespace std::string_view_literals; using namespace AppInstaller::Utility; @@ -147,3 +148,19 @@ TEST_CASE("ExpandEnvironmentVariables", "[strings]") REQUIRE(ExpandEnvironmentVariables(L"%TEMP%") == tempPath); } + +TEST_CASE("PathOutput", "[strings]") +{ + std::string original = "\xe6\xb5\x8b\xe8\xaf\x95"; + std::filesystem::path path = ConvertToUTF16(original); + AICLI_LOG(Test, Info, << path); + + std::istringstream in; + std::ostringstream out; + AppInstaller::CLI::Execution::Reporter reporter{ out, in }; + + reporter.Info() << path; + + std::string output = out.str(); + REQUIRE(output.substr(output.size() - original.size()) == original); +} diff --git a/src/AppInstallerCommonCore/Public/AppInstallerLogging.h b/src/AppInstallerCommonCore/Public/AppInstallerLogging.h @@ -17,7 +17,7 @@ auto& _aicli_log_log = AppInstaller::Logging::Log(); \ if (_aicli_log_log.IsEnabled(_aicli_log_channel, _aicli_log_level)) \ { \ - std::stringstream _aicli_log_strstr; \ + AppInstaller::Logging::LoggingStream _aicli_log_strstr; \ _aicli_log_strstr _outstream_; \ _aicli_log_log.Write(_aicli_log_channel, _aicli_log_level, _aicli_log_strstr.str()); \ } \ @@ -140,6 +140,37 @@ namespace AppInstaller::Logging // Calls the various stream format functions to produce an 8 character hexadecimal output. std::ostream& SetHRFormat(std::ostream& out); + + // This type allows us to override the default behavior of output operators for logging. + struct LoggingStream + { + // Force use of the UTF-8 string from a file path. + // This should not be necessary when we move to C++20 and convert to using u8string. + friend AppInstaller::Logging::LoggingStream& operator<<(AppInstaller::Logging::LoggingStream& out, std::filesystem::path& path) + { + out.m_out << path.u8string(); + return out; + } + + friend AppInstaller::Logging::LoggingStream& operator<<(AppInstaller::Logging::LoggingStream& out, const std::filesystem::path& path) + { + out.m_out << path.u8string(); + return out; + } + + // Everything else. + template <typename T> + friend AppInstaller::Logging::LoggingStream& operator<<(AppInstaller::Logging::LoggingStream& out, T&& t) + { + out.m_out << std::forward<T>(t); + return out; + } + + std::string str() const { return m_out.str(); } + + private: + std::stringstream m_out; + }; } // Enable output of system_clock time_points. diff --git a/src/AppInstallerCommonCore/Public/winget/LocIndependent.h b/src/AppInstallerCommonCore/Public/winget/LocIndependent.h @@ -53,12 +53,12 @@ namespace AppInstaller::Utility bool operator<(const LocIndString& other) const { return m_value < other.m_value; } + friend std::ostream& operator<<(std::ostream& out, const AppInstaller::Utility::LocIndString& lis) + { + return (out << lis.get()); + } + private: std::string m_value; }; } - -inline std::ostream& operator<<(std::ostream& out, const AppInstaller::Utility::LocIndString& lis) -{ - return (out << lis.get()); -}