Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/windows/wslc/commands/ContainerCreateCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ std::vector<Argument> ContainerCreateCommand::GetArguments() const
Argument::Create(ArgType::DNSSearch),
Argument::Create(ArgType::Entrypoint),
Argument::Create(ArgType::Env, false, NO_LIMIT),
Argument::Create(ArgType::EnvFile),
Argument::Create(ArgType::EnvFile, false, NO_LIMIT),
Argument::Create(ArgType::GroupId),
Argument::Create(ArgType::Interactive),
Argument::Create(ArgType::Name),
Expand Down
4 changes: 2 additions & 2 deletions src/windows/wslc/commands/ContainerExecCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ std::vector<Argument> ContainerExecCommand::GetArguments() const
std::nullopt,
L"Arguments to pass to the command being executed inside the container"),
Argument::Create(ArgType::Detach),
Argument::Create(ArgType::Env, std::nullopt, NO_LIMIT),
Argument::Create(ArgType::EnvFile),
Argument::Create(ArgType::Env, false, NO_LIMIT),
Argument::Create(ArgType::EnvFile, false, NO_LIMIT),
Argument::Create(ArgType::Interactive),
Argument::Create(ArgType::Session),
Argument::Create(ArgType::TTY),
Expand Down
4 changes: 2 additions & 2 deletions src/windows/wslc/commands/ContainerRunCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ std::vector<Argument> ContainerRunCommand::GetArguments() const
Argument::Create(ArgType::DNSOption),
Argument::Create(ArgType::DNSSearch),
Argument::Create(ArgType::Entrypoint),
Argument::Create(ArgType::Env, std::nullopt, NO_LIMIT),
Argument::Create(ArgType::EnvFile),
Argument::Create(ArgType::Env, false, NO_LIMIT),
Argument::Create(ArgType::EnvFile, false, NO_LIMIT),
Argument::Create(ArgType::Interactive),
Argument::Create(ArgType::Name),
Argument::Create(ArgType::NoDNS),
Expand Down
88 changes: 85 additions & 3 deletions src/windows/wslc/services/ContainerModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ Module Name:
Abstract:

This file contains the ContainerModel implementation

--*/

#include <precomp.h>
#include "precomp.h"
#include "ContainerModel.h"

namespace wsl::windows::wslc::models {
Expand Down Expand Up @@ -190,4 +189,87 @@ VolumeMount VolumeMount::Parse(const std::wstring& value)
vm.m_hostPath = value.substr(0, splitColon);
return vm;
}
} // namespace wsl::windows::wslc::models

static inline bool IsSpace(wchar_t ch)
{
return std::iswspace(ch) != 0;
}

std::optional<std::wstring> EnvironmentVariable::Parse(const std::wstring& entry)
{
if (entry.empty() || std::all_of(entry.begin(), entry.end(), IsSpace))
{
return std::nullopt;
}

std::wstring key;
std::optional<std::wstring> value;

auto delimiterPos = entry.find('=');
if (delimiterPos == std::wstring::npos)
{
key = entry;
}
else
{
key = entry.substr(0, delimiterPos);
value = entry.substr(delimiterPos + 1);
}

if (key.empty())
{
THROW_HR_WITH_USER_ERROR(E_INVALIDARG, L"Environment variable key cannot be empty");
}

if (std::any_of(key.begin(), key.end(), IsSpace))
{
THROW_HR_WITH_USER_ERROR(E_INVALIDARG, std::format(L"Environment variable key '{}' cannot contain whitespace", key));
}

if (!value.has_value())
{
wil::unique_hglobal_string envValue;
auto hr = wil::GetEnvironmentVariableW(key.c_str(), envValue);
if (FAILED(hr) || envValue.get() == nullptr)
{
return std::nullopt;
}

value = std::wstring(envValue.get());
}

return std::format(L"{}={}", key, value.value());
}

