Skip to content

Commit

Permalink
fix(shell): fix windows support (#269)
Browse files Browse the repository at this point in the history
Signed-off-by: azjezz <azjezz@protonmail.com>
  • Loading branch information
azjezz committed Nov 10, 2021
1 parent de6f130 commit ebbc67d
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 7 deletions.
2 changes: 1 addition & 1 deletion docs/component/shell.md
Expand Up @@ -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)


86 changes: 80 additions & 6 deletions src/Psl/Shell/execute.php
Expand Up @@ -6,6 +6,8 @@

use Psl\Dict;
use Psl\Env;
use Psl\Regex;
use Psl\SecureRandom;
use Psl\Str;
use Psl\Vec;

Expand Down Expand Up @@ -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<array-key, string> $m
*
* @return string
*/
static function (array $m) use (
&$environment,
&$variable_cache,
&$variable_count,
$identifier
): string {
if (!isset($m[1])) {
return $m[0];
}

/** @var array<string, string> $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<string, string> $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)) {
Expand Down

0 comments on commit ebbc67d

Please sign in to comment.