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