Skip to content

[3.1] Fail parser on preg error #308

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions src/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Dotenv;

use Dotenv\Exception\InvalidFileException;
use Dotenv\Regex\Regex;

class Parser
{
Expand Down Expand Up @@ -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();
}

/**
Expand Down
82 changes: 82 additions & 0 deletions src/Regex/Error.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

namespace Dotenv\Regex;

use PhpOption\None;
use PhpOption\Some;

class Error extends Result
{
/**
* @var string
*/
private $value;

/**
* Internal constructor for an error value.
*
* @param string $value
*
* @return void
*/
private function __construct($value)
{
$this->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));
}
}
54 changes: 54 additions & 0 deletions src/Regex/Regex.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace Dotenv\Regex;

use PhpOption\Option;

class Regex
{
/**
* Perform a preg replace, failing with an exception.
*
* @param string $pattern
* @param string $repalcement
* @param string $subject
*
* @return \Dotenv\Regex\Result
*/
public static function pregReplace($pattern, $replacement, $subject)
{
$result = (string) @preg_replace($pattern, $replacement, $subject);

if (($e = preg_last_error()) !== PREG_NO_ERROR) {
return Error::create(self::lookupError($e));
}

return Success::create($result);
}

/**
* Lookup the preg error code.
*
* @param int $code
*
* @return string
*/
private static function lookupError($code)
{
return Option::fromValue(get_defined_constants(true))
->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');
}
}
58 changes: 58 additions & 0 deletions src/Regex/Result.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace Dotenv\Regex;

abstract class Result
{
/**
* Get the success option value.
*
* @return \PhpOption\Option
*/
abstract public function success();

/**
* Get the error value, if possible.
*
* @return string
*/
public function getSuccess()
{
return $this->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);
}
82 changes: 82 additions & 0 deletions src/Regex/Success.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

namespace Dotenv\Regex;

use PhpOption\None;
use PhpOption\Some;

class Success extends Result
{
/**
* @var string
*/
private $value;

/**
* Internal constructor for a success value.
*
* @param string $value
*
* @return void
*/
private function __construct($value)
{
$this->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);
}
}
20 changes: 20 additions & 0 deletions tests/Dotenv/ParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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"'));
Expand Down Expand Up @@ -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"');
}
}
58 changes: 58 additions & 0 deletions tests/Dotenv/ResultTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

use DotEnv\Regex\Error;
use DotEnv\Regex\Success;
use PHPUnit\Framework\TestCase;

class ResultTest extends TestCase
{
public function testSuccessValue()
{
$this->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();
}
}