Parser::isNextLineIndented()   B
last analyzed

Complexity

Conditions 8
Paths 8

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 15
nc 8
nop 0
dl 0
loc 28
rs 8.4444
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
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
use Symfony\Component\Yaml\Tag\TaggedValue;
16
17
/**
18
 * Parser parses YAML strings to convert them to PHP arrays.
19
 *
20
 * @author Fabien Potencier <[email protected]>
21
 *
22
 * @final
23
 */
24
class Parser
25
{
26
    public const TAG_PATTERN = '(?P<tag>![\w!.\/:-]+)';
27
    public const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
28
    public const REFERENCE_PATTERN = '#^&(?P<ref>[^ ]++) *+(?P<value>.*)#u';
29
30
    private $filename;
31
    private $offset = 0;
32
    private $numberOfParsedLines = 0;
33
    private $totalNumberOfLines;
34
    private $lines = [];
35
    private $currentLineNb = -1;
36
    private $currentLine = '';
37
    private $refs = [];
38
    private $skippedLineNumbers = [];
39
    private $locallySkippedLineNumbers = [];
40
    private $refsBeingParsed = [];
41
42
    /**
43
     * Parses a YAML file into a PHP value.
44
     *
45
     * @param string $filename The path to the YAML file to be parsed
46
     * @param int    $flags    A bit field of Yaml::PARSE_* constants to customize the YAML parser behavior
47
     *
48
     * @return mixed
49
     *
50
     * @throws ParseException If the file could not be read or the YAML is not valid
51
     */
52
    public function parseFile(string $filename, int $flags = 0)
53
    {
54
        if (!is_file($filename)) {
55
            throw new ParseException(sprintf('File "%s" does not exist.', $filename));
56
        }
57
58
        if (!is_readable($filename)) {
59
            throw new ParseException(sprintf('File "%s" cannot be read.', $filename));
60
        }
61
62
        $this->filename = $filename;
63
64
        try {
65
            return $this->parse(file_get_contents($filename), $flags);
66
        } finally {
67
            $this->filename = null;
68
        }
69
    }
70
71
    /**
72
     * Parses a YAML string to a PHP value.
73
     *
74
     * @param string $value A YAML string
75
     * @param int    $flags A bit field of Yaml::PARSE_* constants to customize the YAML parser behavior
76
     *
77
     * @return mixed
78
     *
79
     * @throws ParseException If the YAML is not valid
80
     */
81
    public function parse(string $value, int $flags = 0)
82
    {
83
        if (false === preg_match('//u', $value)) {
84
            throw new ParseException('The YAML value does not appear to be valid UTF-8.', -1, null, $this->filename);
85
        }
86
87
        $this->refs = [];
88
89
        $mbEncoding = null;
90
91
        if (2 /* MB_OVERLOAD_STRING */ & (int) \ini_get('mbstring.func_overload')) {
92
            $mbEncoding = mb_internal_encoding();
93
            mb_internal_encoding('UTF-8');
94
        }
95
96
        try {
97
            $data = $this->doParse($value, $flags);
98
        } finally {
99
            if (null !== $mbEncoding) {
100
                mb_internal_encoding($mbEncoding);
0 ignored issues
show
Bug introduced by
It seems like $mbEncoding can also be of type true; however, parameter $encoding of mb_internal_encoding() does only seem to accept null|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

100
                mb_internal_encoding(/** @scrutinizer ignore-type */ $mbEncoding);
Loading history...
101
            }
102
            $this->refsBeingParsed = [];
103
            $this->offset = 0;
104
            $this->lines = [];
105
            $this->currentLine = '';
106
            $this->numberOfParsedLines = 0;
107
            $this->refs = [];
108
            $this->skippedLineNumbers = [];
109
            $this->locallySkippedLineNumbers = [];
110
            $this->totalNumberOfLines = null;
111
        }
112
113
        return $data;
114
    }
115
116
    private function doParse(string $value, int $flags)
117
    {
118
        $this->currentLineNb = -1;
119
        $this->currentLine = '';
120
        $value = $this->cleanup($value);
121
        $this->lines = explode("\n", $value);
122
        $this->numberOfParsedLines = \count($this->lines);
123
        $this->locallySkippedLineNumbers = [];
124
125
        if (null === $this->totalNumberOfLines) {
126
            $this->totalNumberOfLines = $this->numberOfParsedLines;
127
        }
128
129
        if (!$this->moveToNextLine()) {
130
            return null;
131
        }
132
133
        $data = [];
134
        $context = null;
135
        $allowOverwrite = false;
136
137
        while ($this->isCurrentLineEmpty()) {
138
            if (!$this->moveToNextLine()) {
139
                return null;
140
            }
141
        }
142
143
        // Resolves the tag and returns if end of the document
144
        if (null !== ($tag = $this->getLineTag($this->currentLine, $flags, false)) && !$this->moveToNextLine()) {
145
            return new TaggedValue($tag, '');
146
        }
147
148
        do {
149
            if ($this->isCurrentLineEmpty()) {
150
                continue;
151
            }
152
153
            // tab?
154
            if ("\t" === $this->currentLine[0]) {
155
                throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
156
            }
157
158
            Inline::initialize($flags, $this->getRealCurrentLineNb(), $this->filename);
159
160
            $isRef = $mergeNode = false;
161
            if ('-' === $this->currentLine[0] && self::preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+))?$#u', rtrim($this->currentLine), $values)) {
162
                if ($context && 'mapping' == $context) {
163
                    throw new ParseException('You cannot define a sequence item when in a mapping.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
164
                }
165
                $context = 'sequence';
166
167
                if (isset($values['value']) && '&' === $values['value'][0] && self::preg_match(self::REFERENCE_PATTERN, $values['value'], $matches)) {
168
                    $isRef = $matches['ref'];
169
                    $this->refsBeingParsed[] = $isRef;
170
                    $values['value'] = $matches['value'];
171
                }
172
173
                if (isset($values['value'][1]) && '?' === $values['value'][0] && ' ' === $values['value'][1]) {
174
                    throw new ParseException('Complex mappings are not supported.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
175
                }
176
177
                // array
178
                if (isset($values['value']) && 0 === strpos(ltrim($values['value'], ' '), '-')) {
179
                    // Inline first child
180
                    $currentLineNumber = $this->getRealCurrentLineNb();
181
182
                    $sequenceIndentation = \strlen($values['leadspaces']) + 1;
183
                    $sequenceYaml = substr($this->currentLine, $sequenceIndentation);
184
                    $sequenceYaml .= "\n".$this->getNextEmbedBlock($sequenceIndentation, true);
185
186
                    $data[] = $this->parseBlock($currentLineNumber, rtrim($sequenceYaml), $flags);
187
                } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
188
                    $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true) ?? '', $flags);
189
                } elseif (null !== $subTag = $this->getLineTag(ltrim($values['value'], ' '), $flags)) {
190
                    $data[] = new TaggedValue(
191
                        $subTag,
192
                        $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $flags)
193
                    );
194
                } else {
195
                    if (
196
                        isset($values['leadspaces'])
197
                        && (
198
                            '!' === $values['value'][0]
199
                            || self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $this->trimTag($values['value']), $matches)
200
                        )
201
                    ) {
202
                        $block = $values['value'];
203
                        if ($this->isNextLineIndented() || isset($matches['value']) && '>-' === $matches['value']) {
204
                            $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + \strlen($values['leadspaces']) + 1);
205
                        }
