Support environment variables in the settings (#15082)

Existing environment variables can be referenced by enclosing the name
in percent characters (e.g. `%PATH%`).

Resurrects #9287 by @christapley.

Tests added and manually tested.

Closes #2785
Closes #9233

Co-authored-by: Chris Tapley <chris.tapley.81@gmail.com>
This commit is contained in:
Ian O'Neill 2023-04-12 00:01:11 +01:00 committed by GitHub
parent 508adbb1ec
commit 56d451ded7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 322 additions and 262 deletions

View File

@ -2574,6 +2574,13 @@
"default": false,
"description": "When true, this profile should always open in an elevated context. If the window isn't running as an Administrator, then a new elevated window will be created."
},
"environment": {
"description": "Key-value pairs representing environment variables to set. Environment variable names are not case sensitive. You can reference existing environment variable names by enclosing them in literal percent characters (e.g. %PATH%).",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"experimental.autoMarkPrompts": {
"default": false,
"description": "When set to true, prompts will automatically be marked.",

View File

@ -38,6 +38,9 @@ namespace SettingsModelLocalTests
TEST_METHOD(LayerProfileProperties);
TEST_METHOD(LayerProfileIcon);
TEST_METHOD(LayerProfilesOnArray);
TEST_METHOD(ProfileWithEnvVars);
TEST_METHOD(ProfileWithEnvVarsSameNameDifferentCases);
TEST_METHOD(DuplicateProfileTest);
TEST_METHOD(TestGenGuidsForProfiles);
TEST_METHOD(TestCorrectOldDefaultShellPaths);
@ -349,6 +352,50 @@ namespace SettingsModelLocalTests
VERIFY_ARE_NOT_EQUAL(settings->AllProfiles().GetAt(0).Guid(), settings->AllProfiles().GetAt(1).Guid());
}
void ProfileTests::ProfileWithEnvVars()
{
const std::string profileString{ R"({
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"environment": {
"VAR_1": "value1",
"VAR_2": "value2",
"VAR_3": "%VAR_3%;value3"
}
})" };
const auto profile = implementation::Profile::FromJson(VerifyParseSucceeded(profileString));
std::vector<IEnvironmentVariableMap> envVarMaps{};
envVarMaps.emplace_back(profile->EnvironmentVariables());
for (auto& envMap : envVarMaps)
{
VERIFY_ARE_EQUAL(static_cast<uint32_t>(3), envMap.Size());
VERIFY_ARE_EQUAL(L"value1", envMap.Lookup(L"VAR_1"));
VERIFY_ARE_EQUAL(L"value2", envMap.Lookup(L"VAR_2"));
VERIFY_ARE_EQUAL(L"%VAR_3%;value3", envMap.Lookup(L"VAR_3"));
}
}
void ProfileTests::ProfileWithEnvVarsSameNameDifferentCases()
{
const std::string userSettings{ R"({
"profiles": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"environment": {
"FOO": "VALUE",
"Foo": "Value"
}
}
]
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(userSettings);
const auto warnings = settings->Warnings();
VERIFY_ARE_EQUAL(static_cast<uint32_t>(2), warnings.Size());
uint32_t index;
VERIFY_IS_TRUE(warnings.IndexOf(SettingsLoadWarnings::InvalidProfileEnvironmentVariables, index));
}
void ProfileTests::TestCorrectOldDefaultShellPaths()
{
static constexpr std::string_view inboxProfiles{ R"({

View File

@ -165,7 +165,13 @@ namespace SettingsModelLocalTests
"historySize": 9001,
"closeOnExit": "graceful",
"experimental.retroTerminalEffect": false
"experimental.retroTerminalEffect": false,
"environment":
{
"KEY_1": "VALUE_1",
"KEY_2": "%KEY_1%",
"KEY_3": "%PATH%"
}
})" };
static constexpr std::string_view smallProfileString{ R"(

View File

@ -282,6 +282,9 @@
<value>Failed to parse "startupActions".</value>
<comment>{Locked="\"startupActions\""}</comment>
</data>
<data name="InvalidProfileEnvironmentVariables" xml:space="preserve">
<value>Found multiple environment variables with the same name in different cases (lower/upper) - only one value will be used.</value>
</data>
<data name="CmdCommandArgDesc" xml:space="preserve">
<value>An optional command, with arguments, to be spawned in the new tab or pane</value>
</data>

View File

@ -1216,7 +1216,8 @@ namespace winrt::TerminalApp::implementation
nullptr,
settings.InitialRows(),
settings.InitialCols(),
winrt::guid());
winrt::guid(),
profile.Guid());
if constexpr (Feature_VtPassthroughMode::IsEnabled())
{
@ -1228,12 +1229,9 @@ namespace winrt::TerminalApp::implementation
else
{
// profile is guaranteed to exist here
auto guidWString = Utils::GuidToString(profile.Guid());
StringMap envMap{};
envMap.Insert(L"WT_PROFILE_ID", guidWString);
envMap.Insert(L"WSLENV", L"WT_PROFILE_ID");
const auto environment = settings.EnvironmentVariables() != nullptr ?
settings.EnvironmentVariables().GetView() :
nullptr;
// Update the path to be relative to whatever our CWD is.
//
@ -1264,10 +1262,11 @@ namespace winrt::TerminalApp::implementation
auto valueSet = TerminalConnection::ConptyConnection::CreateSettings(settings.Commandline(),
newWorkingDirectory,
settings.StartingTitle(),
envMap.GetView(),
environment,
settings.InitialRows(),
settings.InitialCols(),
winrt::guid());
winrt::guid(),
profile.Guid());
valueSet.Insert(L"passthroughMode", Windows::Foundation::PropertyValue::CreateBoolean(settings.VtPassthrough()));
valueSet.Insert(L"reloadEnvironmentVariables",

View File

@ -50,6 +50,7 @@ static const std::array settingsLoadWarningsLabels{
USES_RESOURCE(L"InvalidColorSchemeInCmd"),
USES_RESOURCE(L"InvalidSplitSize"),
USES_RESOURCE(L"FailedToParseStartupActions"),
USES_RESOURCE(L"InvalidProfileEnvironmentVariables"),
USES_RESOURCE(L"FailedToParseSubCommands"),
USES_RESOURCE(L"UnknownTheme"),
USES_RESOURCE(L"DuplicateRemainingProfilesEntry"),

View File

@ -5,12 +5,12 @@
#include "ConptyConnection.h"
#include <conpty-static.h>
#include <til/string.h>
#include <til/env.h>
#include <winternl.h>
#include "CTerminalHandoff.h"
#include "LibraryResources.h"
#include "../../types/inc/Environment.hpp"
#include "../../types/inc/utils.hpp"
#include "ConptyConnection.g.cpp"
@ -84,31 +84,19 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
auto cmdline{ wil::ExpandEnvironmentStringsW<std::wstring>(_commandline.c_str()) }; // mutable copy -- required for CreateProcessW
Utils::EnvironmentVariableMapW environment;
til::env environment;
auto zeroEnvMap = wil::scope_exit([&]() noexcept {
// Can't zero the keys, but at least we can zero the values.
for (auto& [name, value] : environment)
{
::SecureZeroMemory(value.data(), value.size() * sizeof(decltype(value.begin())::value_type));
}
environment.clear();
});
// Populate the environment map with the current environment.
if (_reloadEnvironmentVariables)
{
til::env refreshedEnvironment;
refreshedEnvironment.regenerate();
for (auto& [key, value] : refreshedEnvironment.as_map())
{
environment.try_emplace(key, std::move(value));
}
environment.regenerate();
}
else
{
RETURN_IF_FAILED(Utils::UpdateEnvironmentMapW(environment));
environment = til::env::from_current_environment();
}
{
@ -119,39 +107,45 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
const auto guidSubStr = std::wstring_view{ wsGuid }.substr(1);
// Ensure every connection has the unique identifier in the environment.
environment.insert_or_assign(L"WT_SESSION", guidSubStr.data());
environment.as_map().insert_or_assign(L"WT_SESSION", guidSubStr.data());
// The profile Guid does include the enclosing '{}'
const auto profileGuid{ Utils::GuidToString(_profileGuid) };
environment.as_map().insert_or_assign(L"WT_PROFILE_ID", profileGuid.data());
// WSLENV is a colon-delimited list of environment variables (+flags) that should appear inside WSL
// https://devblogs.microsoft.com/commandline/share-environment-vars-between-wsl-and-windows/
std::wstring wslEnv{ L"WT_SESSION:WT_PROFILE_ID:" };
if (_environment)
{
// add additional WT env vars like WT_SETTINGS, WT_DEFAULTS and WT_PROFILE_ID
for (auto item : _environment)
// Order the environment variable names so that resolution order is consistent
std::set<std::wstring, til::wstring_case_insensitive_compare> keys{};
for (const auto item : _environment)
{
keys.insert(item.Key().c_str());
}
// add additional env vars
for (const auto& key : keys)
{
try
{
auto key = item.Key();
// This will throw if the value isn't a string. If that
// happens, then just skip this entry.
auto value = winrt::unbox_value<hstring>(item.Value());
const auto value = winrt::unbox_value<hstring>(_environment.Lookup(key));
// avoid clobbering WSLENV
if (std::wstring_view{ key } == L"WSLENV")
{
auto current = environment[L"WSLENV"];
value = current + L":" + value;
}
environment.insert_or_assign(key.c_str(), value.c_str());
environment.set_user_environment_var(key.c_str(), value.c_str());
// For each environment variable added to the environment, also add it to WSLENV
wslEnv += key + L":";
}
CATCH_LOG();
}
}
// WSLENV is a colon-delimited list of environment variables (+flags) that should appear inside WSL
// https://devblogs.microsoft.com/commandline/share-environment-vars-between-wsl-and-windows/
auto wslEnv = environment[L"WSLENV"];
wslEnv = L"WT_SESSION:" + wslEnv; // prepend WT_SESSION to make sure it's visible inside WSL.
environment.insert_or_assign(L"WSLENV", wslEnv);
// We want to prepend new environment variables to WSLENV - that way if a variable already
// exists in WSLENV but with a flag, the flag will be respected.
// (This behaviour was empirically observed)
wslEnv += environment.as_map()[L"WSLENV"];
environment.as_map().insert_or_assign(L"WSLENV", wslEnv);
}
std::vector<wchar_t> newEnvVars;
@ -160,7 +154,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
newEnvVars.size() * sizeof(decltype(newEnvVars.begin())::value_type));
});
RETURN_IF_FAILED(Utils::EnvironmentMapToEnvironmentStringsW(environment, newEnvVars));
RETURN_IF_FAILED(environment.to_environment_strings_w(newEnvVars));
auto lpEnvironment = newEnvVars.empty() ? nullptr : newEnvVars.data();
@ -244,7 +238,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
const Windows::Foundation::Collections::IMapView<hstring, hstring>& environment,
uint32_t rows,
uint32_t columns,
const winrt::guid& guid)
const winrt::guid& guid,
const winrt::guid& profileGuid)
{
Windows::Foundation::Collections::ValueSet vs{};
@ -254,6 +249,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
vs.Insert(L"initialRows", Windows::Foundation::PropertyValue::CreateUInt32(rows));
vs.Insert(L"initialCols", Windows::Foundation::PropertyValue::CreateUInt32(columns));
vs.Insert(L"guid", Windows::Foundation::PropertyValue::CreateGuid(guid));
vs.Insert(L"profileGuid", Windows::Foundation::PropertyValue::CreateGuid(profileGuid));
if (environment)
{
@ -288,6 +284,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
}
_reloadEnvironmentVariables = winrt::unbox_value_or<bool>(settings.TryLookup(L"reloadEnvironmentVariables").try_as<Windows::Foundation::IPropertyValue>(),
_reloadEnvironmentVariables);
_profileGuid = winrt::unbox_value_or<winrt::guid>(settings.TryLookup(L"profileGuid").try_as<Windows::Foundation::IPropertyValue>(), _profileGuid);
}
if (_guid == guid{})

