Skip to content

Commit

Permalink
Execute Exec manually instead of through shell
Browse files Browse the repository at this point in the history
This is safer and it removes the middle man (/bin/sh). This change was
made because the latest specification is missing the [ character in the
reserved characters section of the Exec key specification. [ has special
meaning in dash, but the spec doesn't require it to be quoted. Passing
Exec to the shell is incorrect because of this (the shell approach could
be fixed, but this solution is better).
  • Loading branch information
meator committed Jun 22, 2024
1 parent 6910bcb commit 7efae31
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 37 deletions.
64 changes: 60 additions & 4 deletions src/CMDLineAssembler.cc
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,68 @@ std::string sq_quote(std::string_view input) {
}
}

std::string prepend_exec(std::string_view exec_key) {
return "exec " + std::string(exec_key);
std::vector<std::string> convert_exec_to_command(std::string_view exec_key) {
std::vector<std::string> result;

std::string curr;
bool in_quotes = false;
bool escaping = false;

for (char ch : exec_key) {
if (escaping) {
switch (ch) {
case '"':
curr += '"';
break;
case '`':
curr += '`';
break;
case '$':
curr += '$';
break;
case '\\':
curr += '\\';
break;
}
escaping = false;
} else {
if (in_quotes) {
switch (ch) {
case '"':
in_quotes = false;
break;
case '\\':
escaping = true;
break;
default:
curr += ch;
break;
}
} else {
switch (ch) {
case '"':
in_quotes = true;
break;
case ' ':
result.push_back(std::move(curr));
curr.clear();
break;
default:
curr += ch;
break;
}
}
}
}

if (!curr.empty())
result.push_back(std::move(curr));

return result;
}

std::vector<std::string> wrap_exec_in_shell(std::string_view exec_key) {
return {"/bin/sh", "-c", "--", std::string(exec_key)};
std::vector<std::string> wrap_cmdstring_in_shell(std::string_view cmdstring) {
return {"/bin/sh", "-c", "--", std::string(cmdstring)};
}

std::string convert_argv_to_string(const std::vector<std::string> &command) {
Expand Down
18 changes: 6 additions & 12 deletions src/CMDLineAssembler.hh
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,14 @@ namespace CMDLineAssembly
// of '' literally.
std::string sq_quote(std::string_view);

// Some shells automatically exec() the last command in the
// command_string passed in by sh -c but some do not. For
// example bash does this but dash doesn't. Prepending "exec " to
// the command ensures that the shell will get replaced. Custom
// commands might contain complicated expressions so exec()ing them
// might not be a good idea. Desktop files can contain only a single
// command in Exec so using the exec shell builtin is safe.
// See https://github.com/enkore/j4-dmenu-desktop/issues/135
std::string prepend_exec(std::string_view exec_key);
// Split the Exec key of desktop files to an array of arguments (+ the primary
// executable) according to the XDG specification.
std::vector<std::string> convert_exec_to_command(std::string_view exec_key);

// Pass the Exec key through a shell
// `true` becomes `{"/bin/sh", "-c", "--", "true"}`. exec_key is quoted
// Pass the command string through a shell
// `true` becomes `{"/bin/sh", "-c", "--", "true"}`. cmdstring is quoted
// properly.
std::vector<std::string> wrap_exec_in_shell(std::string_view exec_key);
std::vector<std::string> wrap_cmdstring_in_shell(std::string_view cmdstring);

// Convert raw argv list to a std::string. This is used for i3 IPC mode and in
// --wrapper.
Expand Down
62 changes: 41 additions & 21 deletions src/main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -665,18 +665,20 @@ class NormalExecutable final : public BaseExecutable
const RunPhase::CommandRetrievalLoop::CommandInfo &command_info,
const std::string &wrapper, const std::string &terminal,
CMDLineTerm::term_assembler term_assembler) {
std::string raw_command = command_info.raw_command;
std::vector<std::string> command_array;
if (command_info.is_custom)
raw_command = CMDLineAssembly::prepend_exec(raw_command);
stringlist_t processed_argv =
CMDLineAssembly::wrap_exec_in_shell(raw_command);
command_array = CMDLineAssembly::wrap_cmdstring_in_shell(
command_info.raw_command);
else
command_array = CMDLineAssembly::convert_exec_to_command(
command_info.raw_command);
if (!wrapper.empty())
processed_argv = CMDLineAssembly::wrap_command_in_wrapper(
processed_argv, wrapper);
command_array = CMDLineAssembly::wrap_command_in_wrapper(
command_array, wrapper);
if (command_info.is_terminal)
processed_argv =
term_assembler(processed_argv, terminal, command_info.app_name);
return processed_argv;
command_array =
term_assembler(command_array, terminal, command_info.app_name);
return command_array;
}

void execute(const RunPhase::CommandRetrievalLoop::CommandInfo
Expand Down Expand Up @@ -748,20 +750,38 @@ class I3Executable final : public BaseExecutable

void execute(const RunPhase::CommandRetrievalLoop::CommandInfo
&command_info) override {
std::string command = command_info.raw_command;
if (command_info.is_custom)
command = CMDLineAssembly::prepend_exec(command);

if (!command_info.path.empty())
command =
"cd " + CMDLineAssembly::sq_quote(command) + " && " + command;
stringlist_t processed_argv =
CMDLineAssembly::wrap_exec_in_shell(command);
std::vector<std::string> command_array;
if (command_info.is_custom) {
std::string command = command_info.raw_command;
if (!command_info.path.empty())
command = "cd " + CMDLineAssembly::sq_quote(command_info.path) +
" && " + command;
command_array = CMDLineAssembly::wrap_cmdstring_in_shell(command);
} else {
command_array = CMDLineAssembly::convert_exec_to_command(
command_info.raw_command);
if (!command_info.path.empty()) {
// This is kinda convoluted to be honest. chdir() can't be used
// here, because I3 is responsible for the execution
// environment. I believe that the most portable way to chdir is
// to wrap command_array in shell and prepend the part wrapped
// in shell with "cd <directory> && exec <command>".
command_array = CMDLineAssembly::wrap_cmdstring_in_shell(
"cd " + CMDLineAssembly::sq_quote(command_info.path) +
" && exec " +
CMDLineAssembly::convert_argv_to_string(command_array));
}
}
// wrapper and i3 mode are mutally exclusive, no need to handle it here.
/*
if (!this->wrapper.empty())
...
*/
if (command_info.is_terminal)
processed_argv = this->term_assembler(
processed_argv, this->terminal, command_info.app_name);
command_array = this->term_assembler(command_array, this->terminal,
command_info.app_name);
I3Interface::exec(
CMDLineAssembly::convert_argv_to_string(processed_argv),
CMDLineAssembly::convert_argv_to_string(command_array),
this->i3_ipc_path);
}

Expand Down

0 comments on commit 7efae31

Please # to comment.