206
207
                        $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $flags);
208
                    } else {
209
                        $data[] = $this->parseValue($values['value'], $flags, $context);
210
                    }
211
                }
212
                if ($isRef) {
213
                    $this->refs[$isRef] = end($data);
214
                    array_pop($this->refsBeingParsed);
215
                }
216
            } elseif (
217
                self::preg_match('#^(?P<key>(?:![^\s]++\s++)?(?:'.Inline::REGEX_QUOTED_STRING.'|(?:!?!php/const:)?[^ \'"\[\{!].*?)) *\:(( |\t)++(?P<value>.+))?$#u', rtrim($this->currentLine), $values)
218
                && (false === strpos($values['key'], ' #') || \in_array($values['key'][0], ['"', "'"]))
219
            ) {
220
                if ($context && 'sequence' == $context) {
221
                    throw new ParseException('You cannot define a mapping item when in a sequence.', $this->currentLineNb + 1, $this->currentLine, $this->filename);
222
                }
223
                $context = 'mapping';
224
225
                try {
226
                    $key = Inline::parseScalar($values['key']);
227
                } catch (ParseException $e) {
228
                    $e->setParsedLine($this->getRealCurrentLineNb() + 1);
229
                    $e->setSnippet($this->currentLine);
230
231
                    throw $e;
232
                }
233
234
                if (!\is_string($key) && !\is_int($key)) {
235
                    throw new ParseException((is_numeric($key) ? 'Numeric' : 'Non-string').' keys are not supported. Quote your evaluable mapping keys instead.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
236
                }
237
238
                // Convert float keys to strings, to avoid being converted to integers by PHP
239
                if (\is_float($key)) {
240
                    $key = (string) $key;
241
                }
242
243
                if ('<<' === $key && (!isset($values['value']) || '&' !== $values['value'][0] || !self::preg_match('#^&(?P<ref>[^ ]+)#u', $values['value'], $refMatches))) {
244
                    $mergeNode = true;
245
                    $allowOverwrite = true;
246
                    if (isset($values['value'][0]) && '*' === $values['value'][0]) {
247
                        $refName = substr(rtrim($values['value']), 1);
248
                        if (!\array_key_exists($refName, $this->refs)) {
249
                            if (false !== $pos = array_search($refName, $this->refsBeingParsed, true)) {
250
                                throw new ParseException(sprintf('Circular reference [%s] detected for reference "%s".', implode(', ', array_merge(\array_slice($this->refsBeingParsed, $pos), [$refName])), $refName), $this->currentLineNb + 1, $this->currentLine, $this->filename);
0 ignored issues
show
Bug introduced by
It seems like $pos can also be of type string; however, parameter $offset of array_slice() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

250
                                throw new ParseException(sprintf('Circular reference [%s] detected for reference "%s".', implode(', ', array_merge(\array_slice($this->refsBeingParsed, /** @scrutinizer ignore-type */ $pos), [$refName])), $refName), $this->currentLineNb + 1, $this->currentLine, $this->filename);
Loading history...
251
                            }
252
253
                            throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
254
                        }
255
256
                        $refValue = $this->refs[$refName];
257
258
                        if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $refValue instanceof \stdClass) {
259
                            $refValue = (array) $refValue;
260
                        }
261
262
                        if (!\is_array($refValue)) {
263
                            throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
264
                        }
265
266
                        $data += $refValue; // array union
267
                    } else {
268
                        if (isset($values['value']) && '' !== $values['value']) {
269
                            $value = $values['value'];
270
                        } else {
271
                            $value = $this->getNextEmbedBlock();
272
                        }
273
                        $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $flags);
274
275
                        if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $parsed instanceof \stdClass) {
276
                            $parsed = (array) $parsed;
277
                        }
278
279
                        if (!\is_array($parsed)) {
280
                            throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
281
                        }
282
283
                        if (isset($parsed[0])) {
284
                            // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
285
                            // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
286
                            // in the sequence override keys specified in later mapping nodes.
287
                            foreach ($parsed as $parsedItem) {
288
                                if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $parsedItem instanceof \stdClass) {
289
                                    $parsedItem = (array) $parsedItem;
290
                                }
291
292
                                if (!\is_array($parsedItem)) {
293
                                    throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem, $this->filename);
294
                                }
295
296
                                $data += $parsedItem; // array union
297
                            }
298
                        } else {
299
                            // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
300
                            // current mapping, unless the key already exists in it.
301
                            $data += $parsed; // array union
302
                        }
