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
1 change: 1 addition & 0 deletions config.cmake.in
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS})
elseif(component STREQUAL "io")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake")
elseif(component STREQUAL "process")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_text.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_process.cmake")
elseif(component STREQUAL "parallel")
find_dependency(Threads)
Expand Down
4 changes: 3 additions & 1 deletion src/lang/process/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME process
PRIVATE_HEADERS error.h
SOURCES spawn.cc)
SOURCES spawn.cc command_line.h)

if(SOURCEMETA_CORE_INSTALL)
sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME process)
endif()

target_link_libraries(sourcemeta_core_process PRIVATE sourcemeta::core::text)
58 changes: 58 additions & 0 deletions src/lang/process/command_line.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#ifndef SOURCEMETA_CORE_PROCESS_COMMAND_LINE_H_
#define SOURCEMETA_CORE_PROCESS_COMMAND_LINE_H_

#if defined(_WIN32) && !defined(__MSYS__) && !defined(__CYGWIN__) && \
!defined(__MINGW32__) && !defined(__MINGW64__)

#include <cstddef> // std::size_t
#include <string> // std::string
#include <string_view> // std::string_view

namespace sourcemeta::core {

namespace {

// Quote a single argument for the inverse of CommandLineToArgvW, so that the
// child reconstructs the exact same argument vector
auto append_quoted_argument(std::string &command_line,
const std::string_view argument) -> void {
const bool needs_quoting{argument.empty() ||
argument.find_first_of(" \t\"") !=
std::string_view::npos};

if (!needs_quoting) {
command_line.append(argument);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When an argument contains an embedded NUL without spaces or quotes, this branch copies it into command_line; CreateProcessW then truncates the argument and potentially the remaining command line. Reject embedded NULs before assembling the command line.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lang/process/command_line.h, line 24:

<comment>When an argument contains an embedded NUL without spaces or quotes, this branch copies it into `command_line`; `CreateProcessW` then truncates the argument and potentially the remaining command line. Reject embedded NULs before assembling the command line.</comment>

<file context>
@@ -0,0 +1,58 @@
+                               std::string_view::npos};
+
+  if (!needs_quoting) {
+    command_line.append(argument);
+    return;
+  }
</file context>

return;
}

command_line.push_back('"');

for (auto cursor = argument.cbegin();; ++cursor) {
std::size_t backslash_count{0};
while (cursor != argument.cend() && *cursor == '\\') {
++cursor;
++backslash_count;
}

if (cursor == argument.cend()) {
command_line.append(backslash_count * 2, '\\');
break;
} else if (*cursor == '"') {
command_line.append(backslash_count * 2 + 1, '\\');
command_line.push_back('"');
} else {
command_line.append(backslash_count, '\\');
command_line.push_back(*cursor);
}
}

command_line.push_back('"');
}

} // namespace

} // namespace sourcemeta::core

#endif

#endif
88 changes: 78 additions & 10 deletions src/lang/process/include/sourcemeta/core/process.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@

#include <filesystem> // std::filesystem
#include <initializer_list> // std::initializer_list
#include <map> // std::map
#include <optional> // std::optional
#include <span> // std::span
#include <string> // std::string
#include <string_view> // std::string_view

/// @defgroup process Process
Expand All @@ -25,11 +28,29 @@

namespace sourcemeta::core {

/// @ingroup process
/// The settings for running a program.
///
/// Every member carries a default so that naming only the ones that matter
/// stays free of missing-initializer warnings.
struct ProcessInput {
/// The working directory of the program. It must be an absolute path to an
/// existing directory
std::filesystem::path directory{std::filesystem::current_path()};
/// The entire environment of the program, replacing rather than extending the
/// environment of the caller. Without a value, the caller's environment is
/// inherited as it stands. The referenced names and values must outlive the
/// call
std::optional<std::map<std::string_view, std::string_view>> environment{
std::nullopt};
/// The bytes to feed the program on its standard input. The referenced buffer
/// must outlive the call
std::string_view standard_input{};
};

/// @ingroup process
///
/// Spawn a program piping its output to the current stdio configuration.
/// The directory parameter specifies the working directory for the spawned
/// process. It must be an absolute path to an existing directory.
///
/// ```cpp
/// #include <sourcemeta/core/process.h>
Expand All @@ -41,15 +62,12 @@ namespace sourcemeta::core {
SOURCEMETA_CORE_PROCESS_EXPORT
auto spawn(const std::string &program,
std::initializer_list<std::string_view> arguments,
const std::filesystem::path &directory =
std::filesystem::current_path()) -> int;
const ProcessInput &input = {}) -> int;

/// @ingroup process
///
/// Spawn a program piping its output to the current stdio configuration.
/// This overload accepts a span for dynamic argument lists.
/// The directory parameter specifies the working directory for the spawned
/// process. It must be an absolute path to an existing directory.
///
/// ```cpp
/// #include <sourcemeta/core/process.h>
Expand All @@ -62,10 +80,60 @@ auto spawn(const std::string &program,
/// assert(exit_code == 0);
/// ```
SOURCEMETA_CORE_PROCESS_EXPORT
auto spawn(
const std::string &program, std::span<const std::string_view> arguments,
const std::filesystem::path &directory = std::filesystem::current_path())
-> int;
auto spawn(const std::string &program,
std::span<const std::string_view> arguments,
const ProcessInput &input = {}) -> int;

/// @ingroup process
/// The result of running a program while capturing what it writes.
struct ProcessOutput {
/// The code the program exited with, or no value if it terminated abnormally,
/// such as by a signal
std::optional<int> exit_code{std::nullopt};
/// Everything the program wrote to its standard output
std::string standard_output{};
/// Everything the program wrote to its standard error
std::string standard_error{};
};

/// @ingroup process
///
/// Spawn a program, feeding it the given input and capturing both of its output
/// streams in full.
///
/// ```cpp
/// #include <sourcemeta/core/process.h>
/// #include <cassert>
///
/// const auto result{sourcemeta::core::spawn_and_capture("echo", {"foo"})};
/// assert(result.exit_code.value() == 0);
/// assert(result.standard_output == "foo\n");
/// ```
SOURCEMETA_CORE_PROCESS_EXPORT
auto spawn_and_capture(const std::string &program,
std::initializer_list<std::string_view> arguments,
const ProcessInput &input = {}) -> ProcessOutput;

/// @ingroup process
///
/// Spawn a program, feeding it the given input and capturing both of its output
/// streams in full. This overload accepts a span for dynamic argument lists.
///
/// ```cpp
/// #include <sourcemeta/core/process.h>
/// #include <vector>
/// #include <string_view>
/// #include <cassert>
///
/// std::vector<std::string_view> arguments{"foo", "bar"};
/// const auto result{sourcemeta::core::spawn_and_capture("echo", arguments)};
/// assert(result.exit_code.value() == 0);
/// assert(result.standard_output == "foo bar\n");
/// ```
SOURCEMETA_CORE_PROCESS_EXPORT
auto spawn_and_capture(const std::string &program,
std::span<const std::string_view> arguments,
const ProcessInput &input = {}) -> ProcessOutput;

} // namespace sourcemeta::core

Expand Down
Loading
Loading