Skip to content
Merged
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
3 changes: 3 additions & 0 deletions modules/process/include/rawframe/process/child.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ struct ChildSettings {
/// (Linux a signal, macOS a process source, Windows a wait on the
/// parent), which it takes as any stop, in order.
bool stopsWithThis = false;
/// Environment variables of this process it is not given, by name; on
/// Windows, whose names ignore case, in any case.
std::vector<std::string> withheld;
};

/// The environment variable a child that stops with its parent is told
Expand Down
38 changes: 31 additions & 7 deletions modules/process/src/child.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "rawframe/process/errors.h"

#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
Expand Down Expand Up @@ -65,17 +66,36 @@ void appendQuoted(std::wstring& line, const std::wstring& argument) {
line += L'"';
}

/// This process's environment as a block CreateProcessW takes, with its
/// identity for a child that stops with it and the child's own stop
/// event in place of any it was given itself.
/// Whether `entry`, a NAME=value of the environment, names one of `names`;
/// Windows's names ignore case.
bool withheld(std::wstring_view entry, const std::vector<std::wstring>& names) {
// A name may begin with '=' on Windows (the shell's per-drive
// directories), so its own '=' is the first after its first character.
const std::size_t kEnd = entry.find(L'=', 1);
const std::wstring_view kName = entry.substr(0, kEnd);
return std::ranges::any_of(names, [kName](const std::wstring& name) {
return ::CompareStringOrdinal(
kName.data(), static_cast<int>(kName.size()), name.data(), static_cast<int>(name.size()), TRUE) ==
CSTR_EQUAL;
});
}