303
                    }
304
                } elseif ('<<' !== $key && isset($values['value']) && '&' === $values['value'][0] && self::preg_match(self::REFERENCE_PATTERN, $values['value'], $matches)) {
305
                    $isRef = $matches['ref'];
306
                    $this->refsBeingParsed[] = $isRef;
307
                    $values['value'] = $matches['value'];
308
                }
309
310
                $subTag = null;
311
                if ($mergeNode) {
312
                    // Merge keys
313
                } elseif (!isset($values['value']) || '' === $values['value'] || 0 === strpos($values['value'], '#') || (null !== $subTag = $this->getLineTag($values['value'], $flags)) || '<<' === $key) {
314
                    // hash
315
                    // if next line is less indented or equal, then it means that the current value is null
316
                    if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
317
                        // Spec: Keys MUST be unique; first one wins.
318
                        // But overwriting is allowed when a merge node is used in current block.
319
                        if ($allowOverwrite || !isset($data[$key])) {
320
                            if (null !== $subTag) {
321
                                $data[$key] = new TaggedValue($subTag, '');
0 ignored issues
show
Bug introduced by
$subTag of type void is incompatible with the type string expected by parameter $tag of Symfony\Component\Yaml\T...gedValue::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

321
                                $data[$key] = new TaggedValue(/** @scrutinizer ignore-type */ $subTag, '');
Loading history...
322
                            } else {
323
                                $data[$key] = null;
324
                            }
325
                        } else {
326
                            throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), $this->getRealCurrentLineNb() + 1, $this->currentLine);
327
                        }
328
                    } else {
329
                        // remember the parsed line number here in case we need it to provide some contexts in error messages below
330
                        $realCurrentLineNbKey = $this->getRealCurrentLineNb();
331
                        $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $flags);
332
                        if ('<<' === $key) {
333
                            $this->refs[$refMatches['ref']] = $value;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $refMatches does not seem to be defined for all execution paths leading up to this point.
Loading history...
334
335
                            if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $value instanceof \stdClass) {
336
                                $value = (array) $value;
337
                            }
338
339
                            $data += $value;
340
                        } elseif ($allowOverwrite || !isset($data[$key])) {
341
                            // Spec: Keys MUST be unique; first one wins.
342
                            // But overwriting is allowed when a merge node is used in current block.
343
                            if (null !== $subTag) {
344
                                $data[$key] = new TaggedValue($subTag, $value);
345
                            } else {
346
                                $data[$key] = $value;
347
                            }
348
                        } else {
349
                            throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), $realCurrentLineNbKey + 1, $this->currentLine);
350
                        }
351
                    }
352
                } else {
353
                    $value = $this->parseValue(rtrim($values['value']), $flags, $context);
354
                    // Spec: Keys MUST be unique; first one wins.
355
                    // But overwriting is allowed when a merge node is used in current block.
356
                    if ($allowOverwrite || !isset($data[$key])) {
357
                        $data[$key] = $value;
358
                    } else {
359
                        throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), $this->getRealCurrentLineNb() + 1, $this->currentLine);
360
                    }
361
                }
362
                if ($isRef) {
363
                    $this->refs[$isRef] = $data[$key];
364
                    array_pop($this->refsBeingParsed);
365
                }
366
            } elseif ('"' === $this->currentLine[0] || "'" === $this->currentLine[0]) {
367
                if (null !== $context) {
368
                    throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
369
                }
370
371
                try {
372
                    return Inline::parse($this->lexInlineQuotedString(), $flags, $this->refs);
373
                } catch (ParseException $e) {
374
                    $e->setParsedLine($this->getRealCurrentLineNb() + 1);
375
                    $e->setSnippet($this->currentLine);
376
377
                    throw $e;
378
                }
379
            } elseif ('{' === $this->currentLine[0]) {
380
                if (null !== $context) {
381
                    throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
382
                }
383
384
                try {
385
                    $parsedMapping = Inline::parse($this->lexInlineMapping(), $flags, $this->refs);
386
387
                    while ($this->moveToNextLine()) {
388
                        if (!$this->isCurrentLineEmpty()) {
389
                            throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
390
                        }
391
                    }
392
393
                    return $parsedMapping;
394
                } catch (ParseException $e) {
395
                    $e->setParsedLine($this->getRealCurrentLineNb() + 1);
396
                    $e->setSnippet($this->currentLine);
397
398
                    throw $e;
399
                }
400
            } elseif ('[' === $this->currentLine[0]) {
401
                if (null !== $context) {
402
                    throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
403
                }
404
405
                try {
406
                    $parsedSequence = Inline::parse($this->lexInlineSequence(), $flags, $this->refs);
407
408
                    while ($this->moveToNextLine()) {
409
                        if (!$this->isCurrentLineEmpty()) {
410
                            throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
411
                        }
412
                    }
413
414
                    return $parsedSequence;
415
                } catch (ParseException $e) {
416
                    $e->setParsedLine($this->getRealCurrentLineNb() + 1);
417
                    $e->setSnippet($this->currentLine);
418
419
                    throw $e;
420
                }
421
            } else {
422
                // multiple documents are not supported
423
                if ('---' === $this->currentLine) {
424
                    throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine, $this->filename);
425
                }
