// Copyright (c) 2026 Progmasoft.
// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iterator>
#include <string_view>
#include <thread>
#include "Process.hpp"
#if defined(_WIN32)
# ifndef NOMINMAX
# define NOMINMAX
# endif
# ifndef WIN32_LEAN_AND_MEAN
# define WIN32_LEAN_AND_MEAN
# endif
# include <fcntl.h>
# include <io.h>
# include <share.h>
# include <sys/stat.h>
# include <windows.h>
#else
# include <cerrno>
# include <csignal>
# include <fcntl.h>
# include <spawn.h>
# include <sys/types.h>
# include <sys/wait.h>
# include <unistd.h>
# if defined(__APPLE__)
# include <crt_externs.h>
# include <mach-o/dyld.h>
# endif
# if !defined(__APPLE__) && !(defined(__GLIBC__) && defined(_GNU_SOURCE))
extern char **environ;
# endif
#endif
namespace Progmasoft::Catch3::Runner
{
bool
IsSecretVariable(std::string_view name)
{
std::string upper;
upper.reserve(name.size());
for (const char character : name)
{
upper.push_back(character >= 'a' && character <= 'z'
? static_cast<char>(character - 'a' + 'A')
: character);
}
constexpr std::string_view kSecretWords[] = {
"TOKEN", "SECRET", "PASSWORD", "PASSWD", "CREDENTIAL",
"API_KEY", "APIKEY", "PRIVATE_KEY", "ACCESS_KEY",
};
return std::any_of(std::begin(kSecretWords),
std::end(kSecretWords),
[&](std::string_view word) {
return upper.find(word) != std::string::npos;
});
}
namespace
{
/// Whether a variable of this process stays out of a child: a secret
/// that the options do not keep. Windows spells names in any case.
bool
Withheld(std::string_view name, const std::vector<std::string> &kept)
{
if (!IsSecretVariable(name))
{
return false;
}
return std::none_of(
kept.begin(),
kept.end(),
[&](const std::string &keptName) {
if (keptName.size() != name.size())
{
return false;
}
#if defined(_WIN32)
return std::equal(
keptName.begin(),
keptName.end(),
name.begin(),
[](char left, char right) {
return std::toupper(
static_cast<unsigned char>(left))
== std::toupper(
static_cast<unsigned char>(right));
});
#else
return keptName == name;
#endif
});
}
#if defined(_WIN32)
HANDLE
AsHandle(std::intptr_t value) noexcept
{
return reinterpret_cast<HANDLE>(value);
}
std::intptr_t
FromHandle(HANDLE handle) noexcept
{
return reinterpret_cast<std::intptr_t>(handle);
}
std::wstring
Widen(std::string_view text)
{
if (text.empty())
{
return {};
}
const int size = MultiByteToWideChar(CP_UTF8,
0,
text.data(),
static_cast<int>(text.size()),
nullptr,
0);
std::wstring wide(static_cast<std::size_t>(size), L'\0');
MultiByteToWideChar(CP_UTF8,
0,
text.data(),
static_cast<int>(text.size()),
wide.data(),
size);
return wide;
}
std::string
Narrow(std::wstring_view text)
{
if (text.empty())
{
return {};
}
const int size = WideCharToMultiByte(CP_UTF8,
0,
text.data(),
static_cast<int>(text.size()),
nullptr,
0,
nullptr,
nullptr);
std::string narrow(static_cast<std::size_t>(size), '\0');
WideCharToMultiByte(CP_UTF8,
0,
text.data(),
static_cast<int>(text.size()),
narrow.data(),
size,
nullptr,
nullptr);
return narrow;
}
std::string
LastErrorMessage(const char *what)
{
const DWORD code = GetLastError();
wchar_t *buffer = nullptr;
const DWORD length = FormatMessageW(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM
| FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr,
code,
0,
reinterpret_cast<wchar_t *>(&buffer),
0,
nullptr);
std::string message
= length > 0 ? Narrow(std::wstring_view{ buffer, length })
: std::string{};
if (buffer != nullptr)
{
LocalFree(buffer);
}
while (!message.empty()
&& (message.back() == '\n' || message.back() == '\r'
|| message.back() == ' ' || message.back() == '.'))
{
message.pop_back();
}
return std::string{ what } + ": " + message + " (error "
+ std::to_string(code) + ")";
}
/// Quotes one argument the way the Microsoft C runtime splits a
/// command line back into `argv`.
void
AppendQuotedArgument(std::wstring &commandLine,
const std::wstring &argument)
{
const bool needsQuotes
= argument.empty()
|| argument.find_first_of(L" \t\n\v\"") != std::wstring::npos;
if (!needsQuotes)
{
commandLine += argument;
return;
}
commandLine.push_back(L'"');
std::size_t backslashes = 0;
for (const wchar_t character : argument)
{
if (character == L'\\')
{
++backslashes;
continue;
}
if (character == L'"')
{
commandLine.append(backslashes * 2 + 1, L'\\');
}
else
{
commandLine.append(backslashes, L'\\');
}
backslashes = 0;
commandLine.push_back(character);
}
commandLine.append(backslashes * 2, L'\\');
commandLine.push_back(L'"');
}
std::wstring
EnvironmentBlock(
const std::vector<std::pair<std::string, std::string>> &additions,
const std::vector<std::string> &kept)
{
std::vector<std::wstring> entries;
wchar_t *current = GetEnvironmentStringsW();
if (current != nullptr)
{
for (const wchar_t *entry = current; *entry != L'\0';
entry += wcslen(entry) + 1)
{
// A leading '=' marks the hidden per-drive variables.
const std::wstring_view text{ entry };
const auto equals = text.find(L'=', 1);
if (Withheld(Narrow(text.substr(0, equals)), kept))
{
continue;
}
entries.emplace_back(entry);
}
FreeEnvironmentStringsW(current);
}
for (const auto &[name, value] : additions)
{
const std::wstring wideName = Widen(name);
const auto removed = std::remove_if(
entries.begin(),
entries.end(),
[&](const std::wstring &entry) {
// A leading '=' marks the hidden per-drive variables.
const auto equals = entry.find(L'=', 1);
return equals == wideName.size()
&& CompareStringOrdinal(
entry.data(),
static_cast<int>(equals),
wideName.data(),
static_cast<int>(wideName.size()),
TRUE)
== CSTR_EQUAL;
});
entries.erase(removed, entries.end());
entries.push_back(wideName + L"=" + Widen(value));
}
std::wstring block;
for (const auto &entry : entries)
{
block += entry;
block.push_back(L'\0');
}
// A block ends in an empty entry; one without variables is two
// terminators.
if (entries.empty())
{
block.push_back(L'\0');
}
block.push_back(L'\0');
return block;
}
#else
int
Descriptor(std::intptr_t value) noexcept
{
return static_cast<int>(value);
}
char **
CurrentEnvironment() noexcept
{
# if defined(__APPLE__)
return *_NSGetEnviron();
# else
return environ;
# endif
}
std::string
ErrnoMessage(const char *what, int error)
{
return std::string{ what } + ": " + std::strerror(error);
}
bool
MakePipe(int (&descriptors)[2])
{
# if defined(__linux__)
return pipe2(descriptors, O_CLOEXEC) == 0;
# else
// Only the scheduler thread starts processes, so no other
// thread can fork between creating the pipe and marking it.
if (pipe(descriptors) != 0)
{
return false;
}
fcntl(descriptors[0], F_SETFD, FD_CLOEXEC);
fcntl(descriptors[1], F_SETFD, FD_CLOEXEC);
return true;
# endif
}
const char *
SignalName(int signal) noexcept
{
switch (signal)
{
case SIGSEGV:
ret
|