250 lines
9.2 KiB
PHP
250 lines
9.2 KiB
PHP
<?php
|
|
|
|
namespace Dotenv\Loader;
|
|
|
|
use Dotenv\Exception\InvalidFileException;
|
|
use Dotenv\Regex\Regex;
|
|
use Dotenv\Result\Error;
|
|
use Dotenv\Result\Success;
|
|
use RuntimeException;
|
|
|
|
class Parser
|
|
{
|
|
const INITIAL_STATE = 0;
|
|
const UNQUOTED_STATE = 1;
|
|
const SINGLE_QUOTED_STATE = 2;
|
|
const DOUBLE_QUOTED_STATE = 3;
|
|
const ESCAPE_SEQUENCE_STATE = 4;
|
|
const WHITESPACE_STATE = 5;
|
|
const COMMENT_STATE = 6;
|
|
|
|
/**
|
|
* Parse the given environment variable entry into a name and value.
|
|
*
|
|
* @param string $entry
|
|
*
|
|
* @throws \Dotenv\Exception\InvalidFileException
|
|
*
|
|
* @return array{string,\Dotenv\Loader\Value|null}
|
|
*/
|
|
public static function parse($entry)
|
|
{
|
|
list($name, $value) = self::splitStringIntoParts($entry);
|
|
|
|
return [self::parseName($name), self::parseValue($value)];
|
|
}
|
|
|
|
/**
|
|
* Split the compound string into parts.
|
|
*
|
|
* @param string $line
|
|
*
|
|
* @throws \Dotenv\Exception\InvalidFileException
|
|
*
|
|
* @return array{string,string|null}
|
|
*/
|
|
private static function splitStringIntoParts($line)
|
|
{
|
|
$name = $line;
|
|
$value = null;
|
|
|
|
if (strpos($line, '=') !== false) {
|
|
list($name, $value) = array_map('trim', explode('=', $line, 2));
|
|
}
|
|
|
|
if ($name === '') {
|
|
throw new InvalidFileException(
|
|
self::getErrorMessage('an unexpected equals', $line)
|
|
);
|
|
}
|
|
|
|
return [$name, $value];
|
|
}
|
|
|
|
/**
|
|
* Strips quotes and the optional leading "export " from the variable name.
|
|
*
|
|
* @param string $name
|
|
*
|
|
* @throws \Dotenv\Exception\InvalidFileException
|
|
*
|
|
* @return string
|
|
*/
|
|
private static function parseName($name)
|
|
{
|
|
$name = trim(str_replace(['export ', '\'', '"'], '', $name));
|
|
|
|
if (!self::isValidName($name)) {
|
|
throw new InvalidFileException(
|
|
self::getErrorMessage('an invalid name', $name)
|
|
);
|
|
}
|
|
|
|
return $name;
|
|
}
|
|
|
|
/**
|
|
* Is the given variable name valid?
|
|
*
|
|
* @param string $name
|
|
*
|
|
* @return bool
|
|
*/
|
|
private static function isValidName($name)
|
|
{
|
|
return Regex::match('~\A[a-zA-Z0-9_.]+\z~', $name)->success()->getOrElse(0) === 1;
|
|
}
|
|
|
|
/**
|
|
* Strips quotes and comments from the environment variable value.
|
|
*
|
|
* @param string|null $value
|
|
*
|
|
* @throws \Dotenv\Exception\InvalidFileException
|
|
*
|
|
* @return \Dotenv\Loader\Value|null
|
|
*/
|
|
private static function parseValue($value)
|
|
{
|
|
if ($value === null) {
|
|
return null;
|
|
}
|
|
|
|
if (trim($value) === '') {
|
|
return Value::blank();
|
|
}
|
|
|
|
$result = array_reduce(str_split($value), function ($data, $char) use ($value) {
|
|
return self::processChar($data[1], $char)->mapError(function ($err) use ($value) {
|
|
throw new InvalidFileException(
|
|
self::getErrorMessage($err, $value)
|
|
);
|
|
})->mapSuccess(function ($val) use ($data) {
|
|
return [$data[0]->append($val[0], $val[1]), $val[2]];
|
|
})->getSuccess();
|
|
}, [Value::blank(), self::INITIAL_STATE]);
|
|
|
|
if (in_array($result[1], [self::SINGLE_QUOTED_STATE, self::DOUBLE_QUOTED_STATE, self::ESCAPE_SEQUENCE_STATE], true)) {
|
|
throw new InvalidFileException(
|
|
self::getErrorMessage('a missing closing quote', $value)
|
|
);
|
|
}
|
|
|
|
return $result[0];
|
|
}
|
|
|
|
/**
|
|
* Process the given character.
|
|
*
|
|
* @param int $state
|
|
* @param string $char
|
|
*
|
|
* @return \Dotenv\Result\Result<array{string,bool,int},string>
|
|
*/
|
|
private static function processChar($state, $char)
|
|
{
|
|
switch ($state) {
|
|
case self::INITIAL_STATE:
|
|
if ($char === '\'') {
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Success::create(['', false, self::SINGLE_QUOTED_STATE]);
|
|
} elseif ($char === '"') {
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Success::create(['', false, self::DOUBLE_QUOTED_STATE]);
|
|
} elseif ($char === '#') {
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Success::create(['', false, self::COMMENT_STATE]);
|
|
} elseif ($char === '$') {
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Success::create([$char, true, self::UNQUOTED_STATE]);
|
|
} else {
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Success::create([$char, false, self::UNQUOTED_STATE]);
|
|
}
|
|
case self::UNQUOTED_STATE:
|
|
if ($char === '#') {
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Success::create(['', false, self::COMMENT_STATE]);
|
|
} elseif (ctype_space($char)) {
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Success::create(['', false, self::WHITESPACE_STATE]);
|
|
} elseif ($char === '$') {
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Success::create([$char, true, self::UNQUOTED_STATE]);
|
|
} else {
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Success::create([$char, false, self::UNQUOTED_STATE]);
|
|
}
|
|
case self::SINGLE_QUOTED_STATE:
|
|
if ($char === '\'') {
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Success::create(['', false, self::WHITESPACE_STATE]);
|
|
} else {
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Success::create([$char, false, self::SINGLE_QUOTED_STATE]);
|
|
}
|
|
case self::DOUBLE_QUOTED_STATE:
|
|
if ($char === '"') {
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Success::create(['', false, self::WHITESPACE_STATE]);
|
|
} elseif ($char === '\\') {
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Success::create(['', false, self::ESCAPE_SEQUENCE_STATE]);
|
|
} elseif ($char === '$') {
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Success::create([$char, true, self::DOUBLE_QUOTED_STATE]);
|
|
} else {
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Success::create([$char, false, self::DOUBLE_QUOTED_STATE]);
|
|
}
|
|
case self::ESCAPE_SEQUENCE_STATE:
|
|
if ($char === '"' || $char === '\\') {
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Success::create([$char, false, self::DOUBLE_QUOTED_STATE]);
|
|
} elseif ($char === '$') {
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Success::create([$char, false, self::DOUBLE_QUOTED_STATE]);
|
|
} elseif (in_array($char, ['f', 'n', 'r', 't', 'v'], true)) {
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Success::create([stripcslashes('\\'.$char), false, self::DOUBLE_QUOTED_STATE]);
|
|
} else {
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Error::create('an unexpected escape sequence');
|
|
}
|
|
case self::WHITESPACE_STATE:
|
|
if ($char === '#') {
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Success::create(['', false, self::COMMENT_STATE]);
|
|
} elseif (!ctype_space($char)) {
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Error::create('unexpected whitespace');
|
|
} else {
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Success::create(['', false, self::WHITESPACE_STATE]);
|
|
}
|
|
case self::COMMENT_STATE:
|
|
/** @var \Dotenv\Result\Result<array{string,bool,int},string> */
|
|
return Success::create(['', false, self::COMMENT_STATE]);
|
|
default:
|
|
throw new RuntimeException('Parser entered invalid state.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a friendly error message.
|
|
*
|
|
* @param string $cause
|
|
* @param string $subject
|
|
*
|
|
* @return string
|
|
*/
|
|
private static function getErrorMessage($cause, $subject)
|
|
{
|
|
return sprintf(
|
|
'Failed to parse dotenv file due to %s. Failed at [%s].',
|
|
$cause,
|
|
strtok($subject, "\n")
|
|
);
|
|
}
|
|
}
|