std::vector<std::wstring> EnvironmentVariable::ParseFile(const std::wstring& filePath)
{
std::ifstream file(filePath);
if (!file.is_open() || !file.good())
{
THROW_HR_WITH_USER_ERROR(E_INVALIDARG, std::format(L"Environment file '{}' cannot be opened for reading", filePath));
}

// Read the file line by line
std::vector<std::wstring> envVars;
std::string line;
while (std::getline(file, line))
{
// Remove leading whitespace
line.erase(line.begin(), std::find_if(line.begin(), line.end(), [](unsigned char ch) { return !std::isspace(ch); }));

// Skip empty lines and comments
if (line.empty() || line[0] == '#')
{
continue;
}

auto envVar = Parse(wsl::shared::string::MultiByteToWide(line));
if (envVar.has_value())
{
envVars.push_back(std::move(envVar.value()));
}
}

return envVars;
}
} // namespace wsl::windows::wslc::models
7 changes: 7 additions & 0 deletions src/windows/wslc/services/ContainerModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ enum class FormatType
struct ContainerOptions
{
std::vector<std::string> Arguments;
std::vector<std::string> EnvironmentVariables;
bool Detach = false;
bool Interactive = false;
std::string Name;
Expand Down Expand Up @@ -69,6 +70,12 @@ struct ContainerInformation
NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(ContainerInformation, Id, Name, Image, State, StateChangedAt, CreatedAt);
};

struct EnvironmentVariable
{
static std::optional<std::wstring> Parse(const std::wstring& entry);
static std::vector<std::wstring> ParseFile(const std::wstring& filePath);
};

struct PublishPort
{
enum class Protocol
Expand Down
4 changes: 2 additions & 2 deletions src/windows/wslc/services/ContainerService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ static wsl::windows::common::RunningWSLAContainer CreateInternal(
WI_SetFlagIf(containerFlags, WSLAContainerFlagsRm, options.Remove);

wsl::windows::common::WSLAContainerLauncher containerLauncher(
image, options.Name, options.Arguments, {}, WSLAContainerNetworkTypeBridged, processFlags);
image, options.Name, options.Arguments, options.EnvironmentVariables, WSLAContainerNetworkTypeBridged, processFlags);

// Set port options if provided
for (const auto& port : options.Ports)
Expand Down Expand Up @@ -341,7 +341,7 @@ int ContainerService::Exec(Session& session, const std::string& id, ContainerOpt

ConsoleService consoleService;
return consoleService.AttachToCurrentConsole(
wsl::windows::common::WSLAProcessLauncher({}, options.Arguments, {}, execFlags).Launch(*container));
wsl::windows::common::WSLAProcessLauncher({}, options.Arguments, options.EnvironmentVariables, execFlags).Launch(*container));
}

InspectContainer ContainerService::Inspect(Session& session, const std::string& id)
Expand Down
26 changes: 26 additions & 0 deletions src/windows/wslc/tasks/ContainerTasks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,32 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
options.Arguments.emplace_back(WideToMultiByte(context.Args.Get<ArgType::Command>()));
}

if (context.Args.Contains(ArgType::EnvFile))
{
auto const& envFiles = context.Args.GetAll<ArgType::EnvFile>();
for (const auto& envFile : envFiles)
{
auto parsedEnvVars = EnvironmentVariable::ParseFile(envFile);
for (const auto& envVar : parsedEnvVars)
{
options.EnvironmentVariables.push_back(wsl::shared::string::WideToMultiByte(envVar));
}
}
}

if (context.Args.Contains(ArgType::Env))
{
auto const& envArgs = context.Args.GetAll<ArgType::Env>();
for (const auto& arg : envArgs)
{
auto envVar = EnvironmentVariable::Parse(arg);
if (envVar)
{
options.EnvironmentVariables.push_back(wsl::shared::string::WideToMultiByte(*envVar));
}
}
}

