diff --git a/src/Parser.php b/src/Parser.php index e8cacfd8..f6cb0157 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -3,6 +3,7 @@ namespace Dotenv; use Dotenv\Exception\InvalidFileException; +use Dotenv\Regex\Regex; class Parser { @@ -154,9 +155,16 @@ private static function processQuotedValue($value) $quote ); - $value = (string) preg_replace($pattern, '$1', $value); - - return str_replace('\\\\', '\\', str_replace("\\$quote", $quote, $value)); + return Regex::pregReplace($pattern, '$1', $value) + ->mapSuccess(function ($str) use ($quote) { + return str_replace('\\\\', '\\', str_replace("\\$quote", $quote, $str)); + }) + ->mapError(function ($err) use ($value) { + throw new InvalidFileException( + self::getErrorMessage(sprintf('a quote parsing error (%s)', $err), $value) + ); + }) + ->getSuccess(); } /** diff --git a/src/Regex/Error.php b/src/Regex/Error.php new file mode 100644 index 00000000..6ad7b5d0 --- /dev/null +++ b/src/Regex/Error.php @@ -0,0 +1,82 @@ +value = $value; + } + + /** + * Create a new error value. + * + * @param string $value + * + * @return \Dotenv\Regex\Result + */ + public static function create($value) + { + return new self($value); + } + + /** + * Get the success option value. + * + * @return \PhpOption\Option + */ + public function success() + { + return None::create(); + } + + /** + * Map over the success value. + * + * @param callable $f + * + * @return \Dotenv\Regex\Result + */ + public function mapSuccess(callable $f) + { + return self::create($this->value); + } + + /** + * Get the error option value. + * + * @return \PhpOption\Option + */ + public function error() + { + return Some::create($this->value); + } + + /** + * Map over the error value. + * + * @param callable $f + * + * @return \Dotenv\Regex\Result + */ + public function mapError(callable $f) + { + return self::create($f($this->value)); + } +} diff --git a/src/Regex/Regex.php b/src/Regex/Regex.php new file mode 100644 index 00000000..5ab5b284 --- /dev/null +++ b/src/Regex/Regex.php @@ -0,0 +1,54 @@ +filter(function (array $consts) { + return isset($consts['pcre']) && defined('ARRAY_FILTER_USE_KEY'); + }) + ->map(function (array $consts) { + return array_filter($consts['pcre'], function ($msg) { + return substr($msg, -6) === '_ERROR'; + }, ARRAY_FILTER_USE_KEY); + }) + ->flatMap(function (array $errors) use ($code) { + return Option::fromValue( + array_search($code, $errors, true) + ); + }) + ->getOrElse('PREG_ERROR'); + } +} diff --git a/src/Regex/Result.php b/src/Regex/Result.php new file mode 100644 index 00000000..04638bb3 --- /dev/null +++ b/src/Regex/Result.php @@ -0,0 +1,58 @@ +success()->get(); + } + + /** + * Map over the success value. + * + * @param callable $f + * + * @return \Dotenv\Regex\Result + */ + abstract public function mapSuccess(callable $f); + + /** + * Get the error option value. + * + * @return \PhpOption\Option + */ + abstract public function error(); + + /** + * Get the error value, if possible. + * + * @return string + */ + public function getError() + { + return $this->error()->get(); + } + + /** + * Map over the error value. + * + * @param callable $f + * + * @return \Dotenv\Regex\Result + */ + abstract public function mapError(callable $f); +} diff --git a/src/Regex/Success.php b/src/Regex/Success.php new file mode 100644 index 00000000..47dbda0a --- /dev/null +++ b/src/Regex/Success.php @@ -0,0 +1,82 @@ +value = $value; + } + + /** + * Create a new success value. + * + * @param string $value + * + * @return \Dotenv\Regex\Result + */ + public static function create($value) + { + return new self($value); + } + + /** + * Get the success option value. + * + * @return \PhpOption\Option + */ + public function success() + { + return Some::create($this->value); + } + + /** + * Map over the success value. + * + * @param callable $f + * + * @return \Dotenv\Regex\Result + */ + public function mapSuccess(callable $f) + { + return self::create($f($this->value)); + } + + /** + * Get the error option value. + * + * @return \PhpOption\Option + */ + public function error() + { + return None::create(); + } + + /** + * Map over the error value. + * + * @param callable $f + * + * @return \Dotenv\Regex\Result + */ + public function mapError(callable $f) + { + return self::create($this->value); + } +} diff --git a/tests/Dotenv/ParserTest.php b/tests/Dotenv/ParserTest.php index dba2392b..94b3888c 100644 --- a/tests/Dotenv/ParserTest.php +++ b/tests/Dotenv/ParserTest.php @@ -15,6 +15,11 @@ public function testQuotesParse() $this->assertSame(['FOO', "BAR \n"], Parser::parse("FOO=\"BAR \n\"")); } + public function testWhitespaceParse() + { + $this->assertSame(['FOO', "\n"], Parser::parse("FOO=\"\n\"")); + } + public function testExportParse() { $this->assertSame(['FOO', 'bar baz'], Parser::parse('export FOO="bar baz"')); @@ -46,4 +51,19 @@ public function testParseInvalidName() { Parser::parse('FOO_ASD!=BAZ'); } + + /** + * @expectedException \Dotenv\Exception\InvalidFileException + * @expectedExceptionMessage Failed to parse dotenv file due to a quote parsing error (PREG_ + */ + public function testParserFailsWithException() + { + $limit = (int) ini_get('pcre.backtrack_limit'); + + if ($limit > 1000000) { + $this->markTestSkipped('System pcre.backtrack_limit too large.'); + } + + Parser::parse('FOO_BAD="iiiiviiiixiiiiviiii\\n"'); + } } diff --git a/tests/Dotenv/ResultTest.php b/tests/Dotenv/ResultTest.php new file mode 100644 index 00000000..16e5cd3c --- /dev/null +++ b/tests/Dotenv/ResultTest.php @@ -0,0 +1,58 @@ +assertTrue(Success::create('foo')->error()->isEmpty()); + $this->assertTrue(Success::create('foo')->success()->isDefined()); + $this->assertEquals('foo', Success::create('foo')->getSuccess()); + } + + public function testSuccessMapping() + { + $r = Success::create('foo') + ->mapSuccess('strtoupper') + ->mapError('ucfirst'); + + $this->assertEquals('FOO', $r->getSuccess()); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage None has no value. + */ + public function testSuccessFail() + { + Success::create('foo')->getError(); + } + + public function testErrorValue() + { + $this->assertTrue(Error::create('foo')->error()->isDefined()); + $this->assertTrue(Error::create('foo')->success()->isEmpty()); + $this->assertEquals('foo', Error::create('foo')->getError()); + } + + public function testErrorMapping() + { + $r = Error::create('foo') + ->mapSuccess('strtoupper') + ->mapError('ucfirst'); + + $this->assertEquals('Foo', $r->getError()); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage None has no value. + */ + public function testErrorFail() + { + Error::create('foo')->getSuccess(); + } +}