From ebbc67d4ad71de1d1c308da67bbb844182f2718b Mon Sep 17 00:00:00 2001 From: Saif Eddin Gmati <29315886+azjezz@users.noreply.github.com> Date: Wed, 10 Nov 2021 20:56:56 +0100 Subject: [PATCH] fix(shell): fix windows support (#269) Signed-off-by: azjezz --- docs/component/shell.md | 2 +- src/Psl/Shell/execute.php | 86 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 81 insertions(+), 7 deletions(-) diff --git a/docs/component/shell.md b/docs/component/shell.md index 1417cf61..c523daa4 100644 --- a/docs/component/shell.md +++ b/docs/component/shell.md @@ -14,6 +14,6 @@ - [escape_argument](./../../src/Psl/Shell/escape_argument.php#L17) - [escape_command](./../../src/Psl/Shell/escape_command.php#L14) -- [execute](./../../src/Psl/Shell/execute.php#L37) +- [execute](./../../src/Psl/Shell/execute.php#L39) diff --git a/src/Psl/Shell/execute.php b/src/Psl/Shell/execute.php index 676f0e2b..7ab33a1a 100644 --- a/src/Psl/Shell/execute.php +++ b/src/Psl/Shell/execute.php @@ -6,6 +6,8 @@ use Psl\Dict; use Psl\Env; +use Psl\Regex; +use Psl\SecureRandom; use Psl\Str; use Psl\Vec; @@ -52,18 +54,90 @@ function execute( throw new Exception\PossibleAttackException('NULL byte detected.'); } - $descriptor = [ - 1 => ['pipe', 'w'], - 2 => ['pipe', 'w'], - ]; - $environment = Dict\merge(Env\get_vars(), $environment); $working_directory = $working_directory ?? Env\current_dir(); if (!is_dir($working_directory)) { throw new Exception\RuntimeException('$working_directory does not exist.'); } - $process = proc_open($commandline, $descriptor, $pipes, $working_directory, $environment); + $options = []; + // @codeCoverageIgnoreStart + if (PHP_OS_FAMILY === 'Windows') { + $variable_cache = []; + $variable_count = 0; + /** @psalm-suppress MissingThrowsDocblock */ + $identifier = 'PHP_STANDARD_LIBRARY_TMP_ENV_' . SecureRandom\string(6); + /** @psalm-suppress MissingThrowsDocblock */ + $commandline = Regex\replace_with( + $commandline, + '/"(?:([^"%!^]*+(?:(?:!LF!|"(?:\^[%!^])?+")[^"%!^]*+)++)|[^"]*+ )"/x', + /** + * @param array $m + * + * @return string + */ + static function (array $m) use ( + &$environment, + &$variable_cache, + &$variable_count, + $identifier + ): string { + if (!isset($m[1])) { + return $m[0]; + } + + /** @var array $variable_cache */ + if (isset($variable_cache[$m[0]])) { + /** @var string */ + return $variable_cache[$m[0]]; + } + + $value = $m[1]; + if (Str\Byte\contains($value, "\0")) { + $value = Str\Byte\replace($value, "\0", '?'); + } + + if (false === strpbrk($value, "\"%!\n")) { + return '"' . $value . '"'; + } + + $value = Str\Byte\replace_every( + $value, + ['!LF!' => "\n", '"^!"' => '!', '"^%"' => '%', '"^^"' => '^', '""' => '"'] + ); + $value = '"' . Regex\replace($value, '/(\\\\*)"/', '$1$1\\"') . '"'; + /** + * @psalm-suppress MixedAssignment + * @psalm-suppress MixedOperand + */ + $var = $identifier . ++$variable_count; + + /** + * @psalm-suppress MixedArrayAssignment + */ + $environment[$var] = $value; + + /** + * @psalm-suppress MixedArrayOffset + * @psalm-suppress MixedArrayAssignment + */ + return $variable_cache[$m[0]] = '!' . $var . '!'; + }, + ); + + $commandline = 'cmd /V:ON /E:ON /D /C (' . Str\Byte\replace($commandline, "\n", ' ') . ')'; + $options = [ + 'bypass_shell' => true, + 'blocking_pipes' => false, + ]; + } + // @codeCoverageIgnoreEnd + $descriptor = [ + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + /** @var array $environment */ + $process = proc_open($commandline, $descriptor, $pipes, $working_directory, $environment, $options); // @codeCoverageIgnoreStart // not sure how to replicate this, but it can happen \_o.o_/ if (!is_resource($process)) {