426
427
                if ($deprecatedUsage = (isset($this->currentLine[1]) && '?' === $this->currentLine[0] && ' ' === $this->currentLine[1])) {
428
                    throw new ParseException('Complex mappings are not supported.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
429
                }
430
431
                // 1-liner optionally followed by newline(s)
432
                if (\is_string($value) && $this->lines[0] === trim($value)) {
433
                    try {
434
                        $value = Inline::parse($this->lines[0], $flags, $this->refs);
435
                    } catch (ParseException $e) {
436
                        $e->setParsedLine($this->getRealCurrentLineNb() + 1);
437
                        $e->setSnippet($this->currentLine);
438
439
                        throw $e;
440
                    }
441
442
                    return $value;
443
                }
444
445
                // try to parse the value as a multi-line string as a last resort
446
                if (0 === $this->currentLineNb) {
447
                    $previousLineWasNewline = false;
448
                    $previousLineWasTerminatedWithBackslash = false;
449
                    $value = '';
450
451
                    foreach ($this->lines as $line) {
452
                        $trimmedLine = trim($line);
453
                        if ('#' === ($trimmedLine[0] ?? '')) {
454
                            continue;
455
                        }
456
                        // If the indentation is not consistent at offset 0, it is to be considered as a ParseError
457
                        if (0 === $this->offset && !$deprecatedUsage && isset($line[0]) && ' ' === $line[0]) {
458
                            throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
459
                        }
460
461
                        if (false !== strpos($line, ': ')) {
462
                            throw new ParseException('Mapping values are not allowed in multi-line blocks.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
463
                        }
464
465
                        if ('' === $trimmedLine) {
466
                            $value .= "\n";
467
                        } elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) {
468
                            $value .= ' ';
469
                        }
470
471
                        if ('' !== $trimmedLine && '\\' === substr($line, -1)) {
472
                            $value .= ltrim(substr($line, 0, -1));
473
                        } elseif ('' !== $trimmedLine) {
474
                            $value .= $trimmedLine;
475
                        }
476
477
                        if ('' === $trimmedLine) {
478
                            $previousLineWasNewline = true;
479
                            $previousLineWasTerminatedWithBackslash = false;
480
                        } elseif ('\\' === substr($line, -1)) {
481
                            $previousLineWasNewline = false;
482
                            $previousLineWasTerminatedWithBackslash = true;
483
                        } else {
484
                            $previousLineWasNewline = false;
485
                            $previousLineWasTerminatedWithBackslash = false;
486
                        }
487
                    }
488
489
                    try {
490
                        return Inline::parse(trim($value));
491
                    } catch (ParseException $e) {
492
                        // fall-through to the ParseException thrown below
493
                    }
494
                }
495
496
                throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
497
            }
498
        } while ($this->moveToNextLine());
499
500
        if (null !== $tag) {
501
            $data = new TaggedValue($tag, $data);
502
        }
503
504
        if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && 'mapping' === $context && !\is_object($data)) {
505
            $object = new \stdClass();
506
507
            foreach ($data as $key => $value) {
508
                $object->$key = $value;
509
            }
510
511
            $data = $object;
512
        }
513
514
        return empty($data) ? null : $data;
515
    }
516
517
    private function parseBlock(int $offset, string $yaml, int $flags)
518
    {
519
        $skippedLineNumbers = $this->skippedLineNumbers;
520
521
        foreach ($this->locallySkippedLineNumbers as $lineNumber) {
522
            if ($lineNumber < $offset) {
523
                continue;
524
            }
525
526
            $skippedLineNumbers[] = $lineNumber;
527
        }
528
529
        $parser = new self();
530
        $parser->offset = $offset;
531
        $parser->totalNumberOfLines = $this->totalNumberOfLines;
532
        $parser->skippedLineNumbers = $skippedLineNumbers;
533
        $parser->refs = &$this->refs;
534
        $parser->refsBeingParsed = $this->refsBeingParsed;
535
536
        return $parser->doParse($yaml, $flags);
537
    }
538
539
    /**
540
     * Returns the current line number (takes the offset into account).
541
     *
542
     * @internal
543
     */
544
    public function getRealCurrentLineNb(): int
545
    {
546
        $realCurrentLineNumber = $this->currentLineNb + $this->offset;
547
548
        foreach ($this->skippedLineNumbers as $skippedLineNumber) {
549
            if ($skippedLineNumber > $realCurrentLineNumber) {
550
                break;
551
            }
552
553
            ++$realCurrentLineNumber;
554
        }
555
556
        return $realCurrentLineNumber;
557
    }
558
559
    /**
560
     * Returns the current line indentation.
561
     */
562
    private function getCurrentLineIndentation(): int
563
    {
564
        if (' ' !== ($this->currentLine[0] ?? '')) {
565
            return 0;
566
        }
567
568
        return \strlen($this->currentLine) - \strlen(ltrim($this->currentLine, ' '));
569
    }
570
571
    /**
572
     * Returns the next embed block of YAML.
573
     *
574
     * @param int|null $indentation The indent level at which the block is to be read, or null for default
575
     * @param bool     $inSequence  True if the enclosing data structure is a sequence
576
     *
577
     * @throws ParseException When indentation problem are detected
578
     */
579
    private function getNextEmbedBlock(?int $indentation = null, bool $inSequence = false): string
580
    {
581
        $oldLineIndentation = $this->getCurrentLineIndentation();
582
583
        if (!$this->moveToNextLine()) {
584
            return '';
585
        }
586
587
        if (null === $indentation) {
588
            $newIndent = null;
589
            $movements = 0;
590
591
            do {
592
                $EOF = false;
593
594
                // empty and comment-like lines do not influence the indentation depth
595
                if ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) {
596
                    $EOF = !$this->moveToNextLine();
597
598
                    if (!$EOF) {
599
                        ++$movements;
600
                    }
601
                } else {
602
                    $newIndent = $this->getCurrentLineIndentation();
603
                }
604
            } while (!$EOF && null === $newIndent);
605
606
            for ($i = 0; $i < $movements; ++$i) {
607
                $this->moveToPreviousLine();
608
            }
609
610
            $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem();
611
612
            if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
613
                throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
614
            }
615
        } else {
616
            $newIndent = $indentation;
617
        }
618
619
        $data = [];
620
621
        if ($this->getCurrentLineIndentation() >= $newIndent) {
622
            $data[] = substr($this->currentLine, $newIndent ?? 0);
623
        } elseif ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) {
624
            $data[] = $this->currentLine;
625
        } else {
626
            $this->moveToPreviousLine();
627
628
            return '';
629
        }