View File

@ -52,7 +52,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
const Windows::Foundation::Collections::IMapView<hstring, hstring>& environment,
uint32_t rows,
uint32_t columns,
const winrt::guid& guid);
const winrt::guid& guid,
const winrt::guid& profileGuid);
WINRT_CALLBACK(TerminalOutput, TerminalOutputHandler);
@ -90,6 +91,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
std::array<char, 4096> _buffer{};
bool _passthroughMode{};
bool _reloadEnvironmentVariables{};
guid _profileGuid{};
struct StartupInfoFromDefTerm
{

View File

@ -31,6 +31,7 @@ namespace Microsoft.Terminal.TerminalConnection
IMapView<String, String> environment,
UInt32 rows,
UInt32 columns,
Guid guid);
Guid guid,
Guid profileGuid);
};
}

View File

@ -52,7 +52,6 @@ namespace Microsoft.Terminal.Control
String Commandline { get; };
String StartingDirectory { get; };
String EnvironmentVariables { get; };
TextAntialiasingMode AntialiasingMode { get; };

View File

@ -15,6 +15,7 @@
#include <shellapi.h>
#include <shlwapi.h>
#include <til/latch.h>
#include <til/string.h>
using namespace winrt::Microsoft::Terminal;
using namespace winrt::Microsoft::Terminal::Settings;
@ -417,6 +418,7 @@ void CascadiaSettings::_validateSettings()
_validateKeybindings();
_validateColorSchemesInCommands();
_validateThemeExists();
_validateProfileEnvironmentVariables();
}
// Method Description:
@ -541,6 +543,30 @@ void CascadiaSettings::_validateMediaResources()
}
}
// Method Description:
// - Checks if the profiles contain multiple environment variables with the same name, but different
// cases
void CascadiaSettings::_validateProfileEnvironmentVariables()
{
for (const auto& profile : _allProfiles)
{
std::set<std::wstring, til::wstring_case_insensitive_compare> envVarNames{};
if (profile.EnvironmentVariables() == nullptr)
{
continue;
}
for (const auto [key, value] : profile.EnvironmentVariables())
{
const auto iterator = envVarNames.insert(key.c_str());
if (!iterator.second)
{
_warnings.Append(SettingsLoadWarnings::InvalidProfileEnvironmentVariables);
return;
}
}
}
}
// Method Description:
// - Helper to get the GUID of a profile, given an optional index and a possible
// "profile" value to override that.