if (context.Args.Contains(ArgType::ForwardArgs))
{
auto const& forwardArgs = context.Args.Get<ArgType::ForwardArgs>();
Expand Down
179 changes: 179 additions & 0 deletions test/windows/wslc/WSLCCLIEnvVarParserUnitTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*++

Copyright (c) Microsoft. All rights reserved.

Module Name:

WSLCCLIEnvVarParserUnitTests.cpp

Abstract:

This file contains unit tests for WSLC CLI environment variable parsing and validation.

--*/

#include "precomp.h"
#include "windows/Common.h"
#include "WSLCCLITestHelpers.h"
#include "ContainerModel.h"

#include <filesystem>
#include <fstream>

using namespace wsl::windows::wslc;

namespace WSLCCLIEnvVarParserUnitTests {

class WSLCCLIEnvVarParserUnitTests
{
WSL_TEST_CLASS(WSLCCLIEnvVarParserUnitTests)

TEST_METHOD_SETUP(TestMethodSetup)
{
EnvTestFile = wsl::windows::common::filesystem::GetTempFilename();
return true;
}

TEST_METHOD_CLEANUP(TestMethodCleanup)
{
DeleteFileW(EnvTestFile.c_str());
return true;
}

TEST_METHOD(WSLCCLIEnvVarParser_ValidEnvVars)
{
const auto parsed = models::EnvironmentVariable::Parse(L"FOO=bar");
VERIFY_IS_TRUE(parsed.has_value());
VERIFY_ARE_EQUAL(L"FOO=bar", parsed.value());
}

TEST_METHOD(WSLCCLIEnvVarParser_UsesProcessEnvWhenValueMissing)
{
constexpr const auto key = L"WSLC_TEST_ENV_FROM_PROCESS";
VERIFY_IS_TRUE(SetEnvironmentVariableW(key, L"process_value"));

auto cleanup = wil::scope_exit([&] { SetEnvironmentVariableW(key, nullptr); });

const auto parsed = models::EnvironmentVariable::Parse(key);
VERIFY_IS_TRUE(parsed.has_value());
VERIFY_ARE_EQUAL(L"WSLC_TEST_ENV_FROM_PROCESS=process_value", parsed.value());
}

TEST_METHOD(WSLCCLIEnvVarParser_NulloptForWhitespaceOrUnsetVar)
{
const auto whitespaceOnly = models::EnvironmentVariable::Parse(L" \t ");
VERIFY_IS_FALSE(whitespaceOnly.has_value());

SetEnvironmentVariableA("WSLC_TEST_ENV_UNSET", nullptr);
const auto missingFromProcess = models::EnvironmentVariable::Parse(L"WSLC_TEST_ENV_UNSET");
VERIFY_IS_FALSE(missingFromProcess.has_value());
}

TEST_METHOD(WSLCCLIEnvVarParser_InvalidKeysThrow)
{
VERIFY_THROWS(models::EnvironmentVariable::Parse(L"=value"), std::exception);
VERIFY_THROWS(models::EnvironmentVariable::Parse(L"BAD KEY=value"), std::exception);
VERIFY_THROWS(models::EnvironmentVariable::Parse(L"BAD\tKEY=value"), std::exception);
VERIFY_THROWS(models::EnvironmentVariable::Parse(L"BAD\nKEY=value"), std::exception);
}

TEST_METHOD(WSLCCLIEnvVarParser_ParseFileParsesAndSkipsExpectedLines)
{
constexpr const auto key = L"WSLC_TEST_ENV_FROM_FILE";
VERIFY_IS_TRUE(SetEnvironmentVariableW(key, L"file_process_value") == TRUE);

auto envCleanup = wil::scope_exit([&] { SetEnvironmentVariableW(key, nullptr); });

std::ofstream file(EnvTestFile);
VERIFY_IS_TRUE(file.is_open());
file << "# comment\n";
file << "\n";
file << "KEY1=VALUE1\n";
file << " KEY2=VALUE2\n";
file << "WSLC_TEST_ENV_FROM_FILE\n";
file << "WSLC_TEST_ENV_DOES_NOT_EXIST\n";
file.close();

const auto parsed = models::EnvironmentVariable::ParseFile(EnvTestFile.wstring());

VERIFY_ARE_EQUAL(3U, parsed.size());
VERIFY_ARE_EQUAL(L"KEY1=VALUE1", parsed[0]);
VERIFY_ARE_EQUAL(L"KEY2=VALUE2", parsed[1]);
VERIFY_ARE_EQUAL(L"WSLC_TEST_ENV_FROM_FILE=file_process_value", parsed[2]);
}

TEST_METHOD(WSLCCLIEnvVarParser_ParseFileThrowsWhenMissing)
{
VERIFY_THROWS(models::EnvironmentVariable::ParseFile(L"ENV_FILE_NOT_FOUND"), std::exception);
}

TEST_METHOD(WSLCCLIEnvVarParser_ExplicitEmptyValueIsValid)
{
const auto parsed = models::EnvironmentVariable::Parse(L"FOO=");
VERIFY_IS_TRUE(parsed.has_value());
VERIFY_ARE_EQUAL(L"FOO=", parsed.value());
}

TEST_METHOD(WSLCCLIEnvVarParser_MultipleEqualsPreservedInValue)
{
const auto parsed = models::EnvironmentVariable::Parse(L"FOO=a=b=c");
VERIFY_IS_TRUE(parsed.has_value());
VERIFY_ARE_EQUAL(L"FOO=a=b=c", parsed.value());
}

TEST_METHOD(WSLCCLIEnvVarParser_EmptyInputReturnsNullopt)
{
const auto parsed = models::EnvironmentVariable::Parse(L"");
VERIFY_IS_FALSE(parsed.has_value());
}

TEST_METHOD(WSLCCLIEnvVarParser_UsesProcessEnvWhenValueIsExplicitlyEmpty)
{
constexpr const auto key = L"WSLC_TEST_ENV_EMPTY_VALUE";
VERIFY_IS_TRUE(SetEnvironmentVariableW(key, L""));

auto cleanup = wil::scope_exit([&] { SetEnvironmentVariableW(key, nullptr); });

const auto parsed = models::EnvironmentVariable::Parse(key);
VERIFY_IS_TRUE(parsed.has_value());
VERIFY_ARE_EQUAL(L"WSLC_TEST_ENV_EMPTY_VALUE=", parsed.value());
}

TEST_METHOD(WSLCCLIEnvVarParser_ParseFilePreservesTrailingWhitespaceInValue)
{
std::ofstream file(EnvTestFile);
VERIFY_IS_TRUE(file.is_open());
file << "KEY=value \n";
file.close();

const auto parsed = models::EnvironmentVariable::ParseFile(EnvTestFile.wstring());

VERIFY_ARE_EQUAL(1U, parsed.size());
VERIFY_ARE_EQUAL(L"KEY=value ", parsed[0]);
}

TEST_METHOD(WSLCCLIEnvVarParser_ParseFileThrowsOnInvalidLine)
{
std::ofstream file(EnvTestFile);
VERIFY_IS_TRUE(file.is_open());
file << "BAD KEY=value\n";
file.close();

VERIFY_THROWS(models::EnvironmentVariable::ParseFile(EnvTestFile.wstring()), std::exception);
}

TEST_METHOD(WSLCCLIEnvVarParser_ParseFileEmptyFileReturnsEmpty)
{
std::ofstream file(EnvTestFile);
VERIFY_IS_TRUE(file.is_open());
file.close();

const auto parsed = models::EnvironmentVariable::ParseFile(EnvTestFile.wstring());
VERIFY_ARE_EQUAL(0U, parsed.size());
}

private:
std::filesystem::path EnvTestFile;
};

} // namespace WSLCCLIEnvVarParserUnitTests
Loading
Loading