630
631
        if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) {
632
            // the previous line contained a dash but no item content, this line is a sequence item with the same indentation
633
            // and therefore no nested list or mapping
634
            $this->moveToPreviousLine();
635
636
            return '';
637
        }
638
639
        $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
640
        $isItComment = $this->isCurrentLineComment();
641
642
        while ($this->moveToNextLine()) {
643
            if ($isItComment && !$isItUnindentedCollection) {
644
                $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
645
                $isItComment = $this->isCurrentLineComment();
646
            }
647
648
            $indent = $this->getCurrentLineIndentation();
649
650
            if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {
651
                $this->moveToPreviousLine();
652
                break;
653
            }
654
655
            if ($this->isCurrentLineBlank()) {
656
                $data[] = substr($this->currentLine, $newIndent ?? 0);
657
                continue;
658
            }
659
660
            if ($indent >= $newIndent) {
661
                $data[] = substr($this->currentLine, $newIndent ?? 0);
662
            } elseif ($this->isCurrentLineComment()) {
663
                $data[] = $this->currentLine;
664
            } elseif (0 == $indent) {
665
                $this->moveToPreviousLine();
666
667
                break;
668
            } else {
669
                throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
670
            }
671
        }
672
673
        return implode("\n", $data);
674
    }
675
676
    private function hasMoreLines(): bool
677
    {
678
        return (\count($this->lines) - 1) > $this->currentLineNb;
679
    }
680
681
    /**
682
     * Moves the parser to the next line.
683
     */
684
    private function moveToNextLine(): bool
685
    {
686
        if ($this->currentLineNb >= $this->numberOfParsedLines - 1) {
687
            return false;
688
        }
689
690
        $this->currentLine = $this->lines[++$this->currentLineNb];
691
692
        return true;
693
    }
694
695
    /**
696
     * Moves the parser to the previous line.
697
     */
698
    private function moveToPreviousLine(): bool
699
    {
700
        if ($this->currentLineNb < 1) {
701
            return false;
702
        }
703
704
        $this->currentLine = $this->lines[--$this->currentLineNb];
705
706
        return true;
707
    }
708
709
    /**
710
     * Parses a YAML value.
711
     *
712
     * @param string $value   A YAML value
713
     * @param int    $flags   A bit field of Yaml::PARSE_* constants to customize the YAML parser behavior
714
     * @param string $context The parser context (either sequence or mapping)
715
     *
716
     * @return mixed
717
     *
718
     * @throws ParseException When reference does not exist
719
     */
720
    private function parseValue(string $value, int $flags, string $context)
721
    {
722
        if (0 === strpos($value, '*')) {
723
            if (false !== $pos = strpos($value, '#')) {
724
                $value = substr($value, 1, $pos - 2);
725
            } else {
726
                $value = substr($value, 1);
727
            }
728
729
            if (!\array_key_exists($value, $this->refs)) {
730
                if (false !== $pos = array_search($value, $this->refsBeingParsed, true)) {
731
                    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);
0 ignored issues
show
Bug introduced by
It seems like $pos can also be of type string; however, parameter $offset of array_slice() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

731
                    throw new ParseException(sprintf('Circular reference [%s] detected for reference "%s".', implode(', ', array_merge(\array_slice($this->refsBeingParsed, /** @scrutinizer ignore-type */ $pos), [$value])), $value), $this->currentLineNb + 1, $this->currentLine, $this->filename);
Loading history...
732
                }
733
734
                throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine, $this->filename);
735
            }
736
737
            return $this->refs[$value];
738
        }
739
740
        if (\in_array($value[0], ['!', '|', '>'], true) && self::preg_match('/^(?:'.self::TAG_PATTERN.' +)?'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) {
741
            $modifiers = $matches['modifiers'] ?? '';
742
743
            $data = $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), abs((int) $modifiers));
0 ignored issues
show
Bug introduced by
It seems like abs((int)$modifiers) can also be of type double; however, parameter $indentation of Symfony\Component\Yaml\Parser::parseBlockScalar() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

743
            $data = $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), /** @scrutinizer ignore-type */ abs((int) $modifiers));
Loading history...
744
745
            if ('' !== $matches['tag'] && '!' !== $matches['tag']) {
746
                if ('!!binary' === $matches['tag']) {
747
                    return Inline::evaluateBinaryScalar($data);
748
                }
749
750
                return new TaggedValue(substr($matches['tag'], 1), $data);
751
            }
752
753
            return $data;
754
        }
755
756
        try {
757
            if ('' !== $value && '{' === $value[0]) {
758
                $cursor = \strlen(rtrim($this->currentLine)) - \strlen(rtrim($value));
759
760
                return Inline::parse($this->lexInlineMapping($cursor), $flags, $this->refs);
761
            } elseif ('' !== $value && '[' === $value[0]) {
762
                $cursor = \strlen(rtrim($this->currentLine)) - \strlen(rtrim($value));
763
764
                return Inline::parse($this->lexInlineSequence($cursor), $flags, $this->refs);
765
            }
766
767
            switch ($value[0] ?? '') {
768
                case '"':
769
                case "'":
770
                    $cursor = \strlen(rtrim($this->currentLine)) - \strlen(rtrim($value));
771
                    $parsedValue = Inline::parse($this->lexInlineQuotedString($cursor), $flags, $this->refs);
772
773
                    if (isset($this->currentLine[$cursor]) && preg_replace('/\s*(#.*)?$/A', '', substr($this->currentLine, $cursor))) {
774
                        throw new ParseException(sprintf('Unexpected characters near "%s".', substr($this->currentLine, $cursor)));
775
                    }
776
777
                    return $parsedValue;
778
                default:
779
                    $lines = [];
780
781
                    while ($this->moveToNextLine()) {
782
                        // unquoted strings end before the first unindented line
783
                        if (0 === $this->getCurrentLineIndentation()) {
784
                            $this->moveToPreviousLine();
785
786
                            break;
787
                        }
788
789
                        $lines[] = trim($this->currentLine);
790
                    }
791
792
                    for ($i = 0, $linesCount = \count($lines), $previousLineBlank = false; $i < $linesCount; ++$i) {
793
                        if ('' === $lines[$i]) {
794
                            $value .= "\n";
795
                            $previousLineBlank = true;
796
                        } elseif ($previousLineBlank) {
797
                            $value .= $lines[$i];
798
                            $previousLineBlank = false;
799
                        } else {
800
                            $value .= ' '.$lines[$i];
801
                            $previousLineBlank = false;
802
                        }
803
                    }
804
805
                    Inline::$parsedLineNumber = $this->getRealCurrentLineNb();
806
807
                    $parsedValue = Inline::parse($value, $flags, $this->refs);
808
809
                    if ('mapping' === $context && \is_string($parsedValue) && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) {
810
                        throw new ParseException('A colon cannot be used in an unquoted mapping value.', $this->getRealCurrentLineNb() + 1, $value, $this->filename);
811
                    }
812
813
                    return $parsedValue;
814
            }
815
        } catch (ParseException $e) {
816
            $e->setParsedLine($this->getRealCurrentLineNb() + 1);
817
            $e->setSnippet($this->currentLine);
818
819
            throw $e;
820
        }