View File

@ -162,6 +162,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void _validateSettings();
void _validateAllSchemesExist();
void _validateMediaResources();
void _validateProfileEnvironmentVariables();
void _validateKeybindings() const;
void _validateColorSchemesInCommands() const;
bool _hasInvalidColorScheme(const Model::Command& command) const;

View File

@ -82,6 +82,7 @@ Author(s):
X(CloseOnExitMode, CloseOnExit, "closeOnExit", CloseOnExitMode::Automatic) \
X(hstring, TabTitle, "tabTitle") \
X(Model::BellStyle, BellStyle, "bellStyle", BellStyle::Audible) \
X(IEnvironmentVariableMap, EnvironmentVariables, "environment", nullptr) \
X(bool, UseAtlasEngine, "useAtlasEngine", Feature_AtlasEngine::IsEnabled()) \
X(bool, RightClickContextMenu, "experimental.rightClickContextMenu", false) \
X(Windows::Foundation::Collections::IVector<winrt::hstring>, BellSound, "bellSound", nullptr) \

View File

@ -67,6 +67,8 @@ namespace TerminalAppUnitTests
class JsonTests;
};
using IEnvironmentVariableMap = winrt::Windows::Foundation::Collections::IMap<winrt::hstring, winrt::hstring>;
// GUID used for generating GUIDs at runtime, for profiles that did not have a
// GUID specified manually.
constexpr GUID RUNTIME_GENERATED_PROFILE_NAMESPACE_GUID = { 0xf65ddb7e, 0x706b, 0x4499, { 0x8a, 0x50, 0x40, 0x31, 0x3c, 0xaf, 0x51, 0x0a } };

