diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -179,6 +179,7 @@ 'PhutilHangForeverDaemon' => 'daemon/torture/PhutilHangForeverDaemon.php', 'PhutilHelpArgumentWorkflow' => 'parser/argument/workflow/PhutilHelpArgumentWorkflow.php', 'PhutilHgsprintfTestCase' => 'xsprintf/__tests__/PhutilHgsprintfTestCase.php', + 'PhutilINIParserException' => 'parser/exception/PhutilINIParserException.php', 'PhutilIPAddress' => 'ip/PhutilIPAddress.php', 'PhutilIPAddressTestCase' => 'ip/__tests__/PhutilIPAddressTestCase.php', 'PhutilInRequestKeyValueCache' => 'cache/PhutilInRequestKeyValueCache.php', @@ -409,6 +410,7 @@ 'phutil_get_library_root_for_path' => 'moduleutils/moduleutils.php', 'phutil_get_signal_name' => 'future/exec/execx.php', 'phutil_implode_html' => 'markup/render.php', + 'phutil_ini_decode' => 'utils/utils.php', 'phutil_is_hiphop_runtime' => 'utils/utils.php', 'phutil_is_utf8' => 'utils/utf8.php', 'phutil_is_utf8_slowly' => 'utils/utf8.php', @@ -604,6 +606,7 @@ 'PhutilHangForeverDaemon' => 'PhutilTortureTestDaemon', 'PhutilHelpArgumentWorkflow' => 'PhutilArgumentWorkflow', 'PhutilHgsprintfTestCase' => 'PhutilTestCase', + 'PhutilINIParserException' => 'Exception', 'PhutilIPAddress' => 'Phobject', 'PhutilIPAddressTestCase' => 'PhutilTestCase', 'PhutilInRequestKeyValueCache' => 'PhutilKeyValueCache', diff --git a/src/parser/exception/PhutilINIParserException.php b/src/parser/exception/PhutilINIParserException.php new file mode 100644 --- /dev/null +++ b/src/parser/exception/PhutilINIParserException.php @@ -0,0 +1,3 @@ +assertSkipped($ex->getMessage()); + } + + $valid_cases = array( + '' => array(), + 'foo=' => array('foo' => ''), + 'foo=bar' => array('foo' => 'bar'), + 'foo = bar' => array('foo' => 'bar'), + "foo = bar\n" => array('foo' => 'bar'), + "foo\nbar = baz" => array('bar' => 'baz'), + + "[foo]\nbar = baz" => array('foo' => array('bar' => 'baz')), + "[foo]\n[bar]\nbaz = foo" => array( + 'foo' => array(), + 'bar' => array('baz' => 'foo'), + ), + "[foo]\nbar = baz\n\n[bar]\nbaz = foo" => array( + 'foo' => array('bar' => 'baz'), + 'bar' => array('baz' => 'foo'), + ), + + "; Comment\n[foo]\nbar = baz" => array('foo' => array('bar' => 'baz')), + "# Comment\n[foo]\nbar = baz" => array('foo' => array('bar' => 'baz')), + + "foo = true\n[bar]\nbaz = false" + => array('foo' => true, 'bar' => array('baz' => false)), + "foo = 1\nbar = 1.234" => array('foo' => 1, 'bar' => 1.234), + 'x = {"foo": "bar"}' => array('x' => '{"foo": "bar"}'), + ); + + foreach ($valid_cases as $input => $expect) { + $result = phutil_ini_decode($input); + $this->assertEqual($expect, $result, 'phutil_ini_decode('.$input.')'); + } + + $invalid_cases = array( + '[' => + 'syntax error, unexpected $end, expecting \']\' in Unknown on line 1', + ); + + foreach ($invalid_cases as $input => $expect) { + $caught = null; + try { + phutil_ini_decode($input); + } catch (Exception $ex) { + $caught = $ex; + } + $this->assertTrue($caught instanceof PhutilINIParserException); + $this->assertEqual($expect, $caught->getMessage()); + } + } + public function testCensorCredentials() { $cases = array( '' => '', diff --git a/src/utils/utils.php b/src/utils/utils.php --- a/src/utils/utils.php +++ b/src/utils/utils.php @@ -1058,6 +1058,69 @@ /** + * Decode an INI string. + * + * @param string + * @return mixed + */ +function phutil_ini_decode($string) { + $results = null; + $trap = new PhutilErrorTrap(); + + try { + if (!function_exists('parse_ini_string')) { + throw new PhutilMethodNotImplementedException( + pht( + '%s is not compatible with your version of PHP (%s). This function '. + 'is only supported on PHP versions newer than 5.3.0.', + __FUNCTION__, + phpversion())); + } + + $results = @parse_ini_string($string, true, INI_SCANNER_RAW); + + if ($results === false) { + throw new PhutilINIParserException(trim($trap->getErrorsAsString())); + } + + foreach ($results as $section => $result) { + if (!is_array($result)) { + // We JSON decode the value in ordering to perform the following + // conversions: + // + // - `'true'` => `true` + // - `'false'` => `false` + // - `'123'` => `123` + // - `'1.234'` => `1.234` + // + $result = json_decode($result, true); + + if ($result !== null && !is_array($result)) { + $results[$section] = $result; + } + + continue; + } + + foreach ($result as $key => $value) { + $value = json_decode($value, true); + + if ($value !== null && !is_array($value)) { + $results[$section][$key] = $value; + } + } + } + } catch (Exception $ex) { + $trap->destroy(); + throw $ex; + } + + $trap->destroy(); + return $results; +} + + +/** * Attempt to censor any plaintext credentials from a string. * * The major use case here is to censor usernames and passwords from command