1 <?php
2
3 4 5 6 7 8 9 10
11
12 namespace Symfony\Component\Yaml;
13
14 use Symfony\Component\Yaml\Exception\ParseException;
15 use Symfony\Component\Yaml\Exception\DumpException;
16
17 18 19 20 21
22 class 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 32 33 34 35 36 37 38 39 40 41 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 & (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
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 88 89 90 91 92 93 94 95 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
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 162 163 164 165 166 167 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 184 185 186 187 188 189 190
191 private static function dumpArray($value, $exceptionOnInvalidType, $objectSupport)
192 {
193
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
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 214 215 216 217 218 219 220 221 222 223 224 225 226 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
232 $output = self::parseQuotedScalar($scalar, $i);
233
234 if (null !== $delimiters) {
235 $tmp = ltrim(substr($scalar, $i), ' ');
236 if (!in_array($tmp[0], $delimiters)) {
237 throw new ParseException(sprintf('Unexpected characters (%s).', substr($scalar, $i)));
238 }
239 }
240 } else {
241
242 if (!$delimiters) {
243 $output = substr($scalar, $i);
244 $i += strlen($output);
245
246
247 if (Parser::preg_match('/[ \t]+#/', $output, $match, PREG_OFFSET_CAPTURE)) {
248 $output = substr($output, 0, $match[0][1]);
249 }
250 } elseif (Parser::preg_match('/^(.+?)('.implode('|', $delimiters).')/', substr($scalar, $i), $match)) {
251 $output = $match[1];
252 $i += strlen($output);
253 } else {
254 throw new ParseException(sprintf('Malformed inline YAML string: %s.', $scalar));
255 }
256
257
258 if ($output && ('@' === $output[0] || '`' === $output[0] || '|' === $output[0] || '>' === $output[0])) {
259 @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);
260
261
262
263 }
264
265 if ($evaluate) {
266 $output = self::evaluateScalar($output, $references);
267 }
268 }
269
270 return $output;
271 }
272
273 274 275 276 277 278 279 280 281 282
283 private static function parseQuotedScalar($scalar, &$i)
284 {
285 if (!Parser::preg_match('/'.self::REGEX_QUOTED_STRING.'/Au', substr($scalar, $i), $match)) {
286 throw new ParseException(sprintf('Malformed inline YAML string: %s.', substr($scalar, $i)));
287 }
288
289 $output = substr($match[0], 1, strlen($match[0]) - 2);
290
291 $unescaper = new Unescaper();
292 if ('"' == $scalar[$i]) {
293 $output = $unescaper->unescapeDoubleQuotedString($output);
294 } else {
295 $output = $unescaper->unescapeSingleQuotedString($output);
296 }
297
298 $i += strlen($match[0]);
299
300 return $output;
301 }
302
303 304 305 306 307 308 309 310 311 312 313
314 private static function parseSequence($sequence, &$i = 0, $references = array())
315 {
316 $output = array();
317 $len = strlen($sequence);
318 ++$i;
319
320
321 while ($i < $len) {
322 switch ($sequence[$i]) {
323 case '[':
324
325 $output[] = self::parseSequence($sequence, $i, $references);
326 break;
327 case '{':
328
329 $output[] = self::parseMapping($sequence, $i, $references);
330 break;
331 case ']':
332 return $output;
333 case ',':
334 case ' ':
335 break;
336 default:
337 $isQuoted = in_array($sequence[$i], array('"', "'"));
338 $value = self::parseScalar($sequence, array(',', ']'), array('"', "'"), $i, true, $references);
339
340
341 if (!is_array($value) && !$isQuoted && false !== strpos($value, ': ')) {
342
343 try {
344 $pos = 0;
345 $value = self::parseMapping('{'.$value.'}', $pos, $references);
346 } catch (\InvalidArgumentException $e) {
347
348 }
349 }
350
351 $output[] = $value;
352
353 --$i;
354 }
355
356 ++$i;
357 }
358
359 throw new ParseException(sprintf('Malformed inline YAML string: %s.', $sequence));
360 }
361
362 363 364 365 366 367 368 369 370 371 372
373 private static function parseMapping($mapping, &$i = 0, $references = array())
374 {
375 $output = array();
376 $len = strlen($mapping);
377 ++$i;
378
379
380 while ($i < $len) {
381 switch ($mapping[$i]) {
382 case ' ':
383 case ',':
384 ++$i;
385 continue 2;
386 case '}':
387 if (self::$objectForMap) {
388 return (object) $output;
389 }
390
391 return $output;
392 }
393
394
395 $key = self::parseScalar($mapping, array(':', ' '), array('"', "'"), $i, false);
396
397
398 $done = false;
399
400 while ($i < $len) {
401 switch ($mapping[$i]) {
402 case '[':
403
404 $value = self::parseSequence($mapping, $i, $references);
405
406
407
408 if (!isset($output[$key])) {
409 $output[$key] = $value;
410 }
411 $done = true;
412 break;
413 case '{':
414
415 $value = self::parseMapping($mapping, $i, $references);
416
417
418
419 if (!isset($output[$key])) {
420 $output[$key] = $value;
421 }
422 $done = true;
423 break;
424 case ':':
425 case ' ':
426 break;
427 default:
428 $value = self::parseScalar($mapping, array(',', '}'), array('"', "'"), $i, true, $references);
429
430
431
432 if (!isset($output[$key])) {
433 $output[$key] = $value;
434 }
435 $done = true;
436 --$i;
437 }
438
439 ++$i;
440
441 if ($done) {
442 continue 2;
443 }
444 }
445 }
446
447 throw new ParseException(sprintf('Malformed inline YAML string: %s.', $mapping));
448 }
449
450 451 452 453 454 455 456 457 458 459
460 private static function evaluateScalar($scalar, $references = array())
461 {
462 $scalar = trim($scalar);
463 $scalarLower = strtolower($scalar);
464
465 if (0 === strpos($scalar, '*')) {
466 if (false !== $pos = strpos($scalar, '#')) {
467 $value = substr($scalar, 1, $pos - 2);
468 } else {
469 $value = substr($scalar, 1);
470 }
471
472
473 if (false === $value || '' === $value) {
474 throw new ParseException('A reference must contain at least one character.');
475 }
476
477 if (!array_key_exists($value, $references)) {
478 throw new ParseException(sprintf('Reference "%s" does not exist.', $value));
479 }
480
481 return $references[$value];
482 }
483
484 switch (true) {
485 case 'null' === $scalarLower:
486 case '' === $scalar:
487 case '~' === $scalar:
488 return;
489 case 'true' === $scalarLower:
490 return true;
491 case 'false' === $scalarLower:
492 return false;
493
494 case $scalar[0] === '+' || $scalar[0] === '-' || $scalar[0] === '.' || $scalar[0] === '!' || is_numeric($scalar[0]):
495 switch (true) {
496 case 0 === strpos($scalar, '!str'):
497 return (string) substr($scalar, 5);
498 case 0 === strpos($scalar, '! '):
499 return (int) self::parseScalar(substr($scalar, 2));
500 case 0 === strpos($scalar, '!php/object:'):
501 if (self::$objectSupport) {
502 return unserialize(substr($scalar, 12));
503 }
504
505 if (self::$exceptionOnInvalidType) {
506 throw new ParseException('Object support when parsing a YAML file has been disabled.');
507 }
508
509 return;
510 case 0 === strpos($scalar, '!!php/object:'):
511 if (self::$objectSupport) {
512 return unserialize(substr($scalar, 13));
513 }
514
515 if (self::$exceptionOnInvalidType) {
516 throw new ParseException('Object support when parsing a YAML file has been disabled.');
517 }
518
519 return;
520 case 0 === strpos($scalar, '!!float '):
521 return (float) substr($scalar, 8);
522 case ctype_digit($scalar):
523 $raw = $scalar;
524 $cast = (int) $scalar;
525
526 return '0' == $scalar[0] ? octdec($scalar) : (((string) $raw == (string) $cast) ? $cast : $raw);
527 case '-' === $scalar[0] && ctype_digit(substr($scalar, 1)):
528 $raw = $scalar;
529 $cast = (int) $scalar;
530
531 return '0' == $scalar[1] ? octdec($scalar) : (((string) $raw === (string) $cast) ? $cast : $raw);
532 case is_numeric($scalar):
533 case Parser::preg_match(self::getHexRegex(), $scalar):
534 return '0x' === $scalar[0].$scalar[1] ? hexdec($scalar) : (float) $scalar;
535 case '.inf' === $scalarLower:
536 case '.nan' === $scalarLower:
537 return -log(0);
538 case '-.inf' === $scalarLower:
539 return log(0);
540 case Parser::preg_match('/^(-|\+)?[0-9,]+(\.[0-9]+)?$/', $scalar):
541 return (float) str_replace(',', '', $scalar);
542 case Parser::preg_match(self::getTimestampRegex(), $scalar):
543 $timeZone = date_default_timezone_get();
544 date_default_timezone_set('UTC');
545 $time = strtotime($scalar);
546 date_default_timezone_set($timeZone);
547
548 return $time;
549 }
550 default:
551 return (string) $scalar;
552 }
553 }
554
555 556 557 558 559 560 561
562 private static function getTimestampRegex()
563 {
564 return <<<EOF
565 ~^
566 (?P<year>[0-9][0-9][0-9][0-9])
567 -(?P<month>[0-9][0-9]?)
568 -(?P<day>[0-9][0-9]?)
569 (?:(?:[Tt]|[ \t]+)
570 (?P<hour>[0-9][0-9]?)
571 :(?P<minute>[0-9][0-9])
572 :(?P<second>[0-9][0-9])
573 (?:\.(?P<fraction>[0-9]*))?
574 (?:[ \t]*(?P<tz>Z|(?P<tz_sign>[-+])(?P<tz_hour>[0-9][0-9]?)
575 (?::(?P<tz_minute>[0-9][0-9]))?))?)?
576 $~x
577 EOF;
578 }
579
580 581 582 583 584
585 private static function getHexRegex()
586 {
587 return '~^0x[0-9a-f]++$~i';
588 }
589 }
590