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 @@ + 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)), + ); + + 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', + "[\nfoo\n]\nbar = baz\n" => + '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,62 @@ /** + * Decode an INI string. + * + * @param string + * @return mixed + */ +function phutil_ini_decode($string) { + $trap = new PhutilErrorTrap(); + + if (function_exists('parse_ini_string')) { + $results = @parse_ini_string($string, true, INI_SCANNER_RAW); + } else { + $tmp = new TempFile(); + Filesystem::writeFile($tmp, $string); + $full_path = (string)$tmp; + + $results = @parse_ini_file($full_path, true, INI_SCANNER_RAW); + } + + if ($results === false) { + throw new PhutilINIParserException(trim($trap->getErrorsAsString())); + } + + foreach ($results as $section => $result) { + switch ($result) { + case 'false': + $results[$section] = false; + break; + + case 'true': + $results[$section] = true; + break; + } + + if (!is_array($result)) { + continue; + } + + foreach ($result as $key => $value) { + switch ($value) { + case 'false': + $results[$section][$key] = false; + break; + + case 'true': + $results[$section][$key] = true; + break; + } + } + } + + $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