1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * (c) Fabien Potencier <fabien@symfony.com> 6 * 7 * For the full copyright and license information, please view the LICENSE 8 * file that was distributed with this source code. 9 */ 10 11namespace Symfony\Component\Yaml; 12 13use Symfony\Component\Yaml\Exception\ParseException; 14use Symfony\Component\Yaml\Exception\DumpException; 15 16/** 17 * Inline implements a YAML parser/dumper for the YAML inline syntax. 18 * 19 * @author Fabien Potencier <fabien@symfony.com> 20 */ 21class Inline 22{ 23 const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\']*(?:\'\'[^\']*)*)\')'; 24 25 private static $exceptionOnInvalidType = false; 26 private static $objectSupport = false; 27 28 /** 29 * Converts a YAML string to a PHP array. 30 * 31 * @param string $value A YAML string 32 * @param Boolean $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise 33 * @param Boolean $objectSupport true if object support is enabled, false otherwise 34 * 35 * @return array A PHP array representing the YAML string 36 * 37 * @throws ParseException 38 */ 39 public static function parse($value, $exceptionOnInvalidType = false, $objectSupport = false) 40 { 41 self::$exceptionOnInvalidType = $exceptionOnInvalidType; 42 self::$objectSupport = $objectSupport; 43 44 $value = trim($value); 45 46 if (0 == strlen($value)) { 47 return ''; 48 } 49 50 if (function_exists('mb_internal_encoding') && ((int) ini_get('mbstring.func_overload')) & 2) { 51 $mbEncoding = mb_internal_encoding(); 52 mb_internal_encoding('ASCII'); 53 } 54 55 $i = 0; 56 switch ($value[0]) { 57 case '[': 58 $result = self::parseSequence($value, $i); 59 ++$i; 60 break; 61 case '{': 62 $result = self::parseMapping($value, $i); 63 ++$i; 64 break; 65 default: 66 $result = self::parseScalar($value, null, array('"', "'"), $i); 67 } 68 69 // some comments are allowed at the end 70 if (preg_replace('/\s+#.*$/A', '', substr($value, $i))) { 71 throw new ParseException(sprintf('Unexpected characters near "%s".', substr($value, $i))); 72 } 73 74 if (isset($mbEncoding)) { 75 mb_internal_encoding($mbEncoding); 76 } 77 78 return $result; 79 } 80 81 /** 82 * Dumps a given PHP variable to a YAML string. 83 * 84 * @param mixed $value The PHP variable to convert 85 * @param Boolean $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise 86 * @param Boolean $objectSupport true if object support is enabled, false otherwise 87 * 88 * @return string The YAML string representing the PHP array 89 * 90 * @throws DumpException When trying to dump PHP resource 91 */ 92 public static function dump($value, $exceptionOnInvalidType = false, $objectSupport = false) 93 { 94 switch (true) { 95 case is_resource($value): 96 if ($exceptionOnInvalidType) { 97 throw new DumpException(sprintf('Unable to dump PHP resources in a YAML file ("%s").', get_resource_type($value))); 98 } 99 100 return 'null'; 101 case is_object($value): 102 if ($objectSupport) { 103 return '!!php/object:'.serialize($value); 104 } 105 106 if ($exceptionOnInvalidType) { 107 throw new DumpException('Object support when dumping a YAML file has been disabled.'); 108 } 109 110 return 'null'; 111 case is_array($value): 112 return self::dumpArray($value, $exceptionOnInvalidType, $objectSupport); 113 case null === $value: 114 return 'null'; 115 case true === $value: 116 return 'true'; 117 case false === $value: 118 return 'false'; 119 case ctype_digit($value): 120 return is_string($value) ? "'$value'" : (int) $value; 121 case is_numeric($value): 122 $locale = setlocale(LC_NUMERIC, 0); 123 if (false !== $locale) { 124 setlocale(LC_NUMERIC, 'C'); 125 } 126 $repr = is_string($value) ? "'$value'" : (is_infinite($value) ? str_ireplace('INF', '.Inf', strval($value)) : strval($value)); 127 128 if (false !== $locale) { 129 setlocale(LC_NUMERIC, $locale); 130 } 131 132 return $repr; 133 case Escaper::requiresDoubleQuoting($value): 134 return Escaper::escapeWithDoubleQuotes($value); 135 case Escaper::requiresSingleQuoting($value): 136 return Escaper::escapeWithSingleQuotes($value); 137 case '' == $value: 138 return "''"; 139 case preg_match(self::getTimestampRegex(), $value): 140 case in_array(strtolower($value), array('null', '~', 'true', 'false')): 141 return "'$value'"; 142 default: 143 return $value; 144 } 145 } 146 147 /** 148 * Dumps a PHP array to a YAML string. 149 * 150 * @param array $value The PHP array to dump 151 * @param Boolean $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise 152 * @param Boolean $objectSupport true if object support is enabled, false otherwise 153 * 154 * @return string The YAML string representing the PHP array 155 */ 156 private static function dumpArray($value, $exceptionOnInvalidType, $objectSupport) 157 { 158 // array 159 $keys = array_keys($value); 160 if ((1 == count($keys) && '0' == $keys[0]) 161 || (count($keys) > 1 && array_reduce($keys, function ($v, $w) { return (integer) $v + $w; }, 0) == count($keys) * (count($keys) - 1) / 2) 162 ) { 163 $output = array(); 164 foreach ($value as $val) { 165 $output[] = self::dump($val, $exceptionOnInvalidType, $objectSupport); 166 } 167 168 return sprintf('[%s]', implode(', ', $output)); 169 } 170 171 // mapping 172 $output = array(); 173 foreach ($value as $key => $val) { 174 $output[] = sprintf('%s: %s', self::dump($key, $exceptionOnInvalidType, $objectSupport), self::dump($val, $exceptionOnInvalidType, $objectSupport)); 175 } 176 177 return sprintf('{ %s }', implode(', ', $output)); 178 } 179 180 /** 181 * Parses a scalar to a YAML string. 182 * 183 * @param scalar $scalar 184 * @param string $delimiters 185 * @param array $stringDelimiters 186 * @param integer &$i 187 * @param Boolean $evaluate 188 * 189 * @return string A YAML string 190 * 191 * @throws ParseException When malformed inline YAML string is parsed 192 */ 193 public static function parseScalar($scalar, $delimiters = null, $stringDelimiters = array('"', "'"), &$i = 0, $evaluate = true) 194 { 195 if (in_array($scalar[$i], $stringDelimiters)) { 196 // quoted scalar 197 $output = self::parseQuotedScalar($scalar, $i); 198 199 if (null !== $delimiters) { 200 $tmp = ltrim(substr($scalar, $i), ' '); 201 if (!in_array($tmp[0], $delimiters)) { 202 throw new ParseException(sprintf('Unexpected characters (%s).', substr($scalar, $i))); 203 } 204 } 205 } else { 206 // "normal" string 207 if (!$delimiters) { 208 $output = substr($scalar, $i); 209 $i += strlen($output); 210 211 // remove comments 212 if (false !== $strpos = strpos($output, ' #')) { 213 $output = rtrim(substr($output, 0, $strpos)); 214 } 215 } elseif (preg_match('/^(.+?)('.implode('|', $delimiters).')/', substr($scalar, $i), $match)) { 216 $output = $match[1]; 217 $i += strlen($output); 218 } else { 219 throw new ParseException(sprintf('Malformed inline YAML string (%s).', $scalar)); 220 } 221 222 $output = $evaluate ? self::evaluateScalar($output) : $output; 223 } 224 225 return $output; 226 } 227 228 /** 229 * Parses a quoted scalar to YAML. 230 * 231 * @param string $scalar 232 * @param integer &$i 233 * 234 * @return string A YAML string 235 * 236 * @throws ParseException When malformed inline YAML string is parsed 237 */ 238 private static function parseQuotedScalar($scalar, &$i) 239 { 240 if (!preg_match('/'.self::REGEX_QUOTED_STRING.'/Au', substr($scalar, $i), $match)) { 241 throw new ParseException(sprintf('Malformed inline YAML string (%s).', substr($scalar, $i))); 242 } 243 244 $output = substr($match[0], 1, strlen($match[0]) - 2); 245 246 $unescaper = new Unescaper(); 247 if ('"' == $scalar[$i]) { 248 $output = $unescaper->unescapeDoubleQuotedString($output); 249 } else { 250 $output = $unescaper->unescapeSingleQuotedString($output); 251 } 252 253 $i += strlen($match[0]); 254 255 return $output; 256 } 257 258 /** 259 * Parses a sequence to a YAML string. 260 * 261 * @param string $sequence 262 * @param integer &$i 263 * 264 * @return string A YAML string 265 * 266 * @throws ParseException When malformed inline YAML string is parsed 267 */ 268 private static function parseSequence($sequence, &$i = 0) 269 { 270 $output = array(); 271 $len = strlen($sequence); 272 $i += 1; 273 274 // [foo, bar, ...] 275 while ($i < $len) { 276 switch ($sequence[$i]) { 277 case '[': 278 // nested sequence 279 $output[] = self::parseSequence($sequence, $i); 280 break; 281 case '{': 282 // nested mapping 283 $output[] = self::parseMapping($sequence, $i); 284 break; 285 case ']': 286 return $output; 287 case ',': 288 case ' ': 289 break; 290 default: 291 $isQuoted = in_array($sequence[$i], array('"', "'")); 292 $value = self::parseScalar($sequence, array(',', ']'), array('"', "'"), $i); 293 294 if (!$isQuoted && false !== strpos($value, ': ')) { 295 // embedded mapping? 296 try { 297 $value = self::parseMapping('{'.$value.'}'); 298 } catch (\InvalidArgumentException $e) { 299 // no, it's not 300 } 301 } 302 303 $output[] = $value; 304 305 --$i; 306 } 307 308 ++$i; 309 } 310 311 throw new ParseException(sprintf('Malformed inline YAML string %s', $sequence)); 312 } 313 314 /** 315 * Parses a mapping to a YAML string. 316 * 317 * @param string $mapping 318 * @param integer &$i 319 * 320 * @return string A YAML string 321 * 322 * @throws ParseException When malformed inline YAML string is parsed 323 */ 324 private static function parseMapping($mapping, &$i = 0) 325 { 326 $output = array(); 327 $len = strlen($mapping); 328 $i += 1; 329 330 // {foo: bar, bar:foo, ...} 331 while ($i < $len) { 332 switch ($mapping[$i]) { 333 case ' ': 334 case ',': 335 ++$i; 336 continue 2; 337 case '}': 338 return $output; 339 } 340 341 // key 342 $key = self::parseScalar($mapping, array(':', ' '), array('"', "'"), $i, false); 343 344 // value 345 $done = false; 346 while ($i < $len) { 347 switch ($mapping[$i]) { 348 case '[': 349 // nested sequence 350 $output[$key] = self::parseSequence($mapping, $i); 351 $done = true; 352 break; 353 case '{': 354 // nested mapping 355 $output[$key] = self::parseMapping($mapping, $i); 356 $done = true; 357 break; 358 case ':': 359 case ' ': 360 break; 361 default: 362 $output[$key] = self::parseScalar($mapping, array(',', '}'), array('"', "'"), $i); 363 $done = true; 364 --$i; 365 } 366 367 ++$i; 368 369 if ($done) { 370 continue 2; 371 } 372 } 373 } 374 375 throw new ParseException(sprintf('Malformed inline YAML string %s', $mapping)); 376 } 377 378 /** 379 * Evaluates scalars and replaces magic values. 380 * 381 * @param string $scalar 382 * 383 * @return string A YAML string 384 */ 385 private static function evaluateScalar($scalar) 386 { 387 $scalar = trim($scalar); 388 389 switch (true) { 390 case 'null' == strtolower($scalar): 391 case '' == $scalar: 392 case '~' == $scalar: 393 return null; 394 case 0 === strpos($scalar, '!str'): 395 return (string) substr($scalar, 5); 396 case 0 === strpos($scalar, '! '): 397 return intval(self::parseScalar(substr($scalar, 2))); 398 case 0 === strpos($scalar, '!!php/object:'): 399 if (self::$objectSupport) { 400 return unserialize(substr($scalar, 13)); 401 } 402 403 if (self::$exceptionOnInvalidType) { 404 throw new ParseException('Object support when parsing a YAML file has been disabled.'); 405 } 406 407 return null; 408 case ctype_digit($scalar): 409 $raw = $scalar; 410 $cast = intval($scalar); 411 412 return '0' == $scalar[0] ? octdec($scalar) : (((string) $raw == (string) $cast) ? $cast : $raw); 413 case '-' === $scalar[0] && ctype_digit(substr($scalar, 1)): 414 $raw = $scalar; 415 $cast = intval($scalar); 416 417 return '0' == $scalar[1] ? octdec($scalar) : (((string) $raw == (string) $cast) ? $cast : $raw); 418 case 'true' === strtolower($scalar): 419 return true; 420 case 'false' === strtolower($scalar): 421 return false; 422 case is_numeric($scalar): 423 return '0x' == $scalar[0].$scalar[1] ? hexdec($scalar) : floatval($scalar); 424 case 0 == strcasecmp($scalar, '.inf'): 425 case 0 == strcasecmp($scalar, '.NaN'): 426 return -log(0); 427 case 0 == strcasecmp($scalar, '-.inf'): 428 return log(0); 429 case preg_match('/^(-|\+)?[0-9,]+(\.[0-9]+)?$/', $scalar): 430 return floatval(str_replace(',', '', $scalar)); 431 case preg_match(self::getTimestampRegex(), $scalar): 432 return strtotime($scalar); 433 default: 434 return (string) $scalar; 435 } 436 } 437 438 /** 439 * Gets a regex that matches a YAML date. 440 * 441 * @return string The regular expression 442 * 443 * @see http://www.yaml.org/spec/1.2/spec.html#id2761573 444 */ 445 private static function getTimestampRegex() 446 { 447 return <<<EOF 448 ~^ 449 (?P<year>[0-9][0-9][0-9][0-9]) 450 -(?P<month>[0-9][0-9]?) 451 -(?P<day>[0-9][0-9]?) 452 (?:(?:[Tt]|[ \t]+) 453 (?P<hour>[0-9][0-9]?) 454 :(?P<minute>[0-9][0-9]) 455 :(?P<second>[0-9][0-9]) 456 (?:\.(?P<fraction>[0-9]*))? 457 (?:[ \t]*(?P<tz>Z|(?P<tz_sign>[-+])(?P<tz_hour>[0-9][0-9]?) 458 (?::(?P<tz_minute>[0-9][0-9]))?))?)? 459 $~x 460EOF; 461 } 462} 463