821
    }
822
823
    /**
824
     * Parses a block scalar.
825
     *
826
     * @param string $style       The style indicator that was used to begin this block scalar (| or >)
827
     * @param string $chomping    The chomping indicator that was used to begin this block scalar (+ or -)
828
     * @param int    $indentation The indentation indicator that was used to begin this block scalar
829
     */
830
    private function parseBlockScalar(string $style, string $chomping = '', int $indentation = 0): string
831
    {
832
        $notEOF = $this->moveToNextLine();
833
        if (!$notEOF) {
834
            return '';
835
        }
836
837
        $isCurrentLineBlank = $this->isCurrentLineBlank();
838
        $blockLines = [];
839
840
        // leading blank lines are consumed before determining indentation
841
        while ($notEOF && $isCurrentLineBlank) {
842
            // newline only if not EOF
843
            if ($notEOF = $this->moveToNextLine()) {
844
                $blockLines[] = '';
845
                $isCurrentLineBlank = $this->isCurrentLineBlank();
846
            }
847
        }
848
849
        // determine indentation if not specified
850
        if (0 === $indentation) {
851
            $currentLineLength = \strlen($this->currentLine);
852
853
            for ($i = 0; $i < $currentLineLength && ' ' === $this->currentLine[$i]; ++$i) {
854
                ++$indentation;
855
            }
856
        }
857
858
        if ($indentation > 0) {
859
            $pattern = sprintf('/^ {%d}(.*)$/', $indentation);
860
861
            while (
862
                $notEOF && (
863
                    $isCurrentLineBlank ||
864
                    self::preg_match($pattern, $this->currentLine, $matches)
865
                )
866
            ) {
867
                if ($isCurrentLineBlank && \strlen($this->currentLine) > $indentation) {
868
                    $blockLines[] = substr($this->currentLine, $indentation);
869
                } elseif ($isCurrentLineBlank) {
870
                    $blockLines[] = '';
871
                } else {
872
                    $blockLines[] = $matches[1];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $matches does not seem to be defined for all execution paths leading up to this point.
Loading history...
873
                }
874
875
                // newline only if not EOF
876
                if ($notEOF = $this->moveToNextLine()) {
877
                    $isCurrentLineBlank = $this->isCurrentLineBlank();
878
                }
879
            }
880
        } elseif ($notEOF) {
881
            $blockLines[] = '';
882
        }
883
884
        if ($notEOF) {
885
            $blockLines[] = '';
886
            $this->moveToPreviousLine();
887
        } elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) {
888
            $blockLines[] = '';
889
        }
890
891
        // folded style
892
        if ('>' === $style) {
893
            $text = '';
894
            $previousLineIndented = false;
895
            $previousLineBlank = false;
896
897
            for ($i = 0, $blockLinesCount = \count($blockLines); $i < $blockLinesCount; ++$i) {
898
                if ('' === $blockLines[$i]) {
899
                    $text .= "\n";
900
                    $previousLineIndented = false;
901
                    $previousLineBlank = true;
902
                } elseif (' ' === $blockLines[$i][0]) {
903
                    $text .= "\n".$blockLines[$i];
904
                    $previousLineIndented = true;
905
                    $previousLineBlank = false;
906
                } elseif ($previousLineIndented) {
907
                    $text .= "\n".$blockLines[$i];
908
                    $previousLineIndented = false;
909
                    $previousLineBlank = false;
910
                } elseif ($previousLineBlank || 0 === $i) {
911
                    $text .= $blockLines[$i];
912
                    $previousLineIndented = false;
913
                    $previousLineBlank = false;
914
                } else {
915
                    $text .= ' '.$blockLines[$i];
916
                    $previousLineIndented = false;
917
                    $previousLineBlank = false;
918
                }
919
            }
920
        } else {
921
            $text = implode("\n", $blockLines);
922
        }
923
924
        // deal with trailing newlines
925
        if ('' === $chomping) {
926
            $text = preg_replace('/\n+$/', "\n", $text);
927
        } elseif ('-' === $chomping) {
928
            $text = preg_replace('/\n+$/', '', $text);
929
        }
930
931
        return $text;
932
    }
933
934
    /**
935
     * Returns true if the next line is indented.
936
     */
937
    private function isNextLineIndented(): bool
938
    {
939
        $currentIndentation = $this->getCurrentLineIndentation();
940
        $movements = 0;
941
942
        do {
943
            $EOF = !$this->moveToNextLine();
944
945
            if (!$EOF) {
946
                ++$movements;
947
            }
948
        } while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()));
949
950
        if ($EOF) {
951
            for ($i = 0; $i < $movements; ++$i) {
952
                $this->moveToPreviousLine();
953
            }
954
955
            return false;
956
        }
957
958
        $ret = $this->getCurrentLineIndentation() > $currentIndentation;
