| 1 |
<?php |
| 2 |
|
| 3 |
/* |
| 4 |
* This file is part of the Symfony package. |
| 5 |
* |
| 6 |
* (c) Fabien Potencier <fabien@symfony.com> |
| 7 |
* |
| 8 |
* For the full copyright and license information, please view the LICENSE |
| 9 |
* file that was distributed with this source code. |
| 10 |
*/ |
| 11 |
namespace WindPressDeps\Symfony\Component\Yaml; |
| 12 |
|
| 13 |
use WindPressDeps\Symfony\Component\Yaml\Exception\DumpException; |
| 14 |
use WindPressDeps\Symfony\Component\Yaml\Exception\ParseException; |
| 15 |
use WindPressDeps\Symfony\Component\Yaml\Tag\TaggedValue; |
| 16 |
/** |
| 17 |
* Inline implements a YAML parser/dumper for the YAML inline syntax. |
| 18 |
* |
| 19 |
* @author Fabien Potencier <fabien@symfony.com> |
| 20 |
* |
| 21 |
* @internal |
| 22 |
*/ |
| 23 |
class Inline |
| 24 |
{ |
| 25 |
public const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+)"|\'([^\']*+(?:\'\'[^\']*+)*+)\')'; |
| 26 |
public static $parsedLineNumber = -1; |
| 27 |
public static $parsedFilename; |
| 28 |
private static $exceptionOnInvalidType = \false; |
| 29 |
private static $objectSupport = \false; |
| 30 |
private static $objectForMap = \false; |
| 31 |
private static $constantSupport = \false; |
| 32 |
public static function initialize(int $flags, ?int $parsedLineNumber = null, ?string $parsedFilename = null) |
| 33 |
{ |
| 34 |
self::$exceptionOnInvalidType = (bool) (Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE & $flags); |
| 35 |
self::$objectSupport = (bool) (Yaml::PARSE_OBJECT & $flags); |
| 36 |
self::$objectForMap = (bool) (Yaml::PARSE_OBJECT_FOR_MAP & $flags); |
| 37 |
self::$constantSupport = (bool) (Yaml::PARSE_CONSTANT & $flags); |
| 38 |
self::$parsedFilename = $parsedFilename; |
| 39 |
if (null !== $parsedLineNumber) { |
| 40 |
self::$parsedLineNumber = $parsedLineNumber; |
| 41 |
} |
| 42 |
} |
| 43 |
/** |
| 44 |
* Converts a YAML string to a PHP value. |
| 45 |
* |
| 46 |
* @param string|null $value A YAML string |
| 47 |
* @param int $flags A bit field of Yaml::PARSE_* constants to customize the YAML parser behavior |
| 48 |
* @param array $references Mapping of variable names to values |
| 49 |
* |
| 50 |
* @return mixed |
| 51 |
* |
| 52 |
* @throws ParseException |
| 53 |
*/ |
| 54 |
public static function parse(?string $value = null, int $flags = 0, array &$references = []) |
| 55 |
{ |
| 56 |
if (null === $value) { |
| 57 |
return ''; |
| 58 |
} |
| 59 |
self::initialize($flags); |
| 60 |
$value = trim($value); |
| 61 |
if ('' === $value) { |
| 62 |
return ''; |
| 63 |
} |
| 64 |
if (2 & (int) \ini_get('mbstring.func_overload')) { |
| 65 |
$mbEncoding = mb_internal_encoding(); |
| 66 |
mb_internal_encoding('ASCII'); |
| 67 |
} |
| 68 |
try { |
| 69 |
$i = 0; |
| 70 |
$tag = self::parseTag($value, $i, $flags); |
| 71 |
switch ($value[$i]) { |
| 72 |
case '[': |
| 73 |
$result = self::parseSequence($value, $flags, $i, $references); |
| 74 |
++$i; |
| 75 |
break; |
| 76 |
case '{': |
| 77 |
$result = self::parseMapping($value, $flags, $i, $references); |
| 78 |
++$i; |
| 79 |
break; |
| 80 |
default: |
| 81 |
$result = self::parseScalar($value, $flags, null, $i, \true, $references); |
| 82 |
} |
| 83 |
// some comments are allowed at the end |
| 84 |
if (preg_replace('/\s*#.*$/A', '', substr($value, $i))) { |
| 85 |
throw new ParseException(sprintf('Unexpected characters near "%s".', substr($value, $i)), self::$parsedLineNumber + 1, $value, self::$parsedFilename); |
| 86 |
} |
| 87 |
if (null !== $tag && '' !== $tag) { |
| 88 |
return new TaggedValue($tag, $result); |
| 89 |
} |
| 90 |
return $result; |
| 91 |
} finally { |
| 92 |
if (isset($mbEncoding)) { |
| 93 |
mb_internal_encoding($mbEncoding); |
| 94 |
} |
| 95 |
} |
| 96 |
} |
| 97 |
/** |
| 98 |
* Dumps a given PHP variable to a YAML string. |
| 99 |
* |
| 100 |
* @param mixed $value The PHP variable to convert |
| 101 |
* @param int $flags A bit field of Yaml::DUMP_* constants to customize the dumped YAML string |
| 102 |
* |
| 103 |
* @throws DumpException When trying to dump PHP resource |
| 104 |
*/ |
| 105 |
public static function dump($value, int $flags = 0): string |
| 106 |
{ |
| 107 |
switch (\true) { |
| 108 |
case \is_resource($value): |
| 109 |
if (Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE & $flags) { |
| 110 |
throw new DumpException(sprintf('Unable to dump PHP resources in a YAML file ("%s").', get_resource_type($value))); |
| 111 |
} |
| 112 |
return self::dumpNull($flags); |
| 113 |
case $value instanceof \DateTimeInterface: |
| 114 |
return $value->format('c'); |
| 115 |
case $value instanceof \UnitEnum: |
| 116 |
return sprintf('!php/const %s::%s', \get_class($value), $value->name); |
| 117 |
case \is_object($value): |
| 118 |
if ($value instanceof TaggedValue) { |
| 119 |
return '!' . $value->getTag() . ' ' . self::dump($value->getValue(), $flags); |
| 120 |
} |
| 121 |
if (Yaml::DUMP_OBJECT & $flags) { |
| 122 |
return '!php/object ' . self::dump(serialize($value)); |
| 123 |
} |
| 124 |
if (Yaml::DUMP_OBJECT_AS_MAP & $flags && ($value instanceof \stdClass || $value instanceof \ArrayObject)) { |
| 125 |
$output = []; |
| 126 |
foreach ($value as $key => $val) { |
| 127 |
$output[] = sprintf('%s: %s', self::dump($key, $flags), self::dump($val, $flags)); |
| 128 |
} |
| 129 |
return sprintf('{ %s }', implode(', ', $output)); |
| 130 |
} |
| 131 |
if (Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE & $flags) { |
| 132 |
throw new DumpException('Object support when dumping a YAML file has been disabled.'); |
| 133 |
} |
| 134 |
return self::dumpNull($flags); |
| 135 |
case \is_array($value): |
| 136 |
return self::dumpArray($value, $flags); |
| 137 |
case null === $value: |
| 138 |
return self::dumpNull($flags); |
| 139 |
case \true === $value: |
| 140 |
return 'true'; |
| 141 |
case \false === $value: |
| 142 |
return 'false'; |
| 143 |
case \is_int($value): |
| 144 |
return $value; |
| 145 |
case is_numeric($value) && \false === strpbrk($value, "\f\n\r\t\v"): |
| 146 |
$locale = setlocale(\LC_NUMERIC, 0); |
| 147 |
if (\false !== $locale) { |
| 148 |
setlocale(\LC_NUMERIC, 'C'); |
| 149 |
} |
| 150 |
if (\is_float($value)) { |
| 151 |
$repr = (string) $value; |
| 152 |
if (is_infinite($value)) { |
| 153 |
$repr = str_ireplace('INF', '.Inf', $repr); |
| 154 |
} elseif (floor($value) == $value && $repr == $value) { |
| 155 |
// Preserve float data type since storing a whole number will result in integer value. |
| 156 |
if (\false === strpos($repr, 'E')) { |
| 157 |
$repr = $repr . '.0'; |
| 158 |
} |
| 159 |
} |
| 160 |
} else { |
| 161 |
$repr = \is_string($value) ? "'{$value}'" : (string) $value; |
| 162 |
} |
| 163 |
if (\false !== $locale) { |
| 164 |
setlocale(\LC_NUMERIC, $locale); |
| 165 |
} |
| 166 |
return $repr; |
| 167 |
case '' == $value: |
| 168 |
return "''"; |
| 169 |
case self::isBinaryString($value): |
| 170 |
return '!!binary ' . base64_encode($value); |
| 171 |
case Escaper::requiresDoubleQuoting($value): |
| 172 |
return Escaper::escapeWithDoubleQuotes($value); |
| 173 |
case Escaper::requiresSingleQuoting($value): |
| 174 |
case Parser::preg_match('{^[0-9]+[_0-9]*$}', $value): |
| 175 |
case Parser::preg_match(self::getHexRegex(), $value): |
| 176 |
case Parser::preg_match(self::getTimestampRegex(), $value): |
| 177 |
return Escaper::escapeWithSingleQuotes($value); |
| 178 |
default: |
| 179 |
return $value; |
| 180 |
} |
| 181 |
} |
| 182 |
/** |
| 183 |
* Check if given array is hash or just normal indexed array. |
| 184 |
* |
| 185 |
* @param array|\ArrayObject|\stdClass $value The PHP array or array-like object to check |
| 186 |
*/ |
| 187 |
public static function isHash($value): bool |
| 188 |
{ |
| 189 |
if ($value instanceof \stdClass || $value instanceof \ArrayObject) { |
| 190 |
return \true; |
| 191 |
} |
| 192 |
$expectedKey = 0; |
| 193 |
foreach ($value as $key => $val) { |
| 194 |
if ($key !== $expectedKey++) { |
| 195 |
return \true; |
| 196 |
} |
| 197 |
} |
| 198 |
return \false; |
| 199 |
} |
| 200 |
/** |
| 201 |
* Dumps a PHP array to a YAML string. |
| 202 |
* |
| 203 |
* @param array $value The PHP array to dump |
| 204 |
* @param int $flags A bit field of Yaml::DUMP_* constants to customize the dumped YAML string |
| 205 |
*/ |
| 206 |
private static function dumpArray(array $value, int $flags): string |
| 207 |
{ |
| 208 |
// array |
| 209 |
if (($value || Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE & $flags) && !self::isHash($value)) { |
| 210 |
$output = []; |
| 211 |
foreach ($value as $val) { |
| 212 |
$output[] = self::dump($val, $flags); |
| 213 |
} |
| 214 |
return sprintf('[%s]', implode(', ', $output)); |
| 215 |
} |
| 216 |
// hash |
| 217 |
$output = []; |
| 218 |
foreach ($value as $key => $val) { |
| 219 |
$output[] = sprintf('%s: %s', self::dump($key, $flags), self::dump($val, $flags)); |
| 220 |
} |
| 221 |
return sprintf('{ %s }', implode(', ', $output)); |
| 222 |
} |
| 223 |
private static function dumpNull(int $flags): string |
| 224 |
{ |
| 225 |
if (Yaml::DUMP_NULL_AS_TILDE & $flags) { |
| 226 |
return '~'; |
| 227 |
} |
| 228 |
return 'null'; |
| 229 |
} |
| 230 |
/** |
| 231 |
* Parses a YAML scalar. |
| 232 |
* |
| 233 |
* @return mixed |
| 234 |
* |
| 235 |
* @throws ParseException When malformed inline YAML string is parsed |
| 236 |
*/ |
| 237 |
public static function parseScalar(string $scalar, int $flags = 0, ?array $delimiters = null, int &$i = 0, bool $evaluate = \true, array &$references = [], ?bool &$isQuoted = null) |
| 238 |
{ |
| 239 |
if (\in_array($scalar[$i], ['"', "'"], \true)) { |
| 240 |
// quoted scalar |
| 241 |
$isQuoted = \true; |
| 242 |
$output = self::parseQuotedScalar($scalar, $i); |
| 243 |
if (null !== $delimiters) { |
| 244 |
$tmp = ltrim(substr($scalar, $i), " \n"); |
| 245 |
if ('' === $tmp) { |
| 246 |
throw new ParseException(sprintf('Unexpected end of line, expected one of "%s".', implode('', $delimiters)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); |
| 247 |
} |
| 248 |
if (!\in_array($tmp[0], $delimiters)) { |
| 249 |
throw new ParseException(sprintf('Unexpected characters (%s).', substr($scalar, $i)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); |
| 250 |
} |
| 251 |
} |
| 252 |
} else { |
| 253 |
// "normal" string |
| 254 |
$isQuoted = \false; |
| 255 |
if (!$delimiters) { |
| 256 |
$output = substr($scalar, $i); |
| 257 |
$i += \strlen($output); |
| 258 |
// remove comments |
| 259 |
if (Parser::preg_match('/[ \t]+#/', $output, $match, \PREG_OFFSET_CAPTURE)) { |
| 260 |
$output = substr($output, 0, $match[0][1]); |
| 261 |
} |
| 262 |
} elseif (Parser::preg_match('/^(.*?)(' . implode('|', $delimiters) . ')/', substr($scalar, $i), $match)) { |
| 263 |
$output = $match[1]; |
| 264 |
$i += \strlen($output); |
| 265 |
$output = trim($output); |
| 266 |
} else { |
| 267 |
throw new ParseException(sprintf('Malformed inline YAML string: "%s".', $scalar), self::$parsedLineNumber + 1, null, self::$parsedFilename); |
| 268 |
} |
| 269 |
// a non-quoted string cannot start with @ or ` (reserved) nor with a scalar indicator (| or >) |
| 270 |
if ($output && ('@' === $output[0] || '`' === $output[0] || '|' === $output[0] || '>' === $output[0] || '%' === $output[0])) { |
| 271 |
throw new ParseException(sprintf('The reserved indicator "%s" cannot start a plain scalar; you need to quote the scalar.', $output[0]), self::$parsedLineNumber + 1, $output, self::$parsedFilename); |
| 272 |
} |
| 273 |
if ($evaluate) { |
| 274 |
$output = self::evaluateScalar($output, $flags, $references, $isQuoted); |
| 275 |
} |
| 276 |
} |
| 277 |
return $output; |
| 278 |
} |
| 279 |
/** |
| 280 |
* Parses a YAML quoted scalar. |
| 281 |
* |
| 282 |
* @throws ParseException When malformed inline YAML string is parsed |
| 283 |
*/ |
| 284 |
private static function parseQuotedScalar(string $scalar, int &$i = 0): string |
| 285 |
{ |
| 286 |
if (!Parser::preg_match('/' . self::REGEX_QUOTED_STRING . '/Au', substr($scalar, $i), $match)) { |
| 287 |
throw new ParseException(sprintf('Malformed inline YAML string: "%s".', substr($scalar, $i)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); |
| 288 |
} |
| 289 |
$output = substr($match[0], 1, -1); |
| 290 |
$unescaper = new Unescaper(); |
| 291 |
if ('"' == $scalar[$i]) { |
| 292 |
$output = $unescaper->unescapeDoubleQuotedString($output); |
| 293 |
} else { |
| 294 |
$output = $unescaper->unescapeSingleQuotedString($output); |
| 295 |
} |
| 296 |
$i += \strlen($match[0]); |
| 297 |
return $output; |
| 298 |
} |
| 299 |
/** |
| 300 |
* Parses a YAML sequence. |
| 301 |
* |
| 302 |
* @throws ParseException When malformed inline YAML string is parsed |
| 303 |
*/ |
| 304 |
private static function parseSequence(string $sequence, int $flags, int &$i = 0, array &$references = []): array |
| 305 |
{ |
| 306 |
$output = []; |
| 307 |
$len = \strlen($sequence); |
| 308 |
++$i; |
| 309 |
// [foo, bar, ...] |
| 310 |
$lastToken = null; |
| 311 |
while ($i < $len) { |
| 312 |
if (']' === $sequence[$i]) { |
| 313 |
return $output; |
| 314 |
} |
| 315 |
if (',' === $sequence[$i] || ' ' === $sequence[$i]) { |
| 316 |
if (',' === $sequence[$i] && (null === $lastToken || 'separator' === $lastToken)) { |
| 317 |
$output[] = null; |
| 318 |
} elseif (',' === $sequence[$i]) { |
| 319 |
$lastToken = 'separator'; |
| 320 |
} |
| 321 |
++$i; |
| 322 |
continue; |
| 323 |
} |
| 324 |
$tag = self::parseTag($sequence, $i, $flags); |
| 325 |
switch ($sequence[$i]) { |
| 326 |
case '[': |
| 327 |
// nested sequence |
| 328 |
$value = self::parseSequence($sequence, $flags, $i, $references); |
| 329 |
break; |
| 330 |
case '{': |
| 331 |
// nested mapping |
| 332 |
$value = self::parseMapping($sequence, $flags, $i, $references); |
| 333 |
break; |
| 334 |
default: |
| 335 |
$value = self::parseScalar($sequence, $flags, [',', ']'], $i, null === $tag, $references, $isQuoted); |
| 336 |
// the value can be an array if a reference has been resolved to an array var |
| 337 |
if (\is_string($value) && !$isQuoted && \false !== strpos($value, ': ')) { |
| 338 |
// embedded mapping? |
| 339 |
try { |
| 340 |
$pos = 0; |
| 341 |
$value = self::parseMapping('{' . $value . '}', $flags, $pos, $references); |
| 342 |
} catch (\InvalidArgumentException $e) { |
| 343 |
// no, it's not |
| 344 |
} |
| 345 |
} |
| 346 |
if (!$isQuoted && \is_string($value) && '' !== $value && '&' === $value[0] && Parser::preg_match(Parser::REFERENCE_PATTERN, $value, $matches)) { |
| 347 |
$references[$matches['ref']] = $matches['value']; |
| 348 |
$value = $matches['value']; |
| 349 |
} |
| 350 |
--$i; |
| 351 |
} |
| 352 |
if (null !== $tag && '' !== $tag) { |
| 353 |
$value = new TaggedValue($tag, $value); |
| 354 |
} |
| 355 |
$output[] = $value; |
| 356 |
$lastToken = 'value'; |
| 357 |
++$i; |
| 358 |
} |
| 359 |
throw new ParseException(sprintf('Malformed inline YAML string: "%s".', $sequence), self::$parsedLineNumber + 1, null, self::$parsedFilename); |
| 360 |
} |
| 361 |
/** |
| 362 |
* Parses a YAML mapping. |
| 363 |
* |
| 364 |
* @return array|\stdClass |
| 365 |
* |
| 366 |
* @throws ParseException When malformed inline YAML string is parsed |
| 367 |
*/ |
| 368 |
private static function parseMapping(string $mapping, int $flags, int &$i = 0, array &$references = []) |
| 369 |
{ |
| 370 |
$output = []; |
| 371 |
$len = \strlen($mapping); |
| 372 |
++$i; |
| 373 |
$allowOverwrite = \false; |
| 374 |
// {foo: bar, bar:foo, ...} |
| 375 |
while ($i < $len) { |
| 376 |
switch ($mapping[$i]) { |
| 377 |
case ' ': |
| 378 |
case ',': |
| 379 |
case "\n": |
| 380 |
++$i; |
| 381 |
continue 2; |
| 382 |
case '}': |
| 383 |
if (self::$objectForMap) { |
| 384 |
return (object) $output; |
| 385 |
} |
| 386 |
return $output; |
| 387 |
} |
| 388 |
// key |
| 389 |
$offsetBeforeKeyParsing = $i; |
| 390 |
$isKeyQuoted = \in_array($mapping[$i], ['"', "'"], \true); |
| 391 |
$key = self::parseScalar($mapping, $flags, [':', ' '], $i, \false); |
| 392 |
if ($offsetBeforeKeyParsing === $i) { |
| 393 |
throw new ParseException('Missing mapping key.', self::$parsedLineNumber + 1, $mapping); |
| 394 |
} |
| 395 |
if ('!php/const' === $key) { |
| 396 |
$key .= ' ' . self::parseScalar($mapping, $flags, [':'], $i, \false); |
| 397 |
$key = self::evaluateScalar($key, $flags); |
| 398 |
} |
| 399 |
if (\false === $i = strpos($mapping, ':', $i)) { |
| 400 |
break; |
| 401 |
} |
| 402 |
if (!$isKeyQuoted) { |
| 403 |
$evaluatedKey = self::evaluateScalar($key, $flags, $references); |
| 404 |
if ('' !== $key && $evaluatedKey !== $key && !\is_string($evaluatedKey) && !\is_int($evaluatedKey)) { |
| 405 |
throw new ParseException('Implicit casting of incompatible mapping keys to strings is not supported. Quote your evaluable mapping keys instead.', self::$parsedLineNumber + 1, $mapping); |
| 406 |
} |
| 407 |
} |
| 408 |
if (!$isKeyQuoted && (!isset($mapping[$i + 1]) || !\in_array($mapping[$i + 1], [' ', ',', '[', ']', '{', '}', "\n"], \true))) { |
| 409 |
throw new ParseException('Colons must be followed by a space or an indication character (i.e. " ", ",", "[", "]", "{", "}").', self::$parsedLineNumber + 1, $mapping); |
| 410 |
} |
| 411 |
if ('<<' === $key) { |
| 412 |
$allowOverwrite = \true; |
| 413 |
} |
| 414 |
while ($i < $len) { |
| 415 |
if (':' === $mapping[$i] || ' ' === $mapping[$i] || "\n" === $mapping[$i]) { |
| 416 |
++$i; |
| 417 |
continue; |
| 418 |
} |
| 419 |
$tag = self::parseTag($mapping, $i, $flags); |
| 420 |
switch ($mapping[$i]) { |
| 421 |
case '[': |
| 422 |
// nested sequence |
| 423 |
$value = self::parseSequence($mapping, $flags, $i, $references); |
| 424 |
// Spec: Keys MUST be unique; first one wins. |
| 425 |
// Parser cannot abort this mapping earlier, since lines |
| 426 |
// are processed sequentially. |
| 427 |
// But overwriting is allowed when a merge node is used in current block. |
| 428 |
if ('<<' === $key) { |
| 429 |
foreach ($value as $parsedValue) { |
| 430 |
$output += $parsedValue; |
| 431 |
} |
| 432 |
} elseif ($allowOverwrite || !isset($output[$key])) { |
| 433 |
if (null !== $tag) { |
| 434 |
$output[$key] = new TaggedValue($tag, $value); |
| 435 |
} else { |
| 436 |
$output[$key] = $value; |
| 437 |
} |
| 438 |
} elseif (isset($output[$key])) { |
| 439 |
throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), self::$parsedLineNumber + 1, $mapping); |
| 440 |
} |
| 441 |
break; |
| 442 |
case '{': |
| 443 |
// nested mapping |
| 444 |
$value = self::parseMapping($mapping, $flags, $i, $references); |
| 445 |
// Spec: Keys MUST be unique; first one wins. |
| 446 |
// Parser cannot abort this mapping earlier, since lines |
| 447 |
// are processed sequentially. |
| 448 |
// But overwriting is allowed when a merge node is used in current block. |
| 449 |
if ('<<' === $key) { |
| 450 |
$output += $value; |
| 451 |
} elseif ($allowOverwrite || !isset($output[$key])) { |
| 452 |
if (null !== $tag) { |
| 453 |
$output[$key] = new TaggedValue($tag, $value); |
| 454 |
} else { |
| 455 |
$output[$key] = $value; |
| 456 |
} |
| 457 |
} elseif (isset($output[$key])) { |
| 458 |
throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), self::$parsedLineNumber + 1, $mapping); |
| 459 |
} |
| 460 |
break; |
| 461 |
default: |
| 462 |
$value = self::parseScalar($mapping, $flags, [',', '}', "\n"], $i, null === $tag, $references, $isValueQuoted); |
| 463 |
// Spec: Keys MUST be unique; first one wins. |
| 464 |
// Parser cannot abort this mapping earlier, since lines |
| 465 |
// are processed sequentially. |
| 466 |
// But overwriting is allowed when a merge node is used in current block. |
| 467 |
if ('<<' === $key) { |
| 468 |
$output += $value; |
| 469 |
} elseif ($allowOverwrite || !isset($output[$key])) { |
| 470 |
if (!$isValueQuoted && \is_string($value) && '' !== $value && '&' === $value[0] && !self::isBinaryString($value) && Parser::preg_match(Parser::REFERENCE_PATTERN, $value, $matches)) { |
| 471 |
$references[$matches['ref']] = $matches['value']; |
| 472 |
$value = $matches['value']; |
| 473 |
} |
| 474 |
if (null !== $tag) { |
| 475 |
$output[$key] = new TaggedValue($tag, $value); |
| 476 |
} else { |
| 477 |
$output[$key] = $value; |
| 478 |
} |
| 479 |
} elseif (isset($output[$key])) { |
| 480 |
throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), self::$parsedLineNumber + 1, $mapping); |
| 481 |
} |
| 482 |
--$i; |
| 483 |
} |
| 484 |
++$i; |
| 485 |
continue 2; |
| 486 |
} |
| 487 |
} |
| 488 |
throw new ParseException(sprintf('Malformed inline YAML string: "%s".', $mapping), self::$parsedLineNumber + 1, null, self::$parsedFilename); |
| 489 |
} |
| 490 |
/** |
| 491 |
* Evaluates scalars and replaces magic values. |
| 492 |
* |
| 493 |
* @return mixed |
| 494 |
* |
| 495 |
* @throws ParseException when object parsing support was disabled and the parser detected a PHP object or when a reference could not be resolved |
| 496 |
*/ |
| 497 |
private static function evaluateScalar(string $scalar, int $flags, array &$references = [], ?bool &$isQuotedString = null) |
| 498 |
{ |
| 499 |
$isQuotedString = \false; |
| 500 |
$scalar = trim($scalar); |
| 501 |
if (0 === strpos($scalar, '*')) { |
| 502 |
if (\false !== $pos = strpos($scalar, '#')) { |
| 503 |
$value = substr($scalar, 1, $pos - 2); |
| 504 |
} else { |
| 505 |
$value = substr($scalar, 1); |
| 506 |
} |
| 507 |
// an unquoted * |
| 508 |
if (\false === $value || '' === $value) { |
| 509 |
throw new ParseException('A reference must contain at least one character.', self::$parsedLineNumber + 1, $value, self::$parsedFilename); |
| 510 |
} |
| 511 |
if (!\array_key_exists($value, $references)) { |
| 512 |
throw new ParseException(sprintf('Reference "%s" does not exist.', $value), self::$parsedLineNumber + 1, $value, self::$parsedFilename); |
| 513 |
} |
| 514 |
return $references[$value]; |
| 515 |
} |
| 516 |
$scalarLower = strtolower($scalar); |
| 517 |
switch (\true) { |
| 518 |
case 'null' === $scalarLower: |
| 519 |
case '' === $scalar: |
| 520 |
case '~' === $scalar: |
| 521 |
return null; |
| 522 |
case 'true' === $scalarLower: |
| 523 |
return \true; |
| 524 |
case 'false' === $scalarLower: |
| 525 |
return \false; |
| 526 |
case '!' === $scalar[0]: |
| 527 |
switch (\true) { |
| 528 |
case 0 === strpos($scalar, '!!str '): |
| 529 |
$s = (string) substr($scalar, 6); |
| 530 |
if (\in_array($s[0] ?? '', ['"', "'"], \true)) { |
| 531 |
$isQuotedString = \true; |
| 532 |
$s = self::parseQuotedScalar($s); |
| 533 |
} |
| 534 |
return $s; |
| 535 |
case 0 === strpos($scalar, '! '): |
| 536 |
return substr($scalar, 2); |
| 537 |
case 0 === strpos($scalar, '!php/object'): |
| 538 |
if (self::$objectSupport) { |
| 539 |
if (!isset($scalar[12])) { |
| 540 |
trigger_deprecation('symfony/yaml', '5.1', 'Using the !php/object tag without a value is deprecated.'); |
| 541 |
return \false; |
| 542 |
} |
| 543 |
return unserialize(self::parseScalar(substr($scalar, 12))); |
| 544 |
} |
| 545 |
if (self::$exceptionOnInvalidType) { |
| 546 |
throw new ParseException('Object support when parsing a YAML file has been disabled.', self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); |
| 547 |
} |
| 548 |
return null; |
| 549 |
case 0 === strpos($scalar, '!php/const'): |
| 550 |
if (self::$constantSupport) { |
| 551 |
if (!isset($scalar[11])) { |
| 552 |
trigger_deprecation('symfony/yaml', '5.1', 'Using the !php/const tag without a value is deprecated.'); |
| 553 |
return ''; |
| 554 |
} |
| 555 |
$i = 0; |
| 556 |
if (\defined($const = self::parseScalar(substr($scalar, 11), 0, null, $i, \false))) { |
| 557 |
return \constant($const); |
| 558 |
} |
| 559 |
throw new ParseException(sprintf('The constant "%s" is not defined.', $const), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); |
| 560 |
} |
| 561 |
if (self::$exceptionOnInvalidType) { |
| 562 |
throw new ParseException(sprintf('The string "%s" could not be parsed as a constant. Did you forget to pass the "Yaml::PARSE_CONSTANT" flag to the parser?', $scalar), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); |
| 563 |
} |
| 564 |
return null; |
| 565 |
case 0 === strpos($scalar, '!!float '): |
| 566 |
return (float) substr($scalar, 8); |
| 567 |
case 0 === strpos($scalar, '!!binary '): |
| 568 |
return self::evaluateBinaryScalar(substr($scalar, 9)); |
| 569 |
} |
| 570 |
throw new ParseException(sprintf('The string "%s" could not be parsed as it uses an unsupported built-in tag.', $scalar), self::$parsedLineNumber, $scalar, self::$parsedFilename); |
| 571 |
case preg_match('/^(?:\+|-)?0o(?P<value>[0-7_]++)$/', $scalar, $matches): |
| 572 |
$value = str_replace('_', '', $matches['value']); |
| 573 |
if ('-' === $scalar[0]) { |
| 574 |
return -octdec($value); |
| 575 |
} |
| 576 |
return octdec($value); |
| 577 |
case \in_array($scalar[0], ['+', '-', '.'], \true) || is_numeric($scalar[0]): |
| 578 |
if (Parser::preg_match('{^[+-]?[0-9][0-9_]*$}', $scalar)) { |
| 579 |
$scalar = str_replace('_', '', $scalar); |
| 580 |
} |
| 581 |
switch (\true) { |
| 582 |
case ctype_digit($scalar): |
| 583 |
if (preg_match('/^0[0-7]+$/', $scalar)) { |
| 584 |
trigger_deprecation('symfony/yaml', '5.1', 'Support for parsing numbers prefixed with 0 as octal numbers. They will be parsed as strings as of 6.0. Use "%s" to represent the octal number.', '0o' . substr($scalar, 1)); |
| 585 |
return octdec($scalar); |
| 586 |
} |
| 587 |
$cast = (int) $scalar; |
| 588 |
return $scalar === (string) $cast ? $cast : $scalar; |
| 589 |
case '-' === $scalar[0] && ctype_digit(substr($scalar, 1)): |
| 590 |
if (preg_match('/^-0[0-7]+$/', $scalar)) { |
| 591 |
trigger_deprecation('symfony/yaml', '5.1', 'Support for parsing numbers prefixed with 0 as octal numbers. They will be parsed as strings as of 6.0. Use "%s" to represent the octal number.', '-0o' . substr($scalar, 2)); |
| 592 |
return -octdec(substr($scalar, 1)); |
| 593 |
} |
| 594 |
$cast = (int) $scalar; |
| 595 |
return $scalar === (string) $cast ? $cast : $scalar; |
| 596 |
case is_numeric($scalar): |
| 597 |
case Parser::preg_match(self::getHexRegex(), $scalar): |
| 598 |
$scalar = str_replace('_', '', $scalar); |
| 599 |
return '0x' === $scalar[0] . $scalar[1] ? hexdec($scalar) : (float) $scalar; |
| 600 |
case '.inf' === $scalarLower: |
| 601 |
case '.nan' === $scalarLower: |
| 602 |
return -log(0); |
| 603 |
case '-.inf' === $scalarLower: |
| 604 |
return log(0); |
| 605 |
case Parser::preg_match('/^(-|\+)?[0-9][0-9_]*(\.[0-9_]+)?$/', $scalar): |
| 606 |
return (float) str_replace('_', '', $scalar); |
| 607 |
case Parser::preg_match(self::getTimestampRegex(), $scalar): |
| 608 |
try { |
| 609 |
// When no timezone is provided in the parsed date, YAML spec says we must assume UTC. |
| 610 |
$time = new \DateTime($scalar, new \DateTimeZone('UTC')); |
| 611 |
} catch (\Exception $e) { |
| 612 |
// Some dates accepted by the regex are not valid dates. |
| 613 |
throw new ParseException(\sprintf('The date "%s" could not be parsed as it is an invalid date.', $scalar), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename, $e); |
| 614 |
} |
| 615 |
if (Yaml::PARSE_DATETIME & $flags) { |
| 616 |
return $time; |
| 617 |
} |
| 618 |
try { |
| 619 |
if (\false !== $scalar = $time->getTimestamp()) { |
| 620 |
return $scalar; |
| 621 |
} |
| 622 |
} catch (\ValueError $e) { |
| 623 |
// no-op |
| 624 |
} |
| 625 |
return $time->format('U'); |
| 626 |
} |
| 627 |
} |
| 628 |
return (string) $scalar; |
| 629 |
} |
| 630 |
private static function parseTag(string $value, int &$i, int $flags): ?string |
| 631 |
{ |
| 632 |
if ('!' !== $value[$i]) { |
| 633 |
return null; |
| 634 |
} |
| 635 |
$tagLength = strcspn($value, " \t\n[]{},", $i + 1); |
| 636 |
$tag = substr($value, $i + 1, $tagLength); |
| 637 |
$nextOffset = $i + $tagLength + 1; |
| 638 |
$nextOffset += strspn($value, ' ', $nextOffset); |
| 639 |
if ('' === $tag && (!isset($value[$nextOffset]) || \in_array($value[$nextOffset], [']', '}', ','], \true))) { |
| 640 |
throw new ParseException('Using the unquoted scalar value "!" is not supported. You must quote it.', self::$parsedLineNumber + 1, $value, self::$parsedFilename); |
| 641 |
} |
| 642 |
// Is followed by a scalar and is a built-in tag |
| 643 |
if ('' !== $tag && (!isset($value[$nextOffset]) || !\in_array($value[$nextOffset], ['[', '{'], \true)) && ('!' === $tag[0] || 'str' === $tag || 'php/const' === $tag || 'php/object' === $tag)) { |
| 644 |
// Manage in {@link self::evaluateScalar()} |
| 645 |
return null; |
| 646 |
} |
| 647 |
$i = $nextOffset; |
| 648 |
// Built-in tags |
| 649 |
if ('' !== $tag && '!' === $tag[0]) { |
| 650 |
throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag), self::$parsedLineNumber + 1, $value, self::$parsedFilename); |
| 651 |
} |
| 652 |
if ('' !== $tag && !isset($value[$i])) { |
| 653 |
throw new ParseException(sprintf('Missing value for tag "%s".', $tag), self::$parsedLineNumber + 1, $value, self::$parsedFilename); |
| 654 |
} |
| 655 |
if ('' === $tag || Yaml::PARSE_CUSTOM_TAGS & $flags) { |
| 656 |
return $tag; |
| 657 |
} |
| 658 |
throw new ParseException(sprintf('Tags support is not enabled. Enable the "Yaml::PARSE_CUSTOM_TAGS" flag to use "!%s".', $tag), self::$parsedLineNumber + 1, $value, self::$parsedFilename); |
| 659 |
} |
| 660 |
public static function evaluateBinaryScalar(string $scalar): string |
| 661 |
{ |
| 662 |
$parsedBinaryData = self::parseScalar(preg_replace('/\s/', '', $scalar)); |
| 663 |
if (0 !== \strlen($parsedBinaryData) % 4) { |
| 664 |
throw new ParseException(sprintf('The normalized base64 encoded data (data without whitespace characters) length must be a multiple of four (%d bytes given).', \strlen($parsedBinaryData)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); |
| 665 |
} |
| 666 |
if (!Parser::preg_match('#^[A-Z0-9+/]+={0,2}$#i', $parsedBinaryData)) { |
| 667 |
throw new ParseException(sprintf('The base64 encoded data (%s) contains invalid characters.', $parsedBinaryData), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); |
| 668 |
} |
| 669 |
return base64_decode($parsedBinaryData, \true); |
| 670 |
} |
| 671 |
private static function isBinaryString(string $value): bool |
| 672 |
{ |
| 673 |
return !preg_match('//u', $value) || preg_match('/[^\x00\x07-\x0d\x1B\x20-\xff]/', $value); |
| 674 |
} |
| 675 |
/** |
| 676 |
* Gets a regex that matches a YAML date. |
| 677 |
* |
| 678 |
* @see http://www.yaml.org/spec/1.2/spec.html#id2761573 |
| 679 |
*/ |
| 680 |
private static function getTimestampRegex(): string |
| 681 |
{ |
| 682 |
return <<<EOF |
| 683 |
~^ |
| 684 |
(?P<year>[0-9][0-9][0-9][0-9]) |
| 685 |
-(?P<month>[0-9][0-9]?) |
| 686 |
-(?P<day>[0-9][0-9]?) |
| 687 |
(?:(?:[Tt]|[ \t]+) |
| 688 |
(?P<hour>[0-9][0-9]?) |
| 689 |
:(?P<minute>[0-9][0-9]) |
| 690 |
:(?P<second>[0-9][0-9]) |
| 691 |
(?:\\.(?P<fraction>[0-9]*))? |
| 692 |
(?:[ \t]*(?P<tz>Z|(?P<tz_sign>[-+])(?P<tz_hour>[0-9][0-9]?) |
| 693 |
(?::(?P<tz_minute>[0-9][0-9]))?))?)? |
| 694 |
\$~x |
| 695 |
EOF; |
| 696 |
} |
| 697 |
/** |
| 698 |
* Gets a regex that matches a YAML number in hexadecimal notation. |
| 699 |
*/ |
| 700 |
private static function getHexRegex(): string |
| 701 |
{ |
| 702 |
return '~^0x[0-9a-f_]++$~i'; |
| 703 |
} |
| 704 |
} |
| 705 |
|