Security update for Core, with self-updated composer
[yaffs-website] / vendor / symfony / yaml / Parser.php
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
12 namespace Symfony\Component\Yaml;
13
14 use Symfony\Component\Yaml\Exception\ParseException;
15
16 /**
17  * Parser parses YAML strings to convert them to PHP arrays.
18  *
19  * @author Fabien Potencier <fabien@symfony.com>
20  */
21 class Parser
22 {
23     const TAG_PATTERN = '((?P<tag>![\w!.\/:-]+) +)?';
24     const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
25
26     private $offset = 0;
27     private $totalNumberOfLines;
28     private $lines = array();
29     private $currentLineNb = -1;
30     private $currentLine = '';
31     private $refs = array();
32     private $skippedLineNumbers = array();
33     private $locallySkippedLineNumbers = array();
34
35     /**
36      * Constructor.
37      *
38      * @param int      $offset             The offset of YAML document (used for line numbers in error messages)
39      * @param int|null $totalNumberOfLines The overall number of lines being parsed
40      * @param int[]    $skippedLineNumbers Number of comment lines that have been skipped by the parser
41      */
42     public function __construct($offset = 0, $totalNumberOfLines = null, array $skippedLineNumbers = array())
43     {
44         $this->offset = $offset;
45         $this->totalNumberOfLines = $totalNumberOfLines;
46         $this->skippedLineNumbers = $skippedLineNumbers;
47     }
48
49     /**
50      * Parses a YAML string to a PHP value.
51      *
52      * @param string $value A YAML string
53      * @param int    $flags A bit field of PARSE_* constants to customize the YAML parser behavior
54      *
55      * @return mixed A PHP value
56      *
57      * @throws ParseException If the YAML is not valid
58      */
59     public function parse($value, $flags = 0)
60     {
61         if (is_bool($flags)) {
62             @trigger_error('Passing a boolean flag to toggle exception handling is deprecated since version 3.1 and will be removed in 4.0. Use the Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE flag instead.', E_USER_DEPRECATED);
63
64             if ($flags) {
65                 $flags = Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE;
66             } else {
67                 $flags = 0;
68             }
69         }
70
71         if (func_num_args() >= 3) {
72             @trigger_error('Passing a boolean flag to toggle object support is deprecated since version 3.1 and will be removed in 4.0. Use the Yaml::PARSE_OBJECT flag instead.', E_USER_DEPRECATED);
73
74             if (func_get_arg(2)) {
75                 $flags |= Yaml::PARSE_OBJECT;
76             }
77         }
78
79         if (func_num_args() >= 4) {
80             @trigger_error('Passing a boolean flag to toggle object for map support is deprecated since version 3.1 and will be removed in 4.0. Use the Yaml::PARSE_OBJECT_FOR_MAP flag instead.', E_USER_DEPRECATED);
81
82             if (func_get_arg(3)) {
83                 $flags |= Yaml::PARSE_OBJECT_FOR_MAP;
84             }
85         }
86
87         if (false === preg_match('//u', $value)) {
88             throw new ParseException('The YAML value does not appear to be valid UTF-8.');
89         }
90
91         $this->refs = array();
92
93         $mbEncoding = null;
94         $e = null;
95         $data = null;
96
97         if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) {
98             $mbEncoding = mb_internal_encoding();
99             mb_internal_encoding('UTF-8');
100         }
101
102         try {
103             $data = $this->doParse($value, $flags);
104         } catch (\Exception $e) {
105         } catch (\Throwable $e) {
106         }
107
108         if (null !== $mbEncoding) {
109             mb_internal_encoding($mbEncoding);
110         }
111
112         $this->lines = array();
113         $this->currentLine = '';
114         $this->refs = array();
115         $this->skippedLineNumbers = array();
116         $this->locallySkippedLineNumbers = array();
117
118         if (null !== $e) {
119             throw $e;
120         }
121
122         return $data;
123     }
124
125     private function doParse($value, $flags)
126     {
127         $this->currentLineNb = -1;
128         $this->currentLine = '';
129         $value = $this->cleanup($value);
130         $this->lines = explode("\n", $value);
131         $this->locallySkippedLineNumbers = array();
132
133         if (null === $this->totalNumberOfLines) {
134             $this->totalNumberOfLines = count($this->lines);
135         }
136
137         $data = array();
138         $context = null;
139         $allowOverwrite = false;
140
141         while ($this->moveToNextLine()) {
142             if ($this->isCurrentLineEmpty()) {
143                 continue;
144             }
145
146             // tab?
147             if ("\t" === $this->currentLine[0]) {
148                 throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
149             }
150
151             $isRef = $mergeNode = false;
152             if (self::preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+))?$#u', rtrim($this->currentLine), $values)) {
153                 if ($context && 'mapping' == $context) {
154                     throw new ParseException('You cannot define a sequence item when in a mapping', $this->getRealCurrentLineNb() + 1, $this->currentLine);
155                 }
156                 $context = 'sequence';
157
158                 if (isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
159                     $isRef = $matches['ref'];
160                     $values['value'] = $matches['value'];
161                 }
162
163                 // array
164                 if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
165                     $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $flags);
166                 } else {
167                     if (isset($values['leadspaces'])
168                         && self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+))?$#u', rtrim($values['value']), $matches)
169                     ) {
170                         // this is a compact notation element, add to next block and parse
171                         $block = $values['value'];
172                         if ($this->isNextLineIndented()) {
173                             $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + strlen($values['leadspaces']) + 1);
174                         }
175
176                         $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $flags);
177                     } else {
178                         $data[] = $this->parseValue($values['value'], $flags, $context);
179                     }
180                 }
181                 if ($isRef) {
182                     $this->refs[$isRef] = end($data);
183                 }
184             } elseif (
185                 self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\[\{].*?) *\:(\s+(?P<value>.+))?$#u', rtrim($this->currentLine), $values)
186                 && (false === strpos($values['key'], ' #') || in_array($values['key'][0], array('"', "'")))
187             ) {
188                 if ($context && 'sequence' == $context) {
189                     throw new ParseException('You cannot define a mapping item when in a sequence', $this->currentLineNb + 1, $this->currentLine);
190                 }
191                 $context = 'mapping';
192
193                 // force correct settings
194                 Inline::parse(null, $flags, $this->refs);
195                 try {
196                     Inline::$parsedLineNumber = $this->getRealCurrentLineNb();
197                     $key = Inline::parseScalar($values['key']);
198                 } catch (ParseException $e) {
199                     $e->setParsedLine($this->getRealCurrentLineNb() + 1);
200                     $e->setSnippet($this->currentLine);
201
202                     throw $e;
203                 }
204
205                 // Convert float keys to strings, to avoid being converted to integers by PHP
206                 if (is_float($key)) {
207                     $key = (string) $key;
208                 }
209
210                 if ('<<' === $key) {
211                     $mergeNode = true;
212                     $allowOverwrite = true;
213                     if (isset($values['value']) && 0 === strpos($values['value'], '*')) {
214                         $refName = substr($values['value'], 1);
215                         if (!array_key_exists($refName, $this->refs)) {
216                             throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine);
217                         }
218
219                         $refValue = $this->refs[$refName];
220
221                         if (!is_array($refValue)) {
222                             throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
223                         }
224
225                         $data += $refValue; // array union
226                     } else {
227                         if (isset($values['value']) && $values['value'] !== '') {
228                             $value = $values['value'];
229                         } else {
230                             $value = $this->getNextEmbedBlock();
231                         }
232                         $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $flags);
233
234                         if (!is_array($parsed)) {
235                             throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
236                         }
237
238                         if (isset($parsed[0])) {
239                             // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
240                             // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
241                             // in the sequence override keys specified in later mapping nodes.
242                             foreach ($parsed as $parsedItem) {
243                                 if (!is_array($parsedItem)) {
244                                     throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem);
245                                 }
246
247                                 $data += $parsedItem; // array union
248                             }
249                         } else {
250                             // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
251                             // current mapping, unless the key already exists in it.
252                             $data += $parsed; // array union
253                         }
254                     }
255                 } elseif (isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
256                     $isRef = $matches['ref'];
257                     $values['value'] = $matches['value'];
258                 }
259
260                 if ($mergeNode) {
261                     // Merge keys
262                 } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
263                     // hash
264                     // if next line is less indented or equal, then it means that the current value is null
265                     if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
266                         // Spec: Keys MUST be unique; first one wins.
267                         // But overwriting is allowed when a merge node is used in current block.
268                         if ($allowOverwrite || !isset($data[$key])) {
269                             $data[$key] = null;
270                         } else {
271                             @trigger_error(sprintf('Duplicate key "%s" detected on line %d whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
272                         }
273                     } else {
274                         // remember the parsed line number here in case we need it to provide some contexts in error messages below
275                         $realCurrentLineNbKey = $this->getRealCurrentLineNb();
276                         $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $flags);
277                         // Spec: Keys MUST be unique; first one wins.
278                         // But overwriting is allowed when a merge node is used in current block.
279                         if ($allowOverwrite || !isset($data[$key])) {
280                             $data[$key] = $value;
281                         } else {
282                             @trigger_error(sprintf('Duplicate key "%s" detected on line %d whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key, $realCurrentLineNbKey + 1), E_USER_DEPRECATED);
283                         }
284                     }
285                 } else {
286                     $value = $this->parseValue($values['value'], $flags, $context);
287                     // Spec: Keys MUST be unique; first one wins.
288                     // But overwriting is allowed when a merge node is used in current block.
289                     if ($allowOverwrite || !isset($data[$key])) {
290                         $data[$key] = $value;
291                     } else {
292                         @trigger_error(sprintf('Duplicate key "%s" detected on line %d whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
293                     }
294                 }
295                 if ($isRef) {
296                     $this->refs[$isRef] = $data[$key];
297                 }
298             } else {
299                 // multiple documents are not supported
300                 if ('---' === $this->currentLine) {
301                     throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine);
302                 }
303
304                 // 1-liner optionally followed by newline(s)
305                 if (is_string($value) && $this->lines[0] === trim($value)) {
306                     try {
307                         Inline::$parsedLineNumber = $this->getRealCurrentLineNb();
308                         $value = Inline::parse($this->lines[0], $flags, $this->refs);
309                     } catch (ParseException $e) {
310                         $e->setParsedLine($this->getRealCurrentLineNb() + 1);
311                         $e->setSnippet($this->currentLine);
312
313                         throw $e;
314                     }
315
316                     return $value;
317                 }
318
319                 throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
320             }
321         }
322
323         if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && !is_object($data) && 'mapping' === $context) {
324             $object = new \stdClass();
325
326             foreach ($data as $key => $value) {
327                 $object->$key = $value;
328             }
329
330             $data = $object;
331         }
332
333         return empty($data) ? null : $data;
334     }
335
336     private function parseBlock($offset, $yaml, $flags)
337     {
338         $skippedLineNumbers = $this->skippedLineNumbers;
339
340         foreach ($this->locallySkippedLineNumbers as $lineNumber) {
341             if ($lineNumber < $offset) {
342                 continue;
343             }
344
345             $skippedLineNumbers[] = $lineNumber;
346         }
347
348         $parser = new self($offset, $this->totalNumberOfLines, $skippedLineNumbers);
349         $parser->refs = &$this->refs;
350
351         return $parser->doParse($yaml, $flags);
352     }
353
354     /**
355      * Returns the current line number (takes the offset into account).
356      *
357      * @return int The current line number
358      */
359     private function getRealCurrentLineNb()
360     {
361         $realCurrentLineNumber = $this->currentLineNb + $this->offset;
362
363         foreach ($this->skippedLineNumbers as $skippedLineNumber) {
364             if ($skippedLineNumber > $realCurrentLineNumber) {
365                 break;
366             }
367
368             ++$realCurrentLineNumber;
369         }
370
371         return $realCurrentLineNumber;
372     }
373
374     /**
375      * Returns the current line indentation.
376      *
377      * @return int The current line indentation
378      */
379     private function getCurrentLineIndentation()
380     {
381         return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' '));
382     }
383
384     /**
385      * Returns the next embed block of YAML.
386      *
387      * @param int  $indentation The indent level at which the block is to be read, or null for default
388      * @param bool $inSequence  True if the enclosing data structure is a sequence
389      *
390      * @return string A YAML string
391      *
392      * @throws ParseException When indentation problem are detected
393      */
394     private function getNextEmbedBlock($indentation = null, $inSequence = false)
395     {
396         $oldLineIndentation = $this->getCurrentLineIndentation();
397         $blockScalarIndentations = array();
398
399         if ($this->isBlockScalarHeader()) {
400             $blockScalarIndentations[] = $this->getCurrentLineIndentation();
401         }
402
403         if (!$this->moveToNextLine()) {
404             return;
405         }
406
407         if (null === $indentation) {
408             $newIndent = $this->getCurrentLineIndentation();
409
410             $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem();
411
412             if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
413                 throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
414             }
415         } else {
416             $newIndent = $indentation;
417         }
418
419         $data = array();
420         if ($this->getCurrentLineIndentation() >= $newIndent) {
421             $data[] = substr($this->currentLine, $newIndent);
422         } else {
423             $this->moveToPreviousLine();
424
425             return;
426         }
427
428         if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) {
429             // the previous line contained a dash but no item content, this line is a sequence item with the same indentation
430             // and therefore no nested list or mapping
431             $this->moveToPreviousLine();
432
433             return;
434         }
435
436         $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
437
438         if (empty($blockScalarIndentations) && $this->isBlockScalarHeader()) {
439             $blockScalarIndentations[] = $this->getCurrentLineIndentation();
440         }
441
442         $previousLineIndentation = $this->getCurrentLineIndentation();
443
444         while ($this->moveToNextLine()) {
445             $indent = $this->getCurrentLineIndentation();
446
447             // terminate all block scalars that are more indented than the current line
448             if (!empty($blockScalarIndentations) && $indent < $previousLineIndentation && trim($this->currentLine) !== '') {
449                 foreach ($blockScalarIndentations as $key => $blockScalarIndentation) {
450                     if ($blockScalarIndentation >= $this->getCurrentLineIndentation()) {
451                         unset($blockScalarIndentations[$key]);
452                     }
453                 }
454             }
455
456             if (empty($blockScalarIndentations) && !$this->isCurrentLineComment() && $this->isBlockScalarHeader()) {
457                 $blockScalarIndentations[] = $this->getCurrentLineIndentation();
458             }
459
460             $previousLineIndentation = $indent;
461
462             if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {
463                 $this->moveToPreviousLine();
464                 break;
465             }
466
467             if ($this->isCurrentLineBlank()) {
468                 $data[] = substr($this->currentLine, $newIndent);
469                 continue;
470             }
471
472             // we ignore "comment" lines only when we are not inside a scalar block
473             if (empty($blockScalarIndentations) && $this->isCurrentLineComment()) {
474                 // remember ignored comment lines (they are used later in nested
475                 // parser calls to determine real line numbers)
476                 //
477                 // CAUTION: beware to not populate the global property here as it
478                 // will otherwise influence the getRealCurrentLineNb() call here
479                 // for consecutive comment lines and subsequent embedded blocks
480                 $this->locallySkippedLineNumbers[] = $this->getRealCurrentLineNb();
481
482                 continue;
483             }
484
485             if ($indent >= $newIndent) {
486                 $data[] = substr($this->currentLine, $newIndent);
487             } elseif (0 == $indent) {
488                 $this->moveToPreviousLine();
489
490                 break;
491             } else {
492                 throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
493             }
494         }
495
496         return implode("\n", $data);
497     }
498
499     /**
500      * Moves the parser to the next line.
501      *
502      * @return bool
503      */
504     private function moveToNextLine()
505     {
506         if ($this->currentLineNb >= count($this->lines) - 1) {
507             return false;
508         }
509
510         $this->currentLine = $this->lines[++$this->currentLineNb];
511
512         return true;
513     }
514
515     /**
516      * Moves the parser to the previous line.
517      *
518      * @return bool
519      */
520     private function moveToPreviousLine()
521     {
522         if ($this->currentLineNb < 1) {
523             return false;
524         }
525
526         $this->currentLine = $this->lines[--$this->currentLineNb];
527
528         return true;
529     }
530
531     /**
532      * Parses a YAML value.
533      *
534      * @param string $value   A YAML value
535      * @param int    $flags   A bit field of PARSE_* constants to customize the YAML parser behavior
536      * @param string $context The parser context (either sequence or mapping)
537      *
538      * @return mixed A PHP value
539      *
540      * @throws ParseException When reference does not exist
541      */
542     private function parseValue($value, $flags, $context)
543     {
544         if (0 === strpos($value, '*')) {
545             if (false !== $pos = strpos($value, '#')) {
546                 $value = substr($value, 1, $pos - 2);
547             } else {
548                 $value = substr($value, 1);
549             }
550
551             if (!array_key_exists($value, $this->refs)) {
552                 throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine);
553             }
554
555             return $this->refs[$value];
556         }
557
558         if (self::preg_match('/^'.self::TAG_PATTERN.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) {
559             $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
560
561             $data = $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers));
562
563             if (isset($matches['tag']) && '!!binary' === $matches['tag']) {
564                 return Inline::evaluateBinaryScalar($data);
565             }
566
567             return $data;
568         }
569
570         try {
571             $quotation = '' !== $value && ('"' === $value[0] || "'" === $value[0]) ? $value[0] : null;
572
573             // do not take following lines into account when the current line is a quoted single line value
574             if (null !== $quotation && preg_match('/^'.$quotation.'.*'.$quotation.'(\s*#.*)?$/', $value)) {
575                 return Inline::parse($value, $flags, $this->refs);
576             }
577
578             while ($this->moveToNextLine()) {
579                 // unquoted strings end before the first unindented line
580                 if (null === $quotation && $this->getCurrentLineIndentation() === 0) {
581                     $this->moveToPreviousLine();
582
583                     break;
584                 }
585
586                 $value .= ' '.trim($this->currentLine);
587
588                 // quoted string values end with a line that is terminated with the quotation character
589                 if ('' !== $this->currentLine && substr($this->currentLine, -1) === $quotation) {
590                     break;
591                 }
592             }
593
594             Inline::$parsedLineNumber = $this->getRealCurrentLineNb();
595             $parsedValue = Inline::parse($value, $flags, $this->refs);
596
597             if ('mapping' === $context && is_string($parsedValue) && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) {
598                 throw new ParseException('A colon cannot be used in an unquoted mapping value.');
599             }
600
601             return $parsedValue;
602         } catch (ParseException $e) {
603             $e->setParsedLine($this->getRealCurrentLineNb() + 1);
604             $e->setSnippet($this->currentLine);
605
606             throw $e;
607         }
608     }
609
610     /**
611      * Parses a block scalar.
612      *
613      * @param string $style       The style indicator that was used to begin this block scalar (| or >)
614      * @param string $chomping    The chomping indicator that was used to begin this block scalar (+ or -)
615      * @param int    $indentation The indentation indicator that was used to begin this block scalar
616      *
617      * @return string The text value
618      */
619     private function parseBlockScalar($style, $chomping = '', $indentation = 0)
620     {
621         $notEOF = $this->moveToNextLine();
622         if (!$notEOF) {
623             return '';
624         }
625
626         $isCurrentLineBlank = $this->isCurrentLineBlank();
627         $blockLines = array();
628
629         // leading blank lines are consumed before determining indentation
630         while ($notEOF && $isCurrentLineBlank) {
631             // newline only if not EOF
632             if ($notEOF = $this->moveToNextLine()) {
633                 $blockLines[] = '';
634                 $isCurrentLineBlank = $this->isCurrentLineBlank();
635             }
636         }
637
638         // determine indentation if not specified
639         if (0 === $indentation) {
640             if (self::preg_match('/^ +/', $this->currentLine, $matches)) {
641                 $indentation = strlen($matches[0]);
642             }
643         }
644
645         if ($indentation > 0) {
646             $pattern = sprintf('/^ {%d}(.*)$/', $indentation);
647
648             while (
649                 $notEOF && (
650                     $isCurrentLineBlank ||
651                     self::preg_match($pattern, $this->currentLine, $matches)
652                 )
653             ) {
654                 if ($isCurrentLineBlank && strlen($this->currentLine) > $indentation) {
655                     $blockLines[] = substr($this->currentLine, $indentation);
656                 } elseif ($isCurrentLineBlank) {
657                     $blockLines[] = '';
658                 } else {
659                     $blockLines[] = $matches[1];
660                 }
661
662                 // newline only if not EOF
663                 if ($notEOF = $this->moveToNextLine()) {
664                     $isCurrentLineBlank = $this->isCurrentLineBlank();
665                 }
666             }
667         } elseif ($notEOF) {
668             $blockLines[] = '';
669         }
670
671         if ($notEOF) {
672             $blockLines[] = '';
673             $this->moveToPreviousLine();
674         } elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) {
675             $blockLines[] = '';
676         }
677
678         // folded style
679         if ('>' === $style) {
680             $text = '';
681             $previousLineIndented = false;
682             $previousLineBlank = false;
683
684             for ($i = 0, $blockLinesCount = count($blockLines); $i < $blockLinesCount; ++$i) {
685                 if ('' === $blockLines[$i]) {
686                     $text .= "\n";
687                     $previousLineIndented = false;
688                     $previousLineBlank = true;
689                 } elseif (' ' === $blockLines[$i][0]) {
690                     $text .= "\n".$blockLines[$i];
691                     $previousLineIndented = true;
692                     $previousLineBlank = false;
693                 } elseif ($previousLineIndented) {
694                     $text .= "\n".$blockLines[$i];
695                     $previousLineIndented = false;
696                     $previousLineBlank = false;
697                 } elseif ($previousLineBlank || 0 === $i) {
698                     $text .= $blockLines[$i];
699                     $previousLineIndented = false;
700                     $previousLineBlank = false;
701                 } else {
702                     $text .= ' '.$blockLines[$i];
703                     $previousLineIndented = false;
704                     $previousLineBlank = false;
705                 }
706             }
707         } else {
708             $text = implode("\n", $blockLines);
709         }
710
711         // deal with trailing newlines
712         if ('' === $chomping) {
713             $text = preg_replace('/\n+$/', "\n", $text);
714         } elseif ('-' === $chomping) {
715             $text = preg_replace('/\n+$/', '', $text);
716         }
717
718         return $text;
719     }
720
721     /**
722      * Returns true if the next line is indented.
723      *
724      * @return bool Returns true if the next line is indented, false otherwise
725      */
726     private function isNextLineIndented()
727     {
728         $currentIndentation = $this->getCurrentLineIndentation();
729         $EOF = !$this->moveToNextLine();
730
731         while (!$EOF && $this->isCurrentLineEmpty()) {
732             $EOF = !$this->moveToNextLine();
733         }
734
735         if ($EOF) {
736             return false;
737         }
738
739         $ret = $this->getCurrentLineIndentation() > $currentIndentation;
740
741         $this->moveToPreviousLine();
742
743         return $ret;
744     }
745
746     /**
747      * Returns true if the current line is blank or if it is a comment line.
748      *
749      * @return bool Returns true if the current line is empty or if it is a comment line, false otherwise
750      */
751     private function isCurrentLineEmpty()
752     {
753         return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
754     }
755
756     /**
757      * Returns true if the current line is blank.
758      *
759      * @return bool Returns true if the current line is blank, false otherwise
760      */
761     private function isCurrentLineBlank()
762     {
763         return '' == trim($this->currentLine, ' ');
764     }
765
766     /**
767      * Returns true if the current line is a comment line.
768      *
769      * @return bool Returns true if the current line is a comment line, false otherwise
770      */
771     private function isCurrentLineComment()
772     {
773         //checking explicitly the first char of the trim is faster than loops or strpos
774         $ltrimmedLine = ltrim($this->currentLine, ' ');
775
776         return '' !== $ltrimmedLine && $ltrimmedLine[0] === '#';
777     }
778
779     private function isCurrentLineLastLineInDocument()
780     {
781         return ($this->offset + $this->currentLineNb) >= ($this->totalNumberOfLines - 1);
782     }
783
784     /**
785      * Cleanups a YAML string to be parsed.
786      *
787      * @param string $value The input YAML string
788      *
789      * @return string A cleaned up YAML string
790      */
791     private function cleanup($value)
792     {
793         $value = str_replace(array("\r\n", "\r"), "\n", $value);
794
795         // strip YAML header
796         $count = 0;
797         $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count);
798         $this->offset += $count;
799
800         // remove leading comments
801         $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
802         if ($count == 1) {
803             // items have been removed, update the offset
804             $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
805             $value = $trimmedValue;
806         }
807
808         // remove start of the document marker (---)
809         $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
810         if ($count == 1) {
811             // items have been removed, update the offset
812             $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
813             $value = $trimmedValue;
814
815             // remove end of the document marker (...)
816             $value = preg_replace('#\.\.\.\s*$#', '', $value);
817         }
818
819         return $value;
820     }
821
822     /**
823      * Returns true if the next line starts unindented collection.
824      *
825      * @return bool Returns true if the next line starts unindented collection, false otherwise
826      */
827     private function isNextLineUnIndentedCollection()
828     {
829         $currentIndentation = $this->getCurrentLineIndentation();
830         $notEOF = $this->moveToNextLine();
831
832         while ($notEOF && $this->isCurrentLineEmpty()) {
833             $notEOF = $this->moveToNextLine();
834         }
835
836         if (false === $notEOF) {
837             return false;
838         }
839
840         $ret = $this->getCurrentLineIndentation() === $currentIndentation && $this->isStringUnIndentedCollectionItem();
841
842         $this->moveToPreviousLine();
843
844         return $ret;
845     }
846
847     /**
848      * Returns true if the string is un-indented collection item.
849      *
850      * @return bool Returns true if the string is un-indented collection item, false otherwise
851      */
852     private function isStringUnIndentedCollectionItem()
853     {
854         return '-' === rtrim($this->currentLine) || 0 === strpos($this->currentLine, '- ');
855     }
856
857     /**
858      * Tests whether or not the current line is the header of a block scalar.
859      *
860      * @return bool
861      */
862     private function isBlockScalarHeader()
863     {
864         return (bool) self::preg_match('~'.self::BLOCK_SCALAR_HEADER_PATTERN.'$~', $this->currentLine);
865     }
866
867     /**
868      * A local wrapper for `preg_match` which will throw a ParseException if there
869      * is an internal error in the PCRE engine.
870      *
871      * This avoids us needing to check for "false" every time PCRE is used
872      * in the YAML engine
873      *
874      * @throws ParseException on a PCRE internal error
875      *
876      * @see preg_last_error()
877      *
878      * @internal
879      */
880     public static function preg_match($pattern, $subject, &$matches = null, $flags = 0, $offset = 0)
881     {
882         if (false === $ret = preg_match($pattern, $subject, $matches, $flags, $offset)) {
883             switch (preg_last_error()) {
884                 case PREG_INTERNAL_ERROR:
885                     $error = 'Internal PCRE error.';
886                     break;
887                 case PREG_BACKTRACK_LIMIT_ERROR:
888                     $error = 'pcre.backtrack_limit reached.';
889                     break;
890                 case PREG_RECURSION_LIMIT_ERROR:
891                     $error = 'pcre.recursion_limit reached.';
892                     break;
893                 case PREG_BAD_UTF8_ERROR:
894                     $error = 'Malformed UTF-8 data.';
895                     break;
896                 case PREG_BAD_UTF8_OFFSET_ERROR:
897                     $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
898                     break;
899                 default:
900                     $error = 'Error.';
901             }
902
903             throw new ParseException($error);
904         }
905
906         return $ret;
907     }
908 }