959
960
        for ($i = 0; $i < $movements; ++$i) {
961
            $this->moveToPreviousLine();
962
        }
963
964
        return $ret;
965
    }
966
967
    /**
968
     * Returns true if the current line is blank or if it is a comment line.
969
     */
970
    private function isCurrentLineEmpty(): bool
971
    {
972
        return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
973
    }
974
975
    /**
976
     * Returns true if the current line is blank.
977
     */
978
    private function isCurrentLineBlank(): bool
979
    {
980
        return '' === $this->currentLine || '' === trim($this->currentLine, ' ');
981
    }
982
983
    /**
984
     * Returns true if the current line is a comment line.
985
     */
986
    private function isCurrentLineComment(): bool
987
    {
988
        // checking explicitly the first char of the trim is faster than loops or strpos
989
        $ltrimmedLine = '' !== $this->currentLine && ' ' === $this->currentLine[0] ? ltrim($this->currentLine, ' ') : $this->currentLine;
990
991
        return '' !== $ltrimmedLine && '#' === $ltrimmedLine[0];
992
    }
993
994
    private function isCurrentLineLastLineInDocument(): bool
995
    {
996
        return ($this->offset + $this->currentLineNb) >= ($this->totalNumberOfLines - 1);
997
    }
998
999
    /**
1000
     * Cleanups a YAML string to be parsed.
1001
     *
1002
     * @param string $value The input YAML string
1003
     */
1004
    private function cleanup(string $value): string
1005
    {
1006
        $value = str_replace(["\r\n", "\r"], "\n", $value);
1007
1008
        // strip YAML header
1009
        $count = 0;
1010
        $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count);
1011
        $this->offset += $count;
1012
1013
        // remove leading comments
1014
        $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
1015
        if (1 === $count) {
1016
            // items have been removed, update the offset
1017
            $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
1018
            $value = $trimmedValue;
1019
        }
1020
1021
        // remove start of the document marker (---)
1022
        $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
1023
        if (1 === $count) {
1024
            // items have been removed, update the offset
1025
            $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
1026
            $value = $trimmedValue;
1027
1028
            // remove end of the document marker (...)
1029
            $value = preg_replace('#\.\.\.\s*$#', '', $value);
1030
        }
1031
1032
        return $value;
1033
    }
1034
1035
    /**
1036
     * Returns true if the next line starts unindented collection.
1037
     */
1038
    private function isNextLineUnIndentedCollection(): bool
1039
    {
1040
        $currentIndentation = $this->getCurrentLineIndentation();
1041
        $movements = 0;
1042
1043
        do {
1044
            $EOF = !$this->moveToNextLine();
1045
1046
            if (!$EOF) {
1047
                ++$movements;
1048
            }
1049
        } while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()));
1050
1051
        if ($EOF) {
1052
            return false;
1053
        }
1054
1055
        $ret = $this->getCurrentLineIndentation() === $currentIndentation && $this->isStringUnIndentedCollectionItem();
1056
1057
        for ($i = 0; $i < $movements; ++$i) {
1058
            $this->moveToPreviousLine();
1059
        }
1060
1061
        return $ret;
1062
    }
1063
1064
    /**
1065
     * Returns true if the string is un-indented collection item.
1066
     */
1067
    private function isStringUnIndentedCollectionItem(): bool
1068
    {
1069
        return '-' === rtrim($this->currentLine) || 0 === strpos($this->currentLine, '- ');
1070
    }
1071
1072
    /**
1073
     * A local wrapper for "preg_match" which will throw a ParseException if there
1074
     * is an internal error in the PCRE engine.
1075
     *
1076
     * This avoids us needing to check for "false" every time PCRE is used
1077
     * in the YAML engine
1078
     *
1079
     * @throws ParseException on a PCRE internal error
1080
     *
1081
     * @see preg_last_error()
1082
     *
1083
     * @internal
1084
     */
1085
    public static function preg_match(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): int
1086
    {
1087
        if (false === $ret = preg_match($pattern, $subject, $matches, $flags, $offset)) {
1088
            switch (preg_last_error()) {
1089
                case \PREG_INTERNAL_ERROR:
1090
                    $error = 'Internal PCRE error.';
1091
                    break;
1092
                case \PREG_BACKTRACK_LIMIT_ERROR:
1093
                    $error = 'pcre.backtrack_limit reached.';
1094
                    break;
1095
                case \PREG_RECURSION_LIMIT_ERROR:
1096
                    $error = 'pcre.recursion_limit reached.';
1097
                    break;
1098
                case \PREG_BAD_UTF8_ERROR:
1099
                    $error = 'Malformed UTF-8 data.';
1100
                    break;
1101
                case \PREG_BAD_UTF8_OFFSET_ERROR:
1102
                    $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
1103
                    break;
1104
                default:
1105
                    $error = 'Error.';
1106
            }
1107
1108
            throw new ParseException($error);
1109
        }
1110
1111
        return $ret;
1112
    }
1113
1114
    /**
1115
     * Trim the tag on top of the value.
1116
     *
1117
     * Prevent values such as "!foo {quz: bar}" to be considered as
1118
     * a mapping block.
1119
     */
1120
    private function trimTag(string $value): string
1121
    {
1122
        if ('!' === $value[0]) {
1123
            return ltrim(substr($value, 1, strcspn($value, " \r\n", 1)), ' ');
1124
        }
1125
1126
        return $value;
1127
    }
1128
1129
    private function getLineTag(string $value, int $flags, bool $nextLineCheck = true): ?string
1130
    {
1131
        if ('' === $value || '!' !== $value[0] || 1 !== self::preg_match('/^'.self::TAG_PATTERN.' *( +#.*)?$/', $value, $matches)) {
1132
            return null;
1133
        }
1134
1135
        if ($nextLineCheck && !$this->isNextLineIndented()) {
1136
            return null;
1137
        }
1138
1139
        $tag = substr($matches['tag'], 1);
1140
1141
        // Built-in tags
1142
        if ($tag && '!' === $tag[0]) {
1143
            throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag), $this->getRealCurrentLineNb() + 1, $value, $this->filename);
