1 <?php
2
3 4 5 6 7 8 9 10
11
12 namespace Symfony\Component\Yaml;
13
14 use Symfony\Component\Yaml\Exception\ParseException;
15
16 17 18 19 20
21 class Parser
22 {
23 const = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
24
25 const FOLDED_SCALAR_PATTERN = self::BLOCK_SCALAR_HEADER_PATTERN;
26
27 private $offset = 0;
28 private $totalNumberOfLines;
29 private $lines = array();
30 private $currentLineNb = -1;
31 private $currentLine = '';
32 private $refs = array();
33 private $skippedLineNumbers = array();
34 private $locallySkippedLineNumbers = array();
35
36 37 38 39 40 41 42
43 public function __construct($offset = 0, $totalNumberOfLines = null, array $skippedLineNumbers = array())
44 {
45 $this->offset = $offset;
46 $this->totalNumberOfLines = $totalNumberOfLines;
47 $this->skippedLineNumbers = $skippedLineNumbers;
48 }
49
50 51 52 53 54 55 56 57 58 59 60 61
62 public function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false)
63 {
64 if (false === preg_match('//u', $value)) {
65 throw new ParseException('The YAML value does not appear to be valid UTF-8.');
66 }
67
68 $this->refs = array();
69
70 $mbEncoding = null;
71 $e = null;
72 $data = null;
73
74 if (2 & (int) ini_get('mbstring.func_overload')) {
75 $mbEncoding = mb_internal_encoding();
76 mb_internal_encoding('UTF-8');
77 }
78
79 try {
80 $data = $this->doParse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
81 } catch (\Exception $e) {
82 } catch (\Throwable $e) {
83 }
84
85 if (null !== $mbEncoding) {
86 mb_internal_encoding($mbEncoding);
87 }
88
89 $this->lines = array();
90 $this->currentLine = '';
91 $this->refs = array();
92 $this->skippedLineNumbers = array();
93 $this->locallySkippedLineNumbers = array();
94
95 if (null !== $e) {
96 throw $e;
97 }
98
99 return $data;
100 }
101
102 private function doParse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false)
103 {
104 $this->currentLineNb = -1;
105 $this->currentLine = '';
106 $value = $this->cleanup($value);
107 $this->lines = explode("\n", $value);
108 $this->locallySkippedLineNumbers = array();
109
110 if (null === $this->totalNumberOfLines) {
111 $this->totalNumberOfLines = count($this->lines);
112 }
113
114 $data = array();
115 $context = null;
116 $allowOverwrite = false;
117
118 while ($this->moveToNextLine()) {
119 if ($this->isCurrentLineEmpty()) {
120 continue;
121 }
122
123
124 if ("\t" === $this->currentLine[0]) {
125 throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
126 }
127
128 $isRef = $mergeNode = false;
129 if (self::preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+))?$#u', rtrim($this->currentLine), $values)) {
130 if ($context && 'mapping' == $context) {
131 throw new ParseException('You cannot define a sequence item when in a mapping', $this->getRealCurrentLineNb() + 1, $this->currentLine);
132 }
133 $context = 'sequence';
134
135 if (isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
136 $isRef = $matches['ref'];
137 $values['value'] = $matches['value'];
138 }
139
140
141 if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
142 $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $exceptionOnInvalidType, $objectSupport, $objectForMap);
143 } else {
144 if (isset($values['leadspaces'])
145 && self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+))?$#u', rtrim($values['value']), $matches)
146 ) {
147
148 $block = $values['value'];
149 if ($this->isNextLineIndented()) {
150 $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + strlen($values['leadspaces']) + 1);
151 }
152
153 $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $exceptionOnInvalidType, $objectSupport, $objectForMap);
154 } else {
155 $data[] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context);
156 }
157 }
158 if ($isRef) {
159 $this->refs[$isRef] = end($data);
160 }
161 } elseif (
162 self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\[\{].*?) *\:(\s+(?P<value>.+))?$#u', rtrim($this->currentLine), $values)
163 && (false === strpos($values['key'], ' #') || in_array($values['key'][0], array('"', "'")))
164 ) {
165 if ($context && 'sequence' == $context) {
166 throw new ParseException('You cannot define a mapping item when in a sequence', $this->currentLineNb + 1, $this->currentLine);
167 }
168 $context = 'mapping';
169
170
171 Inline::parse(null, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
172 try {
173 $key = Inline::parseScalar($values['key']);
174 } catch (ParseException $e) {
175 $e->setParsedLine($this->getRealCurrentLineNb() + 1);
176 $e->setSnippet($this->currentLine);
177
178 throw $e;
179 }
180
181
182 if (is_float($key)) {
183 $key = (string) $key;
184 }
185
186 if ('<<' === $key) {
187 $mergeNode = true;
188 $allowOverwrite = true;
189 if (isset($values['value']) && 0 === strpos($values['value'], '*')) {
190 $refName = substr($values['value'], 1);
191 if (!array_key_exists($refName, $this->refs)) {
192 throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine);
193 }
194
195 $refValue = $this->refs[$refName];
196
197 if (!is_array($refValue)) {
198 throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
199 }
200
201 $data += $refValue;
202 } else {
203 if (isset($values['value']) && $values['value'] !== '') {
204 $value = $values['value'];
205 } else {
206 $value = $this->getNextEmbedBlock();
207 }
208 $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
209
210 if (!is_array($parsed)) {
211 throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
212 }
213
214 if (isset($parsed[0])) {
215
216
217
218 foreach ($parsed as $parsedItem) {
219 if (!is_array($parsedItem)) {
220 throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem);
221 }
222
223 $data += $parsedItem;
224 }
225 } else {
226
227
228 $data += $parsed;
229 }
230 }
231 } elseif (isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
232 $isRef = $matches['ref'];
233 $values['value'] = $matches['value'];
234 }
235
236 if ($mergeNode) {
237
238 } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
239
240
241 if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
242
243
244 if ($allowOverwrite || !isset($data[$key])) {
245 $data[$key] = null;
246 }
247 } else {
248 $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap);
249
250
251 if ($allowOverwrite || !isset($data[$key])) {
252 $data[$key] = $value;
253 }
254 }
255 } else {
256 $value = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context);
257
258
259 if ($allowOverwrite || !isset($data[$key])) {
260 $data[$key] = $value;
261 }
262 }
263 if ($isRef) {
264 $this->refs[$isRef] = $data[$key];
265 }
266 } else {
267
268 if ('---' === $this->currentLine) {
269 throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine);
270 }
271
272
273 if (is_string($value) && $this->lines[0] === trim($value)) {
274 try {
275 $value = Inline::parse($this->lines[0], $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
276 } catch (ParseException $e) {
277 $e->setParsedLine($this->getRealCurrentLineNb() + 1);
278 $e->setSnippet($this->currentLine);
279
280 throw $e;
281 }
282
283 return $value;
284 }
285
286 throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
287 }
288 }
289
290 if ($objectForMap && !is_object($data) && 'mapping' === $context) {
291 $object = new \stdClass();
292
293 foreach ($data as $key => $value) {
294 $object->$key = $value;
295 }
296
297 $data = $object;
298 }
299
300 return empty($data) ? null : $data;
301 }
302
303 private function parseBlock($offset, $yaml, $exceptionOnInvalidType, $objectSupport, $objectForMap)
304 {
305 $skippedLineNumbers = $this->skippedLineNumbers;
306
307 foreach ($this->locallySkippedLineNumbers as $lineNumber) {
308 if ($lineNumber < $offset) {
309 continue;
310 }
311
312 $skippedLineNumbers[] = $lineNumber;
313 }
314
315 $parser = new self($offset, $this->totalNumberOfLines, $skippedLineNumbers);
316 $parser->refs = &$this->refs;
317
318 return $parser->doParse($yaml, $exceptionOnInvalidType, $objectSupport, $objectForMap);
319 }
320
321 322 323 324 325
326 private function getRealCurrentLineNb()
327 {
328 $realCurrentLineNumber = $this->currentLineNb + $this->offset;
329
330 foreach ($this->skippedLineNumbers as $skippedLineNumber) {
331 if ($skippedLineNumber > $realCurrentLineNumber) {
332 break;
333 }
334
335 ++$realCurrentLineNumber;
336 }
337
338 return $realCurrentLineNumber;
339 }
340
341 342 343 344 345
346 private function getCurrentLineIndentation()
347 {
348 return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' '));
349 }
350
351 352 353 354 355 356 357 358 359 360
361 private function getNextEmbedBlock($indentation = null, $inSequence = false)
362 {
363 $oldLineIndentation = $this->getCurrentLineIndentation();
364 $blockScalarIndentations = array();
365
366 if ($this->isBlockScalarHeader()) {
367 $blockScalarIndentations[] = $this->getCurrentLineIndentation();
368 }
369
370 if (!$this->moveToNextLine()) {
371 return;
372 }
373
374 if (null === $indentation) {
375 $newIndent = $this->getCurrentLineIndentation();
376
377 $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem();
378
379 if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
380 throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
381 }
382 } else {
383 $newIndent = $indentation;
384 }
385
386 $data = array();
387 if ($this->getCurrentLineIndentation() >= $newIndent) {
388 $data[] = substr($this->currentLine, $newIndent);
389 } else {
390 $this->moveToPreviousLine();
391
392 return;
393 }
394
395 if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) {
396
397
398 $this->moveToPreviousLine();
399
400 return;
401 }
402
403 $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
404
405 if (empty($blockScalarIndentations) && $this->isBlockScalarHeader()) {
406 $blockScalarIndentations[] = $this->getCurrentLineIndentation();
407 }
408
409 $previousLineIndentation = $this->getCurrentLineIndentation();
410
411 while ($this->moveToNextLine()) {
412 $indent = $this->getCurrentLineIndentation();
413
414
415 if (!empty($blockScalarIndentations) && $indent < $previousLineIndentation && trim($this->currentLine) !== '') {
416 foreach ($blockScalarIndentations as $key => $blockScalarIndentation) {
417 if ($blockScalarIndentation >= $this->getCurrentLineIndentation()) {
418 unset($blockScalarIndentations[$key]);
419 }
420 }
421 }
422
423 if (empty($blockScalarIndentations) && !$this->isCurrentLineComment() && $this->isBlockScalarHeader()) {
424 $blockScalarIndentations[] = $this->getCurrentLineIndentation();
425 }
426
427 $previousLineIndentation = $indent;
428
429 if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {
430 $this->moveToPreviousLine();
431 break;
432 }
433
434 if ($this->isCurrentLineBlank()) {
435 $data[] = substr($this->currentLine, $newIndent);
436 continue;
437 }
438
439
440 if (empty($blockScalarIndentations) && $this->isCurrentLineComment()) {
441
442
443
444
445
446
447 $this->locallySkippedLineNumbers[] = $this->getRealCurrentLineNb();
448
449 continue;
450 }
451
452 if ($indent >= $newIndent) {
453 $data[] = substr($this->currentLine, $newIndent);
454 } elseif (0 == $indent) {
455 $this->moveToPreviousLine();
456
457 break;
458 } else {
459 throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
460 }
461 }
462
463 return implode("\n", $data);
464 }
465
466 467 468 469 470
471 private function moveToNextLine()
472 {
473 if ($this->currentLineNb >= count($this->lines) - 1) {
474 return false;
475 }
476
477 $this->currentLine = $this->lines[++$this->currentLineNb];
478
479 return true;
480 }
481
482 483 484 485 486
487 private function moveToPreviousLine()
488 {
489 if ($this->currentLineNb < 1) {
490 return false;
491 }
492
493 $this->currentLine = $this->lines[--$this->currentLineNb];
494
495 return true;
496 }
497
498 499 500 501 502 503 504 505 506 507 508 509 510
511 private function parseValue($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $context)
512 {
513 if (0 === strpos($value, '*')) {
514 if (false !== $pos = strpos($value, '#')) {
515 $value = substr($value, 1, $pos - 2);
516 } else {
517 $value = substr($value, 1);
518 }
519
520 if (!array_key_exists($value, $this->refs)) {
521 throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine);
522 }
523
524 return $this->refs[$value];
525 }
526
527 if (self::preg_match('/^'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) {
528 $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
529
530 return $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers));
531 }
532
533 try {
534 $parsedValue = Inline::parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
535
536 if ('mapping' === $context && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) {
537 @trigger_error(sprintf('Using a colon in the unquoted mapping value "%s" in line %d is deprecated since Symfony 2.8 and will throw a ParseException in 3.0.', $value, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
538
539
540
541 }
542
543 return $parsedValue;
544 } catch (ParseException $e) {
545 $e->setParsedLine($this->getRealCurrentLineNb() + 1);
546 $e->setSnippet($this->currentLine);
547
548 throw $e;
549 }
550 }
551
552 553 554 555 556 557 558 559 560
561 private function parseBlockScalar($style, $chomping = '', $indentation = 0)
562 {
563 $notEOF = $this->moveToNextLine();
564 if (!$notEOF) {
565 return '';
566 }
567
568 $isCurrentLineBlank = $this->isCurrentLineBlank();
569 $blockLines = array();
570
571
572 while ($notEOF && $isCurrentLineBlank) {
573
574 if ($notEOF = $this->moveToNextLine()) {
575 $blockLines[] = '';
576 $isCurrentLineBlank = $this->isCurrentLineBlank();
577 }
578 }
579
580
581 if (0 === $indentation) {
582 if (self::preg_match('/^ +/', $this->currentLine, $matches)) {
583 $indentation = strlen($matches[0]);
584 }
585 }
586
587 if ($indentation > 0) {
588 $pattern = sprintf('/^ {%d}(.*)$/', $indentation);
589
590 while (
591 $notEOF && (
592 $isCurrentLineBlank ||
593 self::preg_match($pattern, $this->currentLine, $matches)
594 )
595 ) {
596 if ($isCurrentLineBlank && strlen($this->currentLine) > $indentation) {
597 $blockLines[] = substr($this->currentLine, $indentation);
598 } elseif ($isCurrentLineBlank) {
599 $blockLines[] = '';
600 } else {
601 $blockLines[] = $matches[1];
602 }
603
604
605 if ($notEOF = $this->moveToNextLine()) {
606 $isCurrentLineBlank = $this->isCurrentLineBlank();
607 }
608 }
609 } elseif ($notEOF) {
610 $blockLines[] = '';
611 }
612
613 if ($notEOF) {
614 $blockLines[] = '';
615 $this->moveToPreviousLine();
616 } elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) {
617 $blockLines[] = '';
618 }
619
620
621 if ('>' === $style) {
622 $text = '';
623 $previousLineIndented = false;
624 $previousLineBlank = false;
625
626 for ($i = 0, $blockLinesCount = count($blockLines); $i < $blockLinesCount; ++$i) {
627 if ('' === $blockLines[$i]) {
628 $text .= "\n";
629 $previousLineIndented = false;
630 $previousLineBlank = true;
631 } elseif (' ' === $blockLines[$i][0]) {
632 $text .= "\n".$blockLines[$i];
633 $previousLineIndented = true;
634 $previousLineBlank = false;
635 } elseif ($previousLineIndented) {
636 $text .= "\n".$blockLines[$i];
637 $previousLineIndented = false;
638 $previousLineBlank = false;
639 } elseif ($previousLineBlank || 0 === $i) {
640 $text .= $blockLines[$i];
641 $previousLineIndented = false;
642 $previousLineBlank = false;
643 } else {
644 $text .= ' '.$blockLines[$i];
645 $previousLineIndented = false;
646 $previousLineBlank = false;
647 }
648 }
649 } else {
650 $text = implode("\n", $blockLines);
651 }
652
653
654 if ('' === $chomping) {
655 $text = preg_replace('/\n+$/', "\n", $text);
656 } elseif ('-' === $chomping) {
657 $text = preg_replace('/\n+$/', '', $text);
658 }
659
660 return $text;
661 }
662
663 664 665 666 667
668 private function isNextLineIndented()
669 {
670 $currentIndentation = $this->getCurrentLineIndentation();
671 $EOF = !$this->moveToNextLine();
672
673 while (!$EOF && $this->isCurrentLineEmpty()) {
674 $EOF = !$this->moveToNextLine();
675 }
676
677 if ($EOF) {
678 return false;
679 }
680
681 $ret = $this->getCurrentLineIndentation() > $currentIndentation;
682
683 $this->moveToPreviousLine();
684
685 return $ret;
686 }
687
688 689 690 691 692
693 private function isCurrentLineEmpty()
694 {
695 return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
696 }
697
698 699 700 701 702
703 private function isCurrentLineBlank()
704 {
705 return '' == trim($this->currentLine, ' ');
706 }
707
708 709 710 711 712
713 private function ()
714 {
715
716 $ltrimmedLine = ltrim($this->currentLine, ' ');
717
718 return '' !== $ltrimmedLine && $ltrimmedLine[0] === '#';
719 }
720
721 private function isCurrentLineLastLineInDocument()
722 {
723 return ($this->offset + $this->currentLineNb) >= ($this->totalNumberOfLines - 1);
724 }
725
726 727 728 729 730 731 732
733 private function cleanup($value)
734 {
735 $value = str_replace(array("\r\n", "\r"), "\n", $value);
736
737
738 $count = 0;
739 $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count);
740 $this->offset += $count;
741
742
743 $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
744 if ($count == 1) {
745
746 $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
747 $value = $trimmedValue;
748 }
749
750
751 $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
752 if ($count == 1) {
753
754 $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
755 $value = $trimmedValue;
756
757
758 $value = preg_replace('#\.\.\.\s*$#', '', $value);
759 }
760
761 return $value;
762 }
763
764 765 766 767 768
769 private function isNextLineUnIndentedCollection()
770 {
771 $currentIndentation = $this->getCurrentLineIndentation();
772 $notEOF = $this->moveToNextLine();
773
774 while ($notEOF && $this->isCurrentLineEmpty()) {
775 $notEOF = $this->moveToNextLine();
776 }
777
778 if (false === $notEOF) {
779 return false;
780 }
781
782 $ret = $this->getCurrentLineIndentation() === $currentIndentation && $this->isStringUnIndentedCollectionItem();
783
784 $this->moveToPreviousLine();
785
786 return $ret;
787 }
788
789 790 791 792 793
794 private function isStringUnIndentedCollectionItem()
795 {
796 return '-' === rtrim($this->currentLine) || 0 === strpos($this->currentLine, '- ');
797 }
798
799 800 801 802 803
804 private function ()
805 {
806 return (bool) self::preg_match('~'.self::BLOCK_SCALAR_HEADER_PATTERN.'$~', $this->currentLine);
807 }
808
809 810 811 812 813 814 815 816 817 818 819 820 821
822 public static function preg_match($pattern, $subject, &$matches = null, $flags = 0, $offset = 0)
823 {
824 if (false === $ret = preg_match($pattern, $subject, $matches, $flags, $offset)) {
825 switch (preg_last_error()) {
826 case PREG_INTERNAL_ERROR:
827 $error = 'Internal PCRE error.';
828 break;
829 case PREG_BACKTRACK_LIMIT_ERROR:
830 $error = 'pcre.backtrack_limit reached.';
831 break;
832 case PREG_RECURSION_LIMIT_ERROR:
833 $error = 'pcre.recursion_limit reached.';
834 break;
835 case PREG_BAD_UTF8_ERROR:
836 $error = 'Malformed UTF-8 data.';
837 break;
838 case PREG_BAD_UTF8_OFFSET_ERROR:
839 $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
840 break;
841 default:
842 $error = 'Error.';
843 }
844
845 throw new ParseException($error);
846 }
847
848 return $ret;
849 }
850 }
851