View File

@ -9,6 +9,8 @@ import "FontConfig.idl";
_BASE_INHERITABLE_SETTING(Type, Name); \
Microsoft.Terminal.Settings.Model.Profile Name##OverrideSource { get; }
#define COMMA ,
namespace Microsoft.Terminal.Settings.Model
{
// This tag is used to identify the context in which the Profile was created
@ -81,6 +83,9 @@ namespace Microsoft.Terminal.Settings.Model
INHERITABLE_PROFILE_SETTING(Boolean, SnapOnInput);
INHERITABLE_PROFILE_SETTING(Boolean, AltGrAliasing);
INHERITABLE_PROFILE_SETTING(BellStyle, BellStyle);
INHERITABLE_PROFILE_SETTING(Windows.Foundation.Collections.IMap<String COMMA String>, EnvironmentVariables);
INHERITABLE_PROFILE_SETTING(Boolean, UseAtlasEngine);
INHERITABLE_PROFILE_SETTING(Windows.Foundation.Collections.IVector<String>, BellSound);

View File

@ -306,6 +306,20 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
_TabColor = static_cast<winrt::Microsoft::Terminal::Core::Color>(colorRef);
}
const auto profileEnvVars = profile.EnvironmentVariables();
if (profileEnvVars == nullptr)
{
_EnvironmentVariables = std::nullopt;
}
else
{
_EnvironmentVariables = winrt::single_threaded_map<winrt::hstring, winrt::hstring>();
for (const auto& [key, value] : profileEnvVars)
{
_EnvironmentVariables.value().Insert(key, value);
}
}
_Elevate = profile.Elevate();
_AutoMarkPrompts = Feature_ScrollbarMarks::IsEnabled() && profile.AutoMarkPrompts();
_ShowMarks = Feature_ScrollbarMarks::IsEnabled() && profile.ShowMarks();

View File

