diff --git a/src/xsprintf/__tests__/PhutilCsprintfTestCase.php b/src/xsprintf/__tests__/PhutilCsprintfTestCase.php index 5c4e78c..05a26c7 100644 --- a/src/xsprintf/__tests__/PhutilCsprintfTestCase.php +++ b/src/xsprintf/__tests__/PhutilCsprintfTestCase.php @@ -1,79 +1,96 @@ assertEqual('ab', (string)csprintf('%R', 'ab')); - - // For arguments which have any characters which are not safe in some - // context, %R should apply standard escaping. - $this->assertFalse('a b' === (string)csprintf('%R', 'a b')); + $inputs = array( + // For arguments comprised of only characters which are safe in any + // context, %R this should avoid adding quotes. + 'ab' => true, + + // For arguments which have any characters which are not safe in some + // context, %R should apply standard escaping. + 'a b' => false, + + + 'http://domain.com/path/' => true, + 'svn+ssh://domain.com/path/' => true, + '`rm -rf`' => false, + '$VALUE' => false, + ); + + foreach ($inputs as $input => $expect_same) { + $actual = (string)csprintf('%R', $input); + if ($expect_same) { + $this->assertEqual($input, $actual); + } else { + $this->assertFalse($input === $actual); + } + } } public function testPowershell() { $cmd = csprintf('%s', "\n"); $cmd->setEscapingMode(PhutilCommandString::MODE_POWERSHELL); $this->assertEqual( '"`n"', (string)$cmd); } public function testNoPowershell() { if (!phutil_is_windows()) { $cmd = csprintf('%s', '#'); $cmd->setEscapingMode(PhutilCommandString::MODE_DEFAULT); $this->assertEqual( '\'#\'', (string)$cmd); } } public function testPasswords() { // Normal "%s" doesn't do anything special. $command = csprintf('echo %s', 'hunter2trustno1'); $this->assertTrue(strpos($command, 'hunter2trustno1') !== false); // "%P" takes a PhutilOpaqueEnvelope. $caught = null; try { csprintf('echo %P', 'hunter2trustno1'); } catch (Exception $ex) { $caught = $ex; } $this->assertTrue($caught instanceof InvalidArgumentException); // "%P" masks the provided value. $command = csprintf('echo %P', new PhutilOpaqueEnvelope('hunter2trustno1')); $this->assertFalse(strpos($command, 'hunter2trustno1')); // Executing the command works as expected. list($out) = execx('%C', $command); $this->assertTrue(strpos($out, 'hunter2trustno1') !== false); } public function testEscapingIsRobust() { if (phutil_is_windows()) { $this->assertSkipped(pht("This test doesn't work on Windows.")); } // Escaping should be robust even when used to escape commands which take // other commands. list($out) = execx( 'sh -c %s', csprintf( 'sh -c %s', csprintf( 'sh -c %s', csprintf( 'echo %P', new PhutilOpaqueEnvelope('!@#$%^&*()'))))); $this->assertTrue(strpos($out, '!@#$%^&*()') !== false); } } diff --git a/src/xsprintf/csprintf.php b/src/xsprintf/csprintf.php index e7c5878..429fe44 100644 --- a/src/xsprintf/csprintf.php +++ b/src/xsprintf/csprintf.php @@ -1,138 +1,138 @@ $pos + 1) ? $pattern[$pos + 1] : null; $is_unmasked = !empty($userdata['unmasked']); if (empty($userdata['mode'])) { $mode = PhutilCommandString::MODE_DEFAULT; } else { $mode = $userdata['mode']; } if ($value instanceof PhutilCommandString) { if ($is_unmasked) { $value = $value->getUnmaskedString(); } else { $value = $value->getMaskedString(); } } switch ($type) { case 'L': // Remove the L. $pattern = substr_replace($pattern, '', $pos, 1); $length = strlen($pattern); $type = 's'; // Check that the value is a non-empty array. if (!is_array($value)) { throw new InvalidArgumentException( pht('Expected an array for %%L%s conversion.', $next)); } switch ($next) { case 's': $values = array(); foreach ($value as $val) { $values[] = csprintf('%s', $val); } $value = implode(' ', $values); break; case 'R': $values = array(); foreach ($value as $val) { $values[] = csprintf('%R', $val); } $value = implode(' ', $values); break; default: throw new XsprintfUnknownConversionException("%L{$next}"); } break; case 'R': - if (!preg_match('(^[a-zA-Z0-9:/@._-]+$)', $value)) { + if (!preg_match('(^[a-zA-Z0-9:/@._+-]+$)', $value)) { $value = PhutilCommandString::escapeArgument($value, $mode); } $type = 's'; break; case 's': $value = PhutilCommandString::escapeArgument($value, $mode); $type = 's'; break; case 'P': if (!($value instanceof PhutilOpaqueEnvelope)) { throw new InvalidArgumentException( pht('Expected %s for %%P conversion.', 'PhutilOpaqueEnvelope')); } if ($is_unmasked) { $value = $value->openEnvelope(); } else { $value = 'xxxxx'; } $value = PhutilCommandString::escapeArgument($value, $mode); $type = 's'; break; case 'C': $type = 's'; break; } $pattern[$pos] = $type; }