Changeset View
Changeset View
Standalone View
Standalone View
src/xsprintf/PhutilCommandString.php
| <?php | <?php | ||||
| final class PhutilCommandString extends Phobject { | final class PhutilCommandString extends Phobject { | ||||
| private $argv; | private $argv; | ||||
| private $escapingMode = false; | private $escapingMode = false; | ||||
| const MODE_DEFAULT = 'default'; | const MODE_DEFAULT = 'default'; | ||||
| const MODE_WIN_PASSTHRU = 'winargv'; | |||||
| const MODE_POWERSHELL = 'powershell'; | const MODE_POWERSHELL = 'powershell'; | ||||
| public function __construct(array $argv) { | public function __construct(array $argv) { | ||||
| $this->argv = $argv; | $this->argv = $argv; | ||||
| $this->escapingMode = self::MODE_DEFAULT; | $this->escapingMode = self::MODE_DEFAULT; | ||||
| // This makes sure we throw immediately if there are errors in the | // This makes sure we throw immediately if there are errors in the | ||||
| Show All 26 Lines | return xsprintf( | ||||
| 'mode' => $this->escapingMode, | 'mode' => $this->escapingMode, | ||||
| ), | ), | ||||
| $this->argv); | $this->argv); | ||||
| } | } | ||||
| public static function escapeArgument($value, $mode) { | public static function escapeArgument($value, $mode) { | ||||
| switch ($mode) { | switch ($mode) { | ||||
| case self::MODE_DEFAULT: | case self::MODE_DEFAULT: | ||||
| return escapeshellarg($value); | return phutil_is_windows() ? self::escapeWindowsCMD($value) : escapeshellarg($value); | ||||
hach-que: There should be `MODE_WIN_CMD` to force Windows CMD escaping for e.g. when Drydock is executing… | |||||
Not Done Inline ActionsGood point. That said where should I change to make Drydock use that mode? Also in that case does MODE_POWERSHELL become obsolete? BYK: Good point. That said where should I change to make Drydock use that mode? Also in that case… | |||||
Not Done Inline ActionsNo need to change Drydock. Adding a specific mode will just future proof this code. hach-que: No need to change Drydock. Adding a specific mode will just future proof this code. | |||||
Not Done Inline ActionsDropping CMD now (apart from the other breakage) will mean we have to come back and add it later, because you can't remotely execute without cmd. hach-que: Dropping CMD now (apart from the other breakage) will mean we have to come back and add it… | |||||
Not Done Inline Actions
I'm not sure if I understand what is behind this. Can you elaborate? BYK: > because you can't remotely execute without cmd.
I'm not sure if I understand what is behind… | |||||
| case self::MODE_POWERSHELL: | case self::MODE_POWERSHELL: | ||||
| return self::escapePowershell($value); | return self::escapePowershell($value); | ||||
| case self::MODE_WIN_PASSTHRU: | |||||
| return self::escapeWindowsArgv($value); | |||||
| default: | default: | ||||
| throw new Exception(pht('Unknown escaping mode!')); | throw new Exception(pht('Unknown escaping mode!')); | ||||
| } | } | ||||
| } | } | ||||
| /** | |||||
| * Escapes a single argument to be glued together and passed into | |||||
| * CreateProcess on Windows through `proc_open`. | |||||
| * | |||||
| * Adapted from https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ | |||||
| * | |||||
| * @param string The argument to be escaped | |||||
| * @result string Escaped argument that can be used in a CreateProcess call | |||||
| */ | |||||
| public static function escapeWindowsArgv($value) { | |||||
| // Don't quote unless we actually need to do so hopefully | |||||
| // avoid problems if programs won't parse quotes properly | |||||
| if ($value && !preg_match('/["[:space:]]/', $value)) { | |||||
| return $value; | |||||
| } | |||||
| $result = '"'; | |||||
| $len = strlen($value); | |||||
| for ($i = 0; $i < $len; $i++) { | |||||
| $numBackslashes = 0; | |||||
| while ($i < $len && $value[$i] == '\\') { | |||||
| $i++; | |||||
| $numBackslashes++; | |||||
| } | |||||
| if ($i == $len) { | |||||
| // Escape all backslashes, but let the terminating | |||||
| // double quotation mark we add below be interpreted | |||||
| // as a metacharacter. | |||||
| $result .= str_repeat('\\', $numBackslashes * 2); | |||||
| break; | |||||
| } elseif ($value[$i] == '"') { | |||||
| // Escape all backslashes and the following double quotation mark. | |||||
| $result .= str_repeat('\\', $numBackslashes * 2 + 1); | |||||
| $result .= $value[$i]; | |||||
| } else { | |||||
| // Backslashes aren't special here. | |||||
| $result .= str_repeat('\\', $numBackslashes); | |||||
| $result .= $value[$i]; | |||||
| } | |||||
| } | |||||
| $result .= '"'; | |||||
| return $result; | |||||
| } | |||||
| /** | |||||
| * Escapes all CMD metacharacters with a `^`. | |||||
| * | |||||
| * Adapted from https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ | |||||
| * | |||||
| * @param string The argument to be escaped | |||||
| * @result string Escaped argument that can be used in CMD.exe | |||||
| */ | |||||
| private static function escapeWindowsCMD($value) { | |||||
| // Make sure this is CreateProcess-safe first | |||||
| $value = self::escapeWindowsArgv($value); | |||||
| // Now prefix all CMD meta characters with a `^` to escape them | |||||
| return preg_replace('/[()%!^"<>&|]/', '^$0', $value); | |||||
| } | |||||
| private static function escapePowershell($value) { | private static function escapePowershell($value) { | ||||
| // These escape sequences are from http://ss64.com/ps/syntax-esc.html | // These escape sequences are from http://ss64.com/ps/syntax-esc.html | ||||
| // Replace backticks first. | // Replace backticks first. | ||||
| $value = str_replace('`', '``', $value); | $value = str_replace('`', '``', $value); | ||||
| // Now replace other required notations. | // Now replace other required notations. | ||||
| $value = str_replace("\0", '`0', $value); | $value = str_replace("\0", '`0', $value); | ||||
| Show All 19 Lines | |||||
There should be MODE_WIN_CMD to force Windows CMD escaping for e.g. when Drydock is executing a command on a remote Windows host (in this scenario, phutil_is_windows returns false because the Phabricator host is Linux).