@ -22,6 +22,7 @@ Author(s):
using IFontAxesMap = winrt::Windows::Foundation::Collections::IMap<winrt::hstring, float>;
using IFontFeatureMap = winrt::Windows::Foundation::Collections::IMap<winrt::hstring, uint32_t>;
using IEnvironmentVariableMap = winrt::Windows::Foundation::Collections::IMap<winrt::hstring, winrt::hstring>;
// fwdecl unittest classes
namespace SettingsModelLocalTests
@ -143,7 +144,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
INHERITABLE_SETTING(Model::TerminalSettings, hstring, StartingDirectory);
INHERITABLE_SETTING(Model::TerminalSettings, hstring, StartingTitle);
INHERITABLE_SETTING(Model::TerminalSettings, bool, SuppressApplicationTitle);
INHERITABLE_SETTING(Model::TerminalSettings, hstring, EnvironmentVariables);
INHERITABLE_SETTING(Model::TerminalSettings, IEnvironmentVariableMap, EnvironmentVariables);
INHERITABLE_SETTING(Model::TerminalSettings, Microsoft::Terminal::Control::ScrollbarState, ScrollState, Microsoft::Terminal::Control::ScrollbarState::Visible);
INHERITABLE_SETTING(Model::TerminalSettings, bool, UseAtlasEngine, false);

View File

