1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Yaml;
13
14use Symfony\Component\Yaml\Exception\DumpException;
15use Symfony\Component\Yaml\Exception\ParseException;
16
17/**
18 * Inline implements a YAML parser/dumper for the YAML inline syntax.
19 *
20 * @author Fabien Potencier <fabien@symfony.com>
21 */
22class Inline
23{
24    const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+)"|\'([^\']*+(?:\'\'[^\']*+)*+)\')';
25
26    private static $exceptionOnInvalidType = false;
27    private static $objectSupport = false;
28    private static $objectForMap = false;
29
30    /**
31     * Converts a YAML string to a PHP value.
32     *
33     * @param string $value                  A YAML string
34     * @param bool   $exceptionOnInvalidType True if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
35     * @param bool   $objectSupport          True if object support is enabled, false otherwise
36     * @param bool   $objectForMap           True if maps should return a stdClass instead of array()
37     * @param array  $references             Mapping of variable names to values
38     *
39     * @return mixed A PHP value
40     *
41     * @throws ParseException
42     */
43    public static function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false, $references = array())
44    {
45        self::$exceptionOnInvalidType = $exceptionOnInvalidType;
46        self::$objectSupport = $objectSupport;
47        self::$objectForMap = $objectForMap;
48
49        $value = trim($value);
50
51        if ('' === $value) {
52            return '';
53        }
54
55        if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) {
56            $mbEncoding = mb_internal_encoding();
57            mb_internal_encoding('ASCII');
58        }
59
60        $i = 0;
61        switch ($value[0]) {
62            case '[':
63                $result = self::parseSequence($value, $i, $references);
64                ++$i;
65                break;
66            case '{':
67                $result = self::parseMapping($value, $i, $references);
68                ++$i;
69                break;
70            default:
71                $result = self::parseScalar($value, null, array('"', "'"), $i, true, $references);
72        }
73
74        // some comments are allowed at the end
75        if (preg_replace('/\s+#.*$/A', '', substr($value, $i))) {
76            throw new ParseException(sprintf('Unexpected characters near "%s".', substr($value, $i)));
77        }
78
79        if (isset($mbEncoding)) {
80            mb_internal_encoding($mbEncoding);
81        }
82
83        return $result;
84    }
85
86    /**
87     * Dumps a given PHP variable to a YAML string.
88     *
89     * @param mixed $value                  The PHP variable to convert
90     * @param bool  $exceptionOnInvalidType True if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
91     * @param bool  $objectSupport          True if object support is enabled, false otherwise
92     *
93     * @return string The YAML string representing the PHP value
94     *
95     * @throws DumpException When trying to dump PHP resource
96     */
97    public static function dump($value, $exceptionOnInvalidType = false, $objectSupport = false)
98    {
99        switch (true) {
100            case \is_resource($value):
101                if ($exceptionOnInvalidType) {
102                    throw new DumpException(sprintf('Unable to dump PHP resources in a YAML file ("%s").', get_resource_type($value)));
103                }
104
105                return 'null';
106            case \is_object($value):
107                if ($objectSupport) {
108                    return '!php/object:'.serialize($value);
109                }
110
111                if ($exceptionOnInvalidType) {
112                    throw new DumpException('Object support when dumping a YAML file has been disabled.');
113                }
114
115                return 'null';
116            case \is_array($value):
117                return self::dumpArray($value, $exceptionOnInvalidType, $objectSupport);
118            case null === $value:
119                return 'null';
120            case true === $value:
121                return 'true';
122            case false === $value:
123                return 'false';
124            case ctype_digit($value):
125                return \is_string($value) ? "'$value'" : (int) $value;
126            case is_numeric($value):
127                $locale = setlocale(LC_NUMERIC, 0);
128                if (false !== $locale) {
129                    setlocale(LC_NUMERIC, 'C');
130                }
131                if (\is_float($value)) {
132                    $repr = (string) $value;
133                    if (is_infinite($value)) {
134                        $repr = str_ireplace('INF', '.Inf', $repr);
135                    } elseif (floor($value) == $value && $repr == $value) {
136                        // Preserve float data type since storing a whole number will result in integer value.
137                        $repr = '!!float '.$repr;
138                    }
139                } else {
140                    $repr = \is_string($value) ? "'$value'" : (string) $value;
141                }
142                if (false !== $locale) {
143                    setlocale(LC_NUMERIC, $locale);
144                }
145
146                return $repr;
147            case '' == $value:
148                return "''";
149            case Escaper::requiresDoubleQuoting($value):
150                return Escaper::escapeWithDoubleQuotes($value);
151            case Escaper::requiresSingleQuoting($value):
152            case Parser::preg_match(self::getHexRegex(), $value):
153            case Parser::preg_match(self::getTimestampRegex(), $value):
154                return Escaper::escapeWithSingleQuotes($value);
155            default:
156                return $value;
157        }
158    }
159
160    /**
161     * Check if given array is hash or just normal indexed array.
162     *
163     * @internal
164     *
165     * @param array $value The PHP array to check
166     *
167     * @return bool true if value is hash array, false otherwise
168     */
169    public static function isHash(array $value)
170    {
171        $expectedKey = 0;
172
173        foreach ($value as $key => $val) {
174            if ($key !== $expectedKey++) {
175                return true;
176            }
177        }
178
179        return false;
180    }
181
182    /**
183     * Dumps a PHP array to a YAML string.
184     *
185     * @param array $value                  The PHP array to dump
186     * @param bool  $exceptionOnInvalidType True if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
187     * @param bool  $objectSupport          True if object support is enabled, false otherwise
188     *
189     * @return string The YAML string representing the PHP array
190     */
191    private static function dumpArray($value, $exceptionOnInvalidType, $objectSupport)
192    {
193        // array
194        if ($value && !self::isHash($value)) {
195            $output = array();
196            foreach ($value as $val) {
197                $output[] = self::dump($val, $exceptionOnInvalidType, $objectSupport);
198            }
199
200            return sprintf('[%s]', implode(', ', $output));
201        }
202
203        // hash
204        $output = array();
205        foreach ($value as $key => $val) {
206            $output[] = sprintf('%s: %s', self::dump($key, $exceptionOnInvalidType, $objectSupport), self::dump($val, $exceptionOnInvalidType, $objectSupport));
207        }
208
209        return sprintf('{ %s }', implode(', ', $output));
210    }
211
212    /**
213     * Parses a YAML scalar.
214     *
215     * @param string   $scalar
216     * @param string[] $delimiters
217     * @param string[] $stringDelimiters
218     * @param int      &$i
219     * @param bool     $evaluate
220     * @param array    $references
221     *
222     * @return string
223     *
224     * @throws ParseException When malformed inline YAML string is parsed
225     *
226     * @internal
227     */
228    public static function parseScalar($scalar, $delimiters = null, $stringDelimiters = array('"', "'"), &$i = 0, $evaluate = true, $references = array())
229    {
230        if (\in_array($scalar[$i], $stringDelimiters)) {
231            // quoted scalar
232            $output = self::parseQuotedScalar($scalar, $i);
233
234            if (null !== $delimiters) {
235                $tmp = ltrim(substr($scalar, $i), ' ');
236                if ('' === $tmp) {
237                    throw new ParseException(sprintf('Unexpected end of line, expected one of "%s".', implode('', $delimiters)));
238                }
239                if (!\in_array($tmp[0], $delimiters)) {
240                    throw new ParseException(sprintf('Unexpected characters (%s).', substr($scalar, $i)));
241                }
242            }
243        } else {
244            // "normal" string
245            if (!$delimiters) {
246                $output = substr($scalar, $i);
247                $i += \strlen($output);
248
249                // remove comments
250                if (Parser::preg_match('/[ \t]+#/', $output, $match, PREG_OFFSET_CAPTURE)) {
251                    $output = substr($output, 0, $match[0][1]);
252                }
253            } elseif (Parser::preg_match('/^(.+?)('.implode('|', $delimiters).')/', substr($scalar, $i), $match)) {
254                $output = $match[1];
255                $i += \strlen($output);
256            } else {
257                throw new ParseException(sprintf('Malformed inline YAML string: %s.', $scalar));
258            }
259
260            // a non-quoted string cannot start with @ or ` (reserved) nor with a scalar indicator (| or >)
261            if ($output && ('@' === $output[0] || '`' === $output[0] || '|' === $output[0] || '>' === $output[0])) {
262                @trigger_error(sprintf('Not quoting the scalar "%s" starting with "%s" is deprecated since Symfony 2.8 and will throw a ParseException in 3.0.', $output, $output[0]), E_USER_DEPRECATED);
263
264                // to be thrown in 3.0
265                // throw new ParseException(sprintf('The reserved indicator "%s" cannot start a plain scalar; you need to quote the scalar.', $output[0]));
266            }
267
268            if ($evaluate) {
269                $output = self::evaluateScalar($output, $references);
270            }
271        }
272
273        return $output;
274    }
275
276    /**
277     * Parses a YAML quoted scalar.
278     *
279     * @param string $scalar
280     * @param int    &$i
281     *
282     * @return string
283     *
284     * @throws ParseException When malformed inline YAML string is parsed
285     */
286    private static function parseQuotedScalar($scalar, &$i)
287    {
288        if (!Parser::preg_match('/'.self::REGEX_QUOTED_STRING.'/Au', substr($scalar, $i), $match)) {
289            throw new ParseException(sprintf('Malformed inline YAML string: %s.', substr($scalar, $i)));
290        }
291
292        $output = substr($match[0], 1, \strlen($match[0]) - 2);
293
294        $unescaper = new Unescaper();
295        if ('"' == $scalar[$i]) {
296            $output = $unescaper->unescapeDoubleQuotedString($output);
297        } else {
298            $output = $unescaper->unescapeSingleQuotedString($output);
299        }
300
301        $i += \strlen($match[0]);
302
303        return $output;
304    }
305
306    /**
307     * Parses a YAML sequence.
308     *
309     * @param string $sequence
310     * @param int    &$i
311     * @param array  $references
312     *
313     * @return array
314     *
315     * @throws ParseException When malformed inline YAML string is parsed
316     */
317    private static function parseSequence($sequence, &$i = 0, $references = array())
318    {
319        $output = array();
320        $len = \strlen($sequence);
321        ++$i;
322
323        // [foo, bar, ...]
324        while ($i < $len) {
325            switch ($sequence[$i]) {
326                case '[':
327                    // nested sequence
328                    $output[] = self::parseSequence($sequence, $i, $references);
329                    break;
330                case '{':
331                    // nested mapping
332                    $output[] = self::parseMapping($sequence, $i, $references);
333                    break;
334                case ']':
335                    return $output;
336                case ',':
337                case ' ':
338                    break;
339                default:
340                    $isQuoted = \in_array($sequence[$i], array('"', "'"));
341                    $value = self::parseScalar($sequence, array(',', ']'), array('"', "'"), $i, true, $references);
342
343                    // the value can be an array if a reference has been resolved to an array var
344                    if (!\is_array($value) && !$isQuoted && false !== strpos($value, ': ')) {
345                        // embedded mapping?
346                        try {
347                            $pos = 0;
348                            $value = self::parseMapping('{'.$value.'}', $pos, $references);
349                        } catch (\InvalidArgumentException $e) {
350                            // no, it's not
351                        }
352                    }
353
354                    $output[] = $value;
355
356                    --$i;
357            }
358
359            ++$i;
360        }
361
362        throw new ParseException(sprintf('Malformed inline YAML string: %s.', $sequence));
363    }
364
365    /**
366     * Parses a YAML mapping.
367     *
368     * @param string $mapping
369     * @param int    &$i
370     * @param array  $references
371     *
372     * @return array|\stdClass
373     *
374     * @throws ParseException When malformed inline YAML string is parsed
375     */
376    private static function parseMapping($mapping, &$i = 0, $references = array())
377    {
378        $output = array();
379        $len = \strlen($mapping);
380        ++$i;
381        $allowOverwrite = false;
382
383        // {foo: bar, bar:foo, ...}
384        while ($i < $len) {
385            switch ($mapping[$i]) {
386                case ' ':
387                case ',':
388                    ++$i;
389                    continue 2;
390                case '}':
391                    if (self::$objectForMap) {
392                        return (object) $output;
393                    }
394
395                    return $output;
396            }
397
398            // key
399            $key = self::parseScalar($mapping, array(':', ' '), array('"', "'"), $i, false);
400
401            if ('<<' === $key) {
402                $allowOverwrite = true;
403            }
404
405            // value
406            $done = false;
407
408            while ($i < $len) {
409                switch ($mapping[$i]) {
410                    case '[':
411                        // nested sequence
412                        $value = self::parseSequence($mapping, $i, $references);
413                        // Spec: Keys MUST be unique; first one wins.
414                        // Parser cannot abort this mapping earlier, since lines
415                        // are processed sequentially.
416                        // But overwriting is allowed when a merge node is used in current block.
417                        if ('<<' === $key) {
418                            foreach ($value as $parsedValue) {
419                                $output += $parsedValue;
420                            }
421                        } elseif ($allowOverwrite || !isset($output[$key])) {
422                            $output[$key] = $value;
423                        }
424                        $done = true;
425                        break;
426                    case '{':
427                        // nested mapping
428                        $value = self::parseMapping($mapping, $i, $references);
429                        // Spec: Keys MUST be unique; first one wins.
430                        // Parser cannot abort this mapping earlier, since lines
431                        // are processed sequentially.
432                        // But overwriting is allowed when a merge node is used in current block.
433                        if ('<<' === $key) {
434                            $output += $value;
435                        } elseif ($allowOverwrite || !isset($output[$key])) {
436                            $output[$key] = $value;
437                        }
438                        $done = true;
439                        break;
440                    case ':':
441                    case ' ':
442                        break;
443                    default:
444                        $value = self::parseScalar($mapping, array(',', '}'), array('"', "'"), $i, true, $references);
445                        // Spec: Keys MUST be unique; first one wins.
446                        // Parser cannot abort this mapping earlier, since lines
447                        // are processed sequentially.
448                        // But overwriting is allowed when a merge node is used in current block.
449                        if ('<<' === $key) {
450                            $output += $value;
451                        } elseif ($allowOverwrite || !isset($output[$key])) {
452                            $output[$key] = $value;
453                        }
454                        $done = true;
455                        --$i;
456                }
457
458                ++$i;
459
460                if ($done) {
461                    continue 2;
462                }
463            }
464        }
465
466        throw new ParseException(sprintf('Malformed inline YAML string: %s.', $mapping));
467    }
468
469    /**
470     * Evaluates scalars and replaces magic values.
471     *
472     * @param string $scalar
473     * @param array  $references
474     *
475     * @return mixed The evaluated YAML string
476     *
477     * @throws ParseException when object parsing support was disabled and the parser detected a PHP object or when a reference could not be resolved
478     */
479    private static function evaluateScalar($scalar, $references = array())
480    {
481        $scalar = trim($scalar);
482        $scalarLower = strtolower($scalar);
483
484        if (0 === strpos($scalar, '*')) {
485            if (false !== $pos = strpos($scalar, '#')) {
486                $value = substr($scalar, 1, $pos - 2);
487            } else {
488                $value = substr($scalar, 1);
489            }
490
491            // an unquoted *
492            if (false === $value || '' === $value) {
493                throw new ParseException('A reference must contain at least one character.');
494            }
495
496            if (!array_key_exists($value, $references)) {
497                throw new ParseException(sprintf('Reference "%s" does not exist.', $value));
498            }
499
500            return $references[$value];
501        }
502
503        switch (true) {
504            case 'null' === $scalarLower:
505            case '' === $scalar:
506            case '~' === $scalar:
507                return;
508            case 'true' === $scalarLower:
509                return true;
510            case 'false' === $scalarLower:
511                return false;
512            // Optimise for returning strings.
513            case '+' === $scalar[0] || '-' === $scalar[0] || '.' === $scalar[0] || '!' === $scalar[0] || is_numeric($scalar[0]):
514                switch (true) {
515                    case 0 === strpos($scalar, '!str'):
516                        return (string) substr($scalar, 5);
517                    case 0 === strpos($scalar, '! '):
518                        return (int) self::parseScalar(substr($scalar, 2));
519                    case 0 === strpos($scalar, '!php/object:'):
520                        if (self::$objectSupport) {
521                            return unserialize(substr($scalar, 12));
522                        }
523
524                        if (self::$exceptionOnInvalidType) {
525                            throw new ParseException('Object support when parsing a YAML file has been disabled.');
526                        }
527
528                        return;
529                    case 0 === strpos($scalar, '!!php/object:'):
530                        if (self::$objectSupport) {
531                            return unserialize(substr($scalar, 13));
532                        }
533
534                        if (self::$exceptionOnInvalidType) {
535                            throw new ParseException('Object support when parsing a YAML file has been disabled.');
536                        }
537
538                        return;
539                    case 0 === strpos($scalar, '!!float '):
540                        return (float) substr($scalar, 8);
541                    case ctype_digit($scalar):
542                        $raw = $scalar;
543                        $cast = (int) $scalar;
544
545                        return '0' == $scalar[0] ? octdec($scalar) : (((string) $raw == (string) $cast) ? $cast : $raw);
546                    case '-' === $scalar[0] && ctype_digit(substr($scalar, 1)):
547                        $raw = $scalar;
548                        $cast = (int) $scalar;
549
550                        return '0' == $scalar[1] ? octdec($scalar) : (((string) $raw === (string) $cast) ? $cast : $raw);
551                    case is_numeric($scalar):
552                    case Parser::preg_match(self::getHexRegex(), $scalar):
553                        return '0x' === $scalar[0].$scalar[1] ? hexdec($scalar) : (float) $scalar;
554                    case '.inf' === $scalarLower:
555                    case '.nan' === $scalarLower:
556                        return -log(0);
557                    case '-.inf' === $scalarLower:
558                        return log(0);
559                    case Parser::preg_match('/^(-|\+)?[0-9,]+(\.[0-9]+)?$/', $scalar):
560                        return (float) str_replace(',', '', $scalar);
561                    case Parser::preg_match(self::getTimestampRegex(), $scalar):
562                        $timeZone = date_default_timezone_get();
563                        date_default_timezone_set('UTC');
564                        $time = strtotime($scalar);
565                        date_default_timezone_set($timeZone);
566
567                        return $time;
568                }
569                // no break
570            default:
571                return (string) $scalar;
572        }
573    }
574
575    /**
576     * Gets a regex that matches a YAML date.
577     *
578     * @return string The regular expression
579     *
580     * @see http://www.yaml.org/spec/1.2/spec.html#id2761573
581     */
582    private static function getTimestampRegex()
583    {
584        return <<<EOF
585        ~^
586        (?P<year>[0-9][0-9][0-9][0-9])
587        -(?P<month>[0-9][0-9]?)
588        -(?P<day>[0-9][0-9]?)
589        (?:(?:[Tt]|[ \t]+)
590        (?P<hour>[0-9][0-9]?)
591        :(?P<minute>[0-9][0-9])
592        :(?P<second>[0-9][0-9])
593        (?:\.(?P<fraction>[0-9]*))?
594        (?:[ \t]*(?P<tz>Z|(?P<tz_sign>[-+])(?P<tz_hour>[0-9][0-9]?)
595        (?::(?P<tz_minute>[0-9][0-9]))?))?)?
596        $~x
597EOF;
598    }
599
600    /**
601     * Gets a regex that matches a YAML number in hexadecimal notation.
602     *
603     * @return string
604     */
605    private static function getHexRegex()
606    {
607        return '~^0x[0-9a-f]++$~i';
608    }
609}
610