| @@ -23,8 +23,10 @@ | ||
| 23 | 23 | { |
| 24 | 24 | public const TAG_PATTERN = '(?P<tag>![\w!.\/:-]+)'; |
| 25 | 25 | public const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?'; |
| 26 | 26 | public const REFERENCE_PATTERN = '#^&(?P<ref>[^ ]++) *+(?P<value>.*)#u'; |
| 27 | + public const DEFAULT_MAX_NESTING_LEVEL = 128; | |
| 28 | + public const DEFAULT_MAX_ALIASES_FOR_COLLECTIONS = 128; | |
| 27 | 29 | private $filename; |
| 28 | 30 | private $offset = 0; |
| 29 | 31 | private $numberOfParsedLines = 0; |
| 30 | 32 | private $totalNumberOfLines; |
| @@ -34,8 +36,20 @@ | ||
| 34 | 36 | private $refs = []; |
| 35 | 37 | private $skippedLineNumbers = []; |
| 36 | 38 | private $locallySkippedLineNumbers = []; |
| 37 | 39 | private $refsBeingParsed = []; |
| 40 | + private $state; | |
| 41 | + public function __construct(int $maxNestingLevel = self::DEFAULT_MAX_NESTING_LEVEL, int $maxAliasesForCollections = self::DEFAULT_MAX_ALIASES_FOR_COLLECTIONS) | |
| 42 | + { | |
| 43 | + if ($maxNestingLevel < 1) { | |
| 44 | + throw new \InvalidArgumentException('The maximum nesting depth must be greater than 0.'); | |
| 45 | + } | |
| 46 | + if ($maxAliasesForCollections < 0) { | |
| 47 | + throw new \InvalidArgumentException('The maximum number of collection aliases must be greater than or equal to 0.'); | |
| 48 | + } | |
| 49 | + $this->getState()->maxNestingLevel = $maxNestingLevel; | |
| 50 | + $this->getState()->maxAliasesForCollections = $maxAliasesForCollections; | |
| 51 | + } | |
| 38 | 52 | /** |
| 39 | 53 | * Parses a YAML file into a PHP value. |
| 40 | 54 | * |
| 41 | 55 | * @param string $filename The path to the YAML file to be parsed |
| @@ -75,8 +89,11 @@ | ||
| 75 | 89 | if (\false === preg_match('//u', $value)) { |
| 76 | 90 | throw new ParseException('The YAML value does not appear to be valid UTF-8.', -1, null, $this->filename); |
| 77 | 91 | } |
| 78 | 92 | $this->refs = []; |
| 93 | + $state = $this->getState(); | |
| 94 | + $state->reset(); | |
| 95 | + $state->aliasesEnabled = 0 === (Yaml::PARSE_EXCEPTION_ON_ALIAS & $flags); | |
| 79 | 96 | $mbEncoding = null; |
| 80 | 97 | if (2 & (int) \ini_get('mbstring.func_overload')) { |
| 81 | 98 | $mbEncoding = mb_internal_encoding(); |
| 82 | 99 | mb_internal_encoding('UTF-8'); |
| @@ -95,11 +112,16 @@ | ||
| 95 | 112 | $this->refs = []; |
| 96 | 113 | $this->skippedLineNumbers = []; |
| 97 | 114 | $this->locallySkippedLineNumbers = []; |
| 98 | 115 | $this->totalNumberOfLines = null; |
| 116 | + $state->reset(); | |
| 99 | 117 | } |
| 100 | 118 | return $data; |
| 101 | 119 | } |
| 120 | + private function getState(): ParserState | |
| 121 | + { | |
| 122 | + return $this->state ?? $this->state = new ParserState(); | |
| 123 | + } | |
| 102 | 124 | private function doParse(string $value, int $flags) |
| 103 | 125 | { |
| 104 | 126 | $this->currentLineNb = -1; |
| 105 | 127 | $this->currentLine = ''; |
| @@ -203,8 +225,9 @@ | ||
| 203 | 225 | } |
| 204 | 226 | throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); |
| 205 | 227 | } |
| 206 | 228 | $refValue = $this->refs[$refName]; |
| 229 | + $this->getState()->countAlias($refValue, $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); | |
| 207 | 230 | if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $refValue instanceof \stdClass) { |
| 208 | 231 | $refValue = (array) $refValue; |
| 209 | 232 | } |
| 210 | 233 | if (!\is_array($refValue)) { |
| @@ -309,9 +332,9 @@ | ||
| 309 | 332 | if (null !== $context) { |
| 310 | 333 | throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); |
| 311 | 334 | } |
| 312 | 335 | try { |
| 313 | - return Inline::parse($this->lexInlineQuotedString(), $flags, $this->refs); | |
| 336 | + return Inline::parse($this->lexInlineQuotedString(), $flags, $this->refs, $this->state); | |
| 314 | 337 | } catch (ParseException $e) { |
| 315 | 338 | $e->setParsedLine($this->getRealCurrentLineNb() + 1); |
| 316 | 339 | $e->setSnippet($this->currentLine); |
| 317 | 340 | throw $e; |
| @@ -320,9 +343,9 @@ | ||
| 320 | 343 | if (null !== $context) { |
| 321 | 344 | throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); |
| 322 | 345 | } |
| 323 | 346 | try { |
| 324 | - $parsedMapping = Inline::parse($this->lexInlineMapping(), $flags, $this->refs); | |
| 347 | + $parsedMapping = Inline::parse($this->lexInlineMapping(), $flags, $this->refs, $this->state); | |
| 325 | 348 | while ($this->moveToNextLine()) { |
| 326 | 349 | if (!$this->isCurrentLineEmpty()) { |
| 327 | 350 | throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); |
| 328 | 351 | } |
| @@ -337,9 +360,9 @@ | ||
| 337 | 360 | if (null !== $context) { |
| 338 | 361 | throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); |
| 339 | 362 | } |
| 340 | 363 | try { |
| 341 | - $parsedSequence = Inline::parse($this->lexInlineSequence(), $flags, $this->refs); | |
| 364 | + $parsedSequence = Inline::parse($this->lexInlineSequence(), $flags, $this->refs, $this->state); | |
| 342 | 365 | while ($this->moveToNextLine()) { |
| 343 | 366 | if (!$this->isCurrentLineEmpty()) { |
| 344 | 367 | throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); |
| 345 | 368 | } |
| @@ -360,9 +383,9 @@ | ||
| 360 | 383 | } |
| 361 | 384 | // 1-liner optionally followed by newline(s) |
| 362 | 385 | if (\is_string($value) && $this->lines[0] === trim($value)) { |
| 363 | 386 | try { |
| 364 | - $value = Inline::parse($this->lines[0], $flags, $this->refs); | |
| 387 | + $value = Inline::parse($this->lines[0], $flags, $this->refs, $this->state); | |
| 365 | 388 | } catch (ParseException $e) { |
| 366 | 389 | $e->setParsedLine($this->getRealCurrentLineNb() + 1); |
| 367 | 390 | $e->setSnippet($this->currentLine); |
| 368 | 391 | throw $e; |
| @@ -407,9 +430,9 @@ | ||
| 407 | 430 | $previousLineWasTerminatedWithBackslash = \false; |
| 408 | 431 | } |
| 409 | 432 | } |
| 410 | 433 | try { |
| 411 | - return Inline::parse(trim($value)); | |
| 434 | + return Inline::parse(trim($value), 0, $this->refs, $this->state); | |
| 412 | 435 | } catch (ParseException $e) { |
| 413 | 436 | // fall-through to the ParseException thrown below |
| 414 | 437 | } |
| 415 | 438 | } |
| @@ -442,9 +465,15 @@ | ||
| 442 | 465 | $parser->totalNumberOfLines = $this->totalNumberOfLines; |
| 443 | 466 | $parser->skippedLineNumbers = $skippedLineNumbers; |
| 444 | 467 | $parser->refs =& $this->refs; |
| 445 | 468 | $parser->refsBeingParsed = $this->refsBeingParsed; |
| 446 | - return $parser->doParse($yaml, $flags); | |
| 469 | + $parser->state = $this->state; | |
| 470 | + $this->getState()->enterNestingLevel($offset + 1, $this->currentLine, $this->filename); | |
| 471 | + try { | |
| 472 | + return $parser->doParse($yaml, $flags); | |
| 473 | + } finally { | |
| 474 | + $this->getState()->leaveNestingLevel(); | |
| 475 | + } | |
| 447 | 476 | } |
| 448 | 477 | /** |
| 449 | 478 | * Returns the current line number (takes the offset into account). |
| 450 | 479 | * |
| @@ -604,8 +633,9 @@ | ||
| 604 | 633 | throw new ParseException(sprintf('Circular reference [%s] detected for reference "%s".', implode(', ', array_merge(\array_slice($this->refsBeingParsed, $pos), [$value])), $value), $this->currentLineNb + 1, $this->currentLine, $this->filename); |
| 605 | 634 | } |
| 606 | 635 | throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine, $this->filename); |
| 607 | 636 | } |
| 637 | + $this->getState()->countAlias($this->refs[$value], $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); | |
| 608 | 638 | return $this->refs[$value]; |
| 609 | 639 | } |
| 610 | 640 | if (\in_array($value[0], ['!', '|', '>'], \true) && self::preg_match('/^(?:' . self::TAG_PATTERN . ' +)?' . self::BLOCK_SCALAR_HEADER_PATTERN . '$/', $value, $matches)) { |
| 611 | 641 | $modifiers = $matches['modifiers'] ?? ''; |
| @@ -620,18 +650,18 @@ | ||
| 620 | 650 | } |
| 621 | 651 | try { |
| 622 | 652 | if ('' !== $value && '{' === $value[0]) { |
| 623 | 653 | $cursor = \strlen(rtrim($this->currentLine)) - \strlen(rtrim($value)); |
| 624 | - return Inline::parse($this->lexInlineMapping($cursor), $flags, $this->refs); | |
| 654 | + return Inline::parse($this->lexInlineMapping($cursor), $flags, $this->refs, $this->state); | |
| 625 | 655 | } elseif ('' !== $value && '[' === $value[0]) { |
| 626 | 656 | $cursor = \strlen(rtrim($this->currentLine)) - \strlen(rtrim($value)); |
| 627 | - return Inline::parse($this->lexInlineSequence($cursor), $flags, $this->refs); | |
| 657 | + return Inline::parse($this->lexInlineSequence($cursor), $flags, $this->refs, $this->state); | |
| 628 | 658 | } |
| 629 | 659 | switch ($value[0] ?? '') { |
| 630 | 660 | case '"': |
| 631 | 661 | case "'": |
| 632 | 662 | $cursor = \strlen(rtrim($this->currentLine)) - \strlen(rtrim($value)); |
| 633 | - $parsedValue = Inline::parse($this->lexInlineQuotedString($cursor), $flags, $this->refs); | |
| 663 | + $parsedValue = Inline::parse($this->lexInlineQuotedString($cursor), $flags, $this->refs, $this->state); | |
| 634 | 664 | if (isset($this->currentLine[$cursor]) && preg_replace('/\s*(#.*)?$/A', '', substr($this->currentLine, $cursor))) { |
| 635 | 665 | throw new ParseException(sprintf('Unexpected characters near "%s".', substr($this->currentLine, $cursor))); |
| 636 | 666 | } |
| 637 | 667 | return $parsedValue; |
| @@ -657,9 +687,9 @@ | ||
| 657 | 687 | $previousLineBlank = \false; |
| 658 | 688 | } |
| 659 | 689 | } |
| 660 | 690 | Inline::$parsedLineNumber = $this->getRealCurrentLineNb(); |
| 661 | - $parsedValue = Inline::parse($value, $flags, $this->refs); | |
| 691 | + $parsedValue = Inline::parse($value, $flags, $this->refs, $this->state); | |
| 662 | 692 | if ('mapping' === $context && \is_string($parsedValue) && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && \false !== strpos($parsedValue, ': ')) { |
| 663 | 693 | throw new ParseException('A colon cannot be used in an unquoted mapping value.', $this->getRealCurrentLineNb() + 1, $value, $this->filename); |
| 664 | 694 | } |
| 665 | 695 | return $parsedValue; |
| @@ -824,12 +854,12 @@ | ||
| 824 | 854 | { |
| 825 | 855 | $value = str_replace(["\r\n", "\r"], "\n", $value); |
| 826 | 856 | // strip YAML header |
| 827 | 857 | $count = 0; |
| 828 | - $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count); | |
| 858 | + $value = preg_replace('#^%YAML[: ][\d.]++[^\n]*+\n#u', '', $value, -1, $count); | |
| 829 | 859 | $this->offset += $count; |
| 830 | 860 | // remove leading comments |
| 831 | - $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count); | |
| 861 | + $trimmedValue = preg_replace('#^(?:\#[^\n]*+\n)++#', '', $value, -1, $count); | |
| 832 | 862 | if (1 === $count) { |
| 833 | 863 | // items have been removed, update the offset |
| 834 | 864 | $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n"); |
| 835 | 865 | $value = $trimmedValue; |
| @@ -834,15 +864,15 @@ | ||
| 834 | 864 | $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n"); |
| 835 | 865 | $value = $trimmedValue; |
| 836 | 866 | } |
| 837 | 867 | // remove start of the document marker (---) |
| 838 | - $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count); | |
| 868 | + $trimmedValue = preg_replace('#^---[^\n]*+\n#', '', $value, -1, $count); | |
| 839 | 869 | if (1 === $count) { |
| 840 | 870 | // items have been removed, update the offset |
| 841 | 871 | $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n"); |
| 842 | 872 | $value = $trimmedValue; |
| 843 | 873 | // remove end of the document marker (...) |
| 844 | - $value = preg_replace('#\.\.\.\s*$#', '', $value); | |
| 874 | + $value = preg_replace('#\.\.\.\s*+$#', '', $value); | |
| 845 | 875 | } |
| 846 | 876 | return $value; |
| 847 | 877 | } |
| 848 | 878 | /** |