@ -3,6 +3,8 @@
import "CascadiaSettings.idl";
#define COMMA ,
namespace Microsoft.Terminal.Settings.Model
{
runtimeclass TerminalSettingsCreateResult
@ -26,6 +28,8 @@ namespace Microsoft.Terminal.Settings.Model
{
TerminalSettings();
Windows.Foundation.Collections.IMap<String COMMA String> EnvironmentVariables;
static TerminalSettings CreateForPreview(CascadiaSettings appSettings, Profile profile);
static TerminalSettingsCreateResult CreateWithProfile(CascadiaSettings appSettings, Profile profile, Microsoft.Terminal.Control.IKeyBindings keybindings);
static TerminalSettingsCreateResult CreateWithNewTerminalArgs(CascadiaSettings appSettings, NewTerminalArgs newTerminalArgs, Microsoft.Terminal.Control.IKeyBindings keybindings);
@ -39,7 +43,6 @@ namespace Microsoft.Terminal.Settings.Model
// able to change these at runtime (e.g. when duplicating a pane).
String Commandline { set; };
String StartingDirectory { set; };
String EnvironmentVariables { set; };
Boolean Elevate;
};

View File

@ -20,6 +20,7 @@ namespace Microsoft.Terminal.Settings.Model
InvalidColorSchemeInCmd,
InvalidSplitSize,
FailedToParseStartupActions,
InvalidProfileEnvironmentVariables,
FailedToParseSubCommands,
UnknownTheme,
DuplicateRemainingProfilesEntry,

View File

@ -66,7 +66,6 @@
X(winrt::Microsoft::Terminal::Control::IKeyBindings, KeyBindings, nullptr) \
X(winrt::hstring, Commandline) \
X(winrt::hstring, StartingDirectory) \
X(winrt::hstring, EnvironmentVariables) \
X(winrt::Microsoft::Terminal::Control::ScrollbarState, ScrollState, winrt::Microsoft::Terminal::Control::ScrollbarState::Visible) \
X(winrt::Microsoft::Terminal::Control::TextAntialiasingMode, AntialiasingMode, winrt::Microsoft::Terminal::Control::TextAntialiasingMode::Grayscale) \
X(bool, ForceFullRepaintRendering, false) \

View File

@ -5,6 +5,7 @@
#include <wil/token_helpers.h>
#include <winternl.h>
#include <til/string.h>
#pragma warning(push)
#pragma warning(disable : 26429) // Symbol '...' is never tested for nullness, it can be marked as not_null (f.23).
@ -21,37 +22,6 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
namespace details
{
//
// A case-insensitive wide-character map is used to store environment variables
// due to documented requirements:
//
// "All strings in the environment block must be sorted alphabetically by name.
// The sort is case-insensitive, Unicode order, without regard to locale.
// Because the equal sign is a separator, it must not be used in the name of
// an environment variable."
// https://docs.microsoft.com/en-us/windows/desktop/ProcThread/changing-environment-variables
//
// - Returns CSTR_LESS_THAN, CSTR_EQUAL or CSTR_GREATER_THAN
[[nodiscard]] inline int compare_string_ordinal(const std::wstring_view& lhs, const std::wstring_view& rhs) noexcept
{
const auto result = CompareStringOrdinal(
lhs.data(),
::base::saturated_cast<int>(lhs.size()),
rhs.data(),
::base::saturated_cast<int>(rhs.size()),
TRUE);
FAIL_FAST_LAST_ERROR_IF(!result);
return result;
}
struct wstring_case_insensitive_compare
{
[[nodiscard]] bool operator()(const std::wstring& lhs, const std::wstring& rhs) const noexcept
{
return compare_string_ordinal(lhs, rhs) == CSTR_LESS_THAN;
}
};
namespace vars
{
inline constexpr wil::zwstring_view system_root{ L"SystemRoot" };
@ -250,7 +220,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
friend class ::EnvTests;
#endif
std::map<std::wstring, std::wstring, til::details::wstring_case_insensitive_compare> _envMap{};
std::map<std::wstring, std::wstring, til::wstring_case_insensitive_compare> _envMap{};
// We make copies of the environment variable names to ensure they are null terminated.
void get(wil::zwstring_view variable)
@ -438,13 +408,6 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
return expanded;
}
void set_user_environment_var(std::wstring_view var, std::wstring_view value)
{
auto valueString = expand_environment_strings(value);
valueString = check_for_temp(var, valueString);
save_to_map(std::wstring{ var }, std::move(valueString));
}
void concat_var(std::wstring var, std::wstring value)
{
if (const auto existing = _envMap.find(var); existing != _envMap.end())
@ -475,8 +438,8 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
{
static constexpr std::wstring_view temp{ L"temp" };
static constexpr std::wstring_view tmp{ L"tmp" };
if (til::details::compare_string_ordinal(var, temp) == CSTR_EQUAL ||
til::details::compare_string_ordinal(var, tmp) == CSTR_EQUAL)
if (til::compare_string_ordinal(var, temp) == CSTR_EQUAL ||
til::compare_string_ordinal(var, tmp) == CSTR_EQUAL)
{
return til::details::wil_env::GetShortPathNameW<std::wstring, 256>(value.data());
}
@ -491,9 +454,9 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
static constexpr std::wstring_view path{ L"Path" };
static constexpr std::wstring_view libPath{ L"LibPath" };
static constexpr std::wstring_view os2LibPath{ L"Os2LibPath" };
return til::details::compare_string_ordinal(input, path) == CSTR_EQUAL ||
til::details::compare_string_ordinal(input, libPath) == CSTR_EQUAL ||
til::details::compare_string_ordinal(input, os2LibPath) == CSTR_EQUAL;
return til::compare_string_ordinal(input, path) == CSTR_EQUAL ||
til::compare_string_ordinal(input, libPath) == CSTR_EQUAL ||
til::compare_string_ordinal(input, os2LibPath) == CSTR_EQUAL;
}
static void strip_trailing_null(std::wstring& str) noexcept
@ -533,6 +496,35 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
parse(block);
}
// Function Description:
// - Creates a new environment with the current process's unicode environment
// variables.
// Return Value:
// - A new environment
static til::env from_current_environment()
{
LPWCH currentEnvVars{};
auto freeCurrentEnv = wil::scope_exit([&] {
if (currentEnvVars)
{
FreeEnvironmentStringsW(currentEnvVars);
currentEnvVars = nullptr;
}
});
currentEnvVars = ::GetEnvironmentStringsW();
THROW_HR_IF_NULL(E_OUTOFMEMORY, currentEnvVars);
return til::env{ currentEnvVars };
}
void set_user_environment_var(std::wstring_view var, std::wstring_view value)
{
auto valueString = expand_environment_strings(value);
valueString = check_for_temp(var, valueString);
save_to_map(std::wstring{ var }, std::move(valueString));
}
void regenerate()
{
// Generally replicates the behavior of shell32!RegenerateUserEnvironment
@ -572,6 +564,93 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
return result;
}
void clear() noexcept
{
// Can't zero the keys, but at least we can zero the values.
for (auto& [name, value] : _envMap)
{
::SecureZeroMemory(value.data(), value.size() * sizeof(decltype(value.begin())::value_type));
}
_envMap.clear();
}
// Function Description:
// - Creates a new environment block using the provided vector as appropriate
// (resizing if needed) based on the current environment variable map
// matching the format of GetEnvironmentStringsW.
// Arguments:
// - newEnvVars: The vector that will be used to create the new environment block.
// Return Value:
// - S_OK if we succeeded, or an appropriate HRESULT for failing
HRESULT to_environment_strings_w(std::vector<wchar_t>& newEnvVars)
try
{
// Clear environment block before use.
constexpr auto cbChar{ sizeof(decltype(newEnvVars.begin())::value_type) };
if (!newEnvVars.empty())
{
::SecureZeroMemory(newEnvVars.data(), newEnvVars.size() * cbChar);
}
// Resize environment block to fit map.
size_t cchEnv{ 2 }; // For the block's double NULL-terminator.
for (const auto& [name, value] : _envMap)
{
// Final form of "name=value\0".
cchEnv += name.size() + 1 + value.size() + 1;
}
newEnvVars.resize(cchEnv);
// Ensure new block is wiped if we exit due to failure.
auto zeroNewEnv = wil::scope_exit([&]() noexcept {
::SecureZeroMemory(newEnvVars.data(), newEnvVars.size() * cbChar);
});
// Transform each map entry and copy it into the new environment block.
auto pEnvVars{ newEnvVars.data() };
auto cbRemaining{ cchEnv * cbChar };
for (const auto& [name, value] : _envMap)
{
// Final form of "name=value\0" for every entry.
{
const auto cchSrc{ name.size() };
const auto cbSrc{ cchSrc * cbChar };
RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, name.c_str(), cbSrc) != 0);
pEnvVars += cchSrc;
cbRemaining -= cbSrc;
}
RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, L"=", cbChar) != 0);
++pEnvVars;
cbRemaining -= cbChar;
{
const auto cchSrc{ value.size() };
const auto cbSrc{ cchSrc * cbChar };
RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, value.c_str(), cbSrc) != 0);
pEnvVars += cchSrc;
cbRemaining -= cbSrc;
}
RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, L"\0", cbChar) != 0);
++pEnvVars;
cbRemaining -= cbChar;
}
// Environment block only has to be NULL-terminated, but double NULL-terminate anyway.
RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, L"\0\0", cbChar * 2) != 0);
cbRemaining -= cbChar * 2;
RETURN_HR_IF(E_UNEXPECTED, cbRemaining != 0);
zeroNewEnv.release(); // success; don't wipe new environment block on exit
return S_OK;
}
CATCH_RETURN();
auto& as_map() noexcept
{
return _envMap;

View File

@ -342,4 +342,35 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
{
return prefix_split<>(str, needle);
}
//
// A case-insensitive wide-character map is used to store environment variables
// due to documented requirements:
//
// "All strings in the environment block must be sorted alphabetically by name.
// The sort is case-insensitive, Unicode order, without regard to locale.
// Because the equal sign is a separator, it must not be used in the name of
// an environment variable."
// https://docs.microsoft.com/en-us/windows/desktop/ProcThread/changing-environment-variables
//
// - Returns CSTR_LESS_THAN, CSTR_EQUAL or CSTR_GREATER_THAN
[[nodiscard]] inline int compare_string_ordinal(const std::wstring_view& lhs, const std::wstring_view& rhs) noexcept
{
const auto result = CompareStringOrdinal(
lhs.data(),
::base::saturated_cast<int>(lhs.size()),
rhs.data(),
::base::saturated_cast<int>(rhs.size()),
TRUE);
FAIL_FAST_LAST_ERROR_IF(!result);
return result;
}
struct wstring_case_insensitive_compare
{
[[nodiscard]] bool operator()(const std::wstring& lhs, const std::wstring& rhs) const noexcept
{
return compare_string_ordinal(lhs, rhs) == CSTR_LESS_THAN;
}
};
}