/// This process's environment as a block CreateProcessW takes, without the
/// variables the child is not given, with its identity for a child that
/// stops with it and the child's own stop event in place of any it was
/// given itself.
std::wstring environmentFor(const ChildSettings& settings, HANDLE stop) {
const std::wstring kParent = wide(std::string{kParentVariable} + "=");
const std::wstring kStop = wide(std::string{kStopVariable} + "=");
std::vector<std::wstring> names;
for (const std::string& each : settings.withheld) {
names.push_back(wide(each));
}
std::wstring block;
if (wchar_t* strings = ::GetEnvironmentStringsW(); strings != nullptr) {
for (const wchar_t* each = strings; *each != L'\0'; each += std::wstring_view{each}.size() + 1) {
const std::wstring_view kEntry{each};
if (!kEntry.starts_with(kParent) && !kEntry.starts_with(kStop)) {
if (!kEntry.starts_with(kParent) && !kEntry.starts_with(kStop) && !withheld(kEntry, names)) {
block.append(kEntry);
block += L'\0';
}
Expand Down Expand Up @@ -294,12 +314,16 @@ result::Result<Child> Child::start(const ChildSettings& settings) {
if (failed == 0) {
failed = posix_spawnattr_setflags(&attributes, POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSIGMASK);
}
// This process's environment, with its identity for a child that stops
// with it in place of any it was given itself.
// This process's environment without the variables the child is not
// given, with its identity for a child that stops with it in place of
// any it was given itself.
std::vector<std::string> told;
const std::string kParentPrefix = std::string{kParentVariable} + "=";
for (char** each = environ; *each != nullptr; ++each) {
if (std::string_view{*each}.substr(0, kParentPrefix.size()) != kParentPrefix) {
const std::string_view kEntry{*each};
const std::string_view kName = kEntry.substr(0, kEntry.find('='));
if (kEntry.substr(0, kParentPrefix.size()) != kParentPrefix &&
std::ranges::find(settings.withheld, kName) == settings.withheld.end()) {
told.emplace_back(*each);
}
}
Expand Down
5 changes: 5 additions & 0 deletions modules/process/tests/child.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ int main(int argc, char** argv) {
std::fputs(parent != nullptr ? parent : "none", stdout);
return 0;
}
if (kMode == "variable" && argc > 2) {
const char* value = std::getenv(argv[2]);
std::fputs(value != nullptr ? value : "none", stdout);
return 0;
}
if (kMode == "wait") {
#if defined(_WIN32)
::SetConsoleCtrlHandler(&askStopOnEvent, TRUE);
Expand Down
45 changes: 42 additions & 3 deletions modules/process/tests/child_test.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Child processes: a child's exit code, its output and error in one
// file, its arguments passed as they are (spaces, quotes, and UTF-8 too), a stop
// request it answers as a Host does, a kill, a kill when its owner goes,
// and a program that cannot be started.
// file, its arguments passed as they are (spaces, quotes, and UTF-8 too), the
// variables withheld from it, a stop request it answers as a Host does, a
// kill, a kill when its owner goes, and a program that cannot be started.

#include "rawframe/process/child.h"
#include "rawframe/process/errors.h"
Expand All @@ -17,6 +17,7 @@
#include <string>
#include <string_view>
#include <thread>
#include <vector>

#if defined(_WIN32)
#include <windows.h>
Expand Down Expand Up @@ -134,6 +135,44 @@ RAWFRAME_TEST(AChildThatStopsWithThisIsToldWhoThisIs) {
}
}

RAWFRAME_TEST(AChildIsNotGivenTheVariablesWithheldFromIt) {
// Withheld by name, in any case on Windows, whose names ignore it; the
// others are given as they are.
#if defined(_WIN32)
const auto kSet = [](const char* name, const char* value) {
::_putenv_s(name, value);
};
const std::vector<std::string> kWithheld = {"rawframe_withheld"};
#else
const auto kSet = [](const char* name, const char* value) {
if (value[0] == '\0') {
::unsetenv(name);
} else {
::setenv(name, value, 1);
}
};
const std::vector<std::string> kWithheld = {"RAWFRAME_WITHHELD"};
#endif
kSet("RAWFRAME_WITHHELD", "held");
kSet("RAWFRAME_GIVEN", "given");
const auto kRead = [&kWithheld](const char* name, const char* file) {
const std::filesystem::path kRead = scratch(file);
auto read = Child::start({.program = RAWFRAME_PROCESS_CHILD,
.arguments = {"variable", name},
.output = kRead,
.withheld = kWithheld});
RAWFRAME_EXPECT(startedAll(read));
if (read.has_value()) {
RAWFRAME_EXPECT(waitFor(*read) == 0);
}
return test::readFile(kRead.string());
};
RAWFRAME_EXPECT(kRead("RAWFRAME_WITHHELD", "withheld.txt") == "none");
RAWFRAME_EXPECT(kRead("RAWFRAME_GIVEN", "given.txt") == "given");
kSet("RAWFRAME_WITHHELD", "");
kSet("RAWFRAME_GIVEN", "");
}

RAWFRAME_TEST(AChildStopsWhenAskedAndIsKilledOtherwise) {
const std::filesystem::path kHeard = scratch("heard.txt");
auto waiting = Child::start({.program = RAWFRAME_PROCESS_CHILD, .arguments = {"wait"}, .output = kHeard});
Expand Down
24 changes: 22 additions & 2 deletions modules/studio/src/version_control.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@

namespace rawframe::studio {

std::vector<std::string> gitRepositoryVariables() {
return {"GIT_ALTERNATE_OBJECT_DIRECTORIES",
"GIT_CONFIG",
"GIT_CONFIG_PARAMETERS",
"GIT_CONFIG_COUNT",
"GIT_OBJECT_DIRECTORY",
"GIT_DIR",
"GIT_WORK_TREE",
"GIT_IMPLICIT_WORK_TREE",
"GIT_GRAFT_FILE",
"GIT_INDEX_FILE",
"GIT_NO_REPLACE_OBJECTS",
"GIT_REPLACE_REF_BASE",
"GIT_PREFIX",
"GIT_SHALLOW_FILE",
"GIT_COMMON_DIR"};
}

void VersionControl::ask(std::string purpose, std::vector<std::string> arguments) {
if (!trusted_) {
return;
Expand Down Expand Up @@ -39,8 +57,10 @@ std::optional<VersionControl::Refusal> VersionControl::start(std::string_view pa
}
auto [purpose, arguments] = std::move(waiting_.front());
waiting_.pop_front();
auto started =
process::Child::start({.program = *kProgram, .arguments = words(directory, arguments), .output = output});
auto started = process::Child::start({.program = *kProgram,
.arguments = words(directory, arguments),
.output = output,
.withheld = gitRepositoryVariables()});
if (!started.has_value()) {
waiting_.clear();
return Refusal{.kind = Refusal::Kind::Failed,
Expand Down
9 changes: 8 additions & 1 deletion modules/studio/src/version_control.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
// hooks), so git runs only in a repository the author has said they trust,
// as an editor asks before it trusts a workspace. Even there, the programs
// a repository names that reading never needs stay off: its file monitor,
// its external diff, and its text conversions. Nothing here waits.
// its external diff, and its text conversions. Git is not given the
// variables that name a repository, so the game's directory is the one it
// works in, whatever started Studio set. Nothing here waits.

#include "rawframe/process/child.h"

Expand All @@ -21,6 +23,11 @@

namespace rawframe::studio {

/// The environment variables that tell git which repository, index, objects
/// and settings to use, as `git rev-parse --local-env-vars` lists them. Git
/// sets them for a hook it runs; any of them outranks `-C`.
[[nodiscard]] std::vector<std::string> gitRepositoryVariables();

class VersionControl {
public:
/// Why what waited did not start; what waited is dropped.
Expand Down
24 changes: 23 additions & 1 deletion modules/studio/tests/version_control_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#endif

using rawframe::process::Child;
using rawframe::studio::gitRepositoryVariables;
using rawframe::studio::mintedIdentity;
using rawframe::studio::programOnPath;
using rawframe::studio::VersionControl;
Expand All @@ -31,14 +32,27 @@ std::string pathVariable() {
return kPath != nullptr ? kPath : "";
}

/// Sets this process's variable `name`, or removes it for an empty value.
void setVariable(const char* name, const std::string& value) {
#if defined(_WIN32)
::_putenv_s(name, value.c_str());
#else
if (value.empty()) {
::unsetenv(name);
} else {
::setenv(name, value.c_str(), 1);
}
#endif
}

/// Git run to its end in `directory`, for the test's own setup.
bool git(const std::filesystem::path& directory, std::vector<std::string> arguments) {
const auto kProgram = programOnPath("git", pathVariable());
if (!kProgram.has_value()) {
return false;
}
arguments.insert(arguments.begin(), {"-C", directory.string()});
auto child = Child::start({.program = *kProgram, .arguments = arguments});
auto child = Child::start({.program = *kProgram, .arguments = arguments, .withheld = gitRepositoryVariables()});
for (int tick = 0; child.has_value() && tick < 1000; ++tick) {
if (const auto kCode = child->exited()) {
return *kCode == 0;
Expand Down Expand Up @@ -113,6 +127,11 @@ RAWFRAME_TEST(GitRunsOnlyInATrustedRepositoryAndNeverItsReadersPrograms) {
const std::filesystem::path kOutput = kRoot / "git.out";
std::filesystem::create_directories(kGame);
std::filesystem::create_directories(kMarks);
// Started as a hook is, told another repository: git, given that,
// would make and use it instead of the game's.
const std::filesystem::path kElsewhere = kRoot / "elsewhere";
setVariable("GIT_DIR", (kElsewhere / ".git").string());
setVariable("GIT_WORK_TREE", kElsewhere.string());
RAWFRAME_EXPECT(git(kGame, {"init", "--quiet"}));
RAWFRAME_EXPECT(git(kGame, {"config", "user.name", "Author"}));
RAWFRAME_EXPECT(git(kGame, {"config", "user.email", "author@example.com"}));
Expand Down Expand Up @@ -164,6 +183,9 @@ RAWFRAME_TEST(GitRunsOnlyInATrustedRepositoryAndNeverItsReadersPrograms) {
RAWFRAME_EXPECT(runAll(control, kGame, kOutput) == 2);
RAWFRAME_EXPECT(std::filesystem::exists(kMarks / "hook"));
RAWFRAME_EXPECT(!std::filesystem::exists(kMarks / "monitor"));
RAWFRAME_EXPECT(!std::filesystem::exists(kElsewhere));
setVariable("GIT_DIR", "");
setVariable("GIT_WORK_TREE", "");
std::filesystem::remove_all(kRoot, error);
}
#endif
Loading