1144
        }
1145
1146
        if (Yaml::PARSE_CUSTOM_TAGS & $flags) {
1147
            return $tag;
1148
        }
1149
1150
        throw new ParseException(sprintf('Tags support is not enabled. You must use the flag "Yaml::PARSE_CUSTOM_TAGS" to use "%s".', $matches['tag']), $this->getRealCurrentLineNb() + 1, $value, $this->filename);
1151
    }
1152
1153
    private function lexInlineQuotedString(int &$cursor = 0): string
1154
    {
1155
        $quotation = $this->currentLine[$cursor];
1156
        $value = $quotation;
1157
        ++$cursor;
1158
1159
        $previousLineWasNewline = true;
1160
        $previousLineWasTerminatedWithBackslash = false;
1161
        $lineNumber = 0;
1162
1163
        do {
1164
            if (++$lineNumber > 1) {
1165
                $cursor += strspn($this->currentLine, ' ', $cursor);
1166
            }
1167
1168
            if ($this->isCurrentLineBlank()) {
1169
                $value .= "\n";
1170
            } elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) {
1171
                $value .= ' ';
1172
            }
1173
1174
            for (; \strlen($this->currentLine) > $cursor; ++$cursor) {
1175
                switch ($this->currentLine[$cursor]) {
1176
                    case '\\':
1177
                        if ("'" === $quotation) {
1178
                            $value .= '\\';
1179
                        } elseif (isset($this->currentLine[++$cursor])) {
1180
                            $value .= '\\'.$this->currentLine[$cursor];
1181
                        }
1182
1183
                        break;
1184
                    case $quotation:
1185
                        ++$cursor;
1186
1187
                        if ("'" === $quotation && isset($this->currentLine[$cursor]) && "'" === $this->currentLine[$cursor]) {
1188
                            $value .= "''";
1189
                            break;
1190
                        }
1191
1192
                        return $value.$quotation;
1193
                    default:
1194
                        $value .= $this->currentLine[$cursor];
1195
                }
1196
            }
1197
1198
            if ($this->isCurrentLineBlank()) {
1199
                $previousLineWasNewline = true;
1200
                $previousLineWasTerminatedWithBackslash = false;
1201
            } elseif ('\\' === $this->currentLine[-1]) {
1202
                $previousLineWasNewline = false;
1203
                $previousLineWasTerminatedWithBackslash = true;
1204
            } else {
1205
                $previousLineWasNewline = false;
1206
                $previousLineWasTerminatedWithBackslash = false;
1207
            }
1208
1209
            if ($this->hasMoreLines()) {
1210
                $cursor = 0;
1211
            }
1212
        } while ($this->moveToNextLine());
1213
1214
        throw new ParseException('Malformed inline YAML string.');
1215
    }
1216
1217
    private function lexUnquotedString(int &$cursor): string
1218
    {
1219
        $offset = $cursor;
1220
        $cursor += strcspn($this->currentLine, '[]{},: ', $cursor);
1221
1222
        if ($cursor === $offset) {
1223
            throw new ParseException('Malformed unquoted YAML string.');
1224
        }
1225
1226
        return substr($this->currentLine, $offset, $cursor - $offset);
1227
    }
1228
1229
    private function lexInlineMapping(int &$cursor = 0): string
1230
    {
1231
        return $this->lexInlineStructure($cursor, '}');
1232
    }
1233
1234
    private function lexInlineSequence(int &$cursor = 0): string
1235
    {
1236
        return $this->lexInlineStructure($cursor, ']');
1237
    }
1238
1239
    private function lexInlineStructure(int &$cursor, string $closingTag): string
1240
    {
1241
        $value = $this->currentLine[$cursor];
1242
        ++$cursor;
1243
1244
        do {
1245
            $this->consumeWhitespaces($cursor);
1246
1247
            while (isset($this->currentLine[$cursor])) {
1248
                switch ($this->currentLine[$cursor]) {
1249
                    case '"':
1250
                    case "'":
1251
                        $value .= $this->lexInlineQuotedString($cursor);
1252
                        break;
1253
                    case ':':
1254
                    case ',':
1255
                        $value .= $this->currentLine[$cursor];
1256
                        ++$cursor;
1257
                        break;
1258
                    case '{':
1259
                        $value .= $this->lexInlineMapping($cursor);
1260
                        break;
1261
                    case '[':
1262
                        $value .= $this->lexInlineSequence($cursor);
1263
                        break;
1264
                    case $closingTag:
1265
                        $value .= $this->currentLine[$cursor];
1266
                        ++$cursor;
1267
1268
                        return $value;
1269
                    case '#':
1270
                        break 2;
1271
                    default:
1272
                        $value .= $this->lexUnquotedString($cursor);
1273
                }
1274
1275
                if ($this->consumeWhitespaces($cursor)) {
1276
                    $value .= ' ';
1277
                }
1278
            }
1279
1280
            if ($this->hasMoreLines()) {
1281
                $cursor = 0;
1282
            }
1283
        } while ($this->moveToNextLine());
1284
1285
        throw new ParseException('Malformed inline YAML string.');
1286
    }
1287
1288
    private function consumeWhitespaces(int &$cursor): bool
1289
    {
1290
        $whitespacesConsumed = 0;
1291
1292
        do {
1293
            $whitespaceOnlyTokenLength = strspn($this->currentLine, ' ', $cursor);
1294
            $whitespacesConsumed += $whitespaceOnlyTokenLength;
1295
            $cursor += $whitespaceOnlyTokenLength;
1296
1297
            if (isset($this->currentLine[$cursor])) {
1298
                return 0 < $whitespacesConsumed;
1299
            }
1300
1301
            if ($this->hasMoreLines()) {
1302
                $cursor = 0;
1303
            }
1304
        } while ($this->moveToNextLine());
1305
1306
        return 0 < $whitespacesConsumed;
1307
    }
1308
}
1309