View File

@ -1,132 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "inc/Environment.hpp"
using namespace ::Microsoft::Console::Utils;
// We cannot use spand or not_null because we're dealing with \0\0-terminated buffers of unknown length
#pragma warning(disable : 26481 26429)
// Function Description:
// - Updates an EnvironmentVariableMapW with the current process's unicode
// environment variables ignoring ones already set in the provided map.
// Arguments:
// - map: The map to populate with the current processes's environment variables.
// Return Value:
// - S_OK if we succeeded, or an appropriate HRESULT for failing
HRESULT Microsoft::Console::Utils::UpdateEnvironmentMapW(EnvironmentVariableMapW& map) noexcept
try
{
LPWCH currentEnvVars{};
auto freeCurrentEnv = wil::scope_exit([&] {
if (currentEnvVars)
{
FreeEnvironmentStringsW(currentEnvVars);
currentEnvVars = nullptr;
}
});
currentEnvVars = ::GetEnvironmentStringsW();
RETURN_HR_IF_NULL(E_OUTOFMEMORY, currentEnvVars);
// Each entry is NULL-terminated; block is guaranteed to be double-NULL terminated at a minimum.
for (const wchar_t* lastCh{ currentEnvVars }; *lastCh != '\0'; ++lastCh)
{
// Copy current entry into temporary map.
const auto cchEntry{ ::wcslen(lastCh) };
const std::wstring_view entry{ lastCh, cchEntry };
// Every entry is of the form "name=value\0".
const auto pos = entry.find_first_of(L"=", 0, 1);
RETURN_HR_IF(E_UNEXPECTED, pos == std::wstring::npos);
std::wstring name{ entry.substr(0, pos) }; // portion before '='
std::wstring value{ entry.substr(pos + 1) }; // portion after '='
// Don't replace entries that already exist.
map.try_emplace(std::move(name), std::move(value));
lastCh += cchEntry;
}
return S_OK;
}
CATCH_RETURN();
// Function Description:
// - Creates a new environment block using the provided vector as appropriate
// (resizing if needed) based on the provided environment variable map
// matching the format of GetEnvironmentStringsW.
// Arguments:
// - map: The map to populate the new environment block vector with.
// - newEnvVars: The vector that will be used to create the new environment block.
// Return Value:
// - S_OK if we succeeded, or an appropriate HRESULT for failing
HRESULT Microsoft::Console::Utils::EnvironmentMapToEnvironmentStringsW(EnvironmentVariableMapW& map, std::vector<wchar_t>& newEnvVars) noexcept
try
{
// Clear environment block before use.
constexpr auto cbChar{ sizeof(decltype(newEnvVars.begin())::value_type) };
if (!newEnvVars.empty())
{
::SecureZeroMemory(newEnvVars.data(), newEnvVars.size() * cbChar);
}
// Resize environment block to fit map.
size_t cchEnv{ 2 }; // For the block's double NULL-terminator.
for (const auto& [name, value] : map)
{
// Final form of "name=value\0".
cchEnv += name.size() + 1 + value.size() + 1;
}
newEnvVars.resize(cchEnv);
// Ensure new block is wiped if we exit due to failure.
auto zeroNewEnv = wil::scope_exit([&]() noexcept {
::SecureZeroMemory(newEnvVars.data(), newEnvVars.size() * cbChar);
});
// Transform each map entry and copy it into the new environment block.
auto pEnvVars{ newEnvVars.data() };
auto cbRemaining{ cchEnv * cbChar };
for (const auto& [name, value] : map)
{
// Final form of "name=value\0" for every entry.
{
const auto cchSrc{ name.size() };
const auto cbSrc{ cchSrc * cbChar };
RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, name.c_str(), cbSrc) != 0);
pEnvVars += cchSrc;
cbRemaining -= cbSrc;
}
RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, L"=", cbChar) != 0);
++pEnvVars;
cbRemaining -= cbChar;
{
const auto cchSrc{ value.size() };
const auto cbSrc{ cchSrc * cbChar };
RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, value.c_str(), cbSrc) != 0);
pEnvVars += cchSrc;
cbRemaining -= cbSrc;
}
RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, L"\0", cbChar) != 0);
++pEnvVars;
cbRemaining -= cbChar;
}
// Environment block only has to be NULL-terminated, but double NULL-terminate anyway.
RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, L"\0\0", cbChar * 2) != 0);
cbRemaining -= cbChar * 2;
RETURN_HR_IF(E_UNEXPECTED, cbRemaining != 0);
zeroNewEnv.release(); // success; don't wipe new environment block on exit
return S_OK;
}
CATCH_RETURN();

View File

@ -1,31 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
namespace Microsoft::Console::Utils
{
//
// A case-insensitive wide-character map is used to store environment variables
// due to documented requirements:
//
// "All strings in the environment block must be sorted alphabetically by name.
// The sort is case-insensitive, Unicode order, without regard to locale.
// Because the equal sign is a separator, it must not be used in the name of
// an environment variable."
// https://docs.microsoft.com/en-us/windows/desktop/ProcThread/changing-environment-variables
//
struct WStringCaseInsensitiveCompare
{
[[nodiscard]] bool operator()(const std::wstring& lhs, const std::wstring& rhs) const noexcept
{
return (::_wcsicmp(lhs.c_str(), rhs.c_str()) < 0);
}
};
using EnvironmentVariableMapW = std::map<std::wstring, std::wstring, WStringCaseInsensitiveCompare>;
[[nodiscard]] HRESULT UpdateEnvironmentMapW(EnvironmentVariableMapW& map) noexcept;
[[nodiscard]] HRESULT EnvironmentMapToEnvironmentStringsW(EnvironmentVariableMapW& map,
std::vector<wchar_t>& newEnvVars) noexcept;
};

View File

@ -15,7 +15,6 @@
<ClCompile Include="..\ColorFix.cpp" />
<ClCompile Include="..\convert.cpp" />
<ClCompile Include="..\colorTable.cpp" />
<ClCompile Include="..\Environment.cpp" />
<ClCompile Include="..\GlyphWidth.cpp" />
<ClCompile Include="..\MouseEvent.cpp" />
<ClCompile Include="..\FocusEvent.cpp" />
@ -43,7 +42,6 @@
<ClInclude Include="..\inc\ColorFix.hpp" />
<ClInclude Include="..\inc\convert.hpp" />
<ClInclude Include="..\inc\colorTable.hpp" />
<ClInclude Include="..\inc\Environment.hpp" />
<ClInclude Include="..\inc\GlyphWidth.hpp" />
<ClInclude Include="..\inc\IInputEvent.hpp" />
<ClInclude Include="..\inc\sgrStack.hpp" />