1*04fd306cSNickeau<?php 2*04fd306cSNickeau 3*04fd306cSNickeau/* 4*04fd306cSNickeau * This file is part of the Symfony package. 5*04fd306cSNickeau * 6*04fd306cSNickeau * (c) Fabien Potencier <fabien@symfony.com> 7*04fd306cSNickeau * 8*04fd306cSNickeau * For the full copyright and license information, please view the LICENSE 9*04fd306cSNickeau * file that was distributed with this source code. 10*04fd306cSNickeau */ 11*04fd306cSNickeau 12*04fd306cSNickeaunamespace Symfony\Component\Yaml; 13*04fd306cSNickeau 14*04fd306cSNickeauuse Symfony\Component\Yaml\Exception\DumpException; 15*04fd306cSNickeauuse Symfony\Component\Yaml\Exception\ParseException; 16*04fd306cSNickeauuse Symfony\Component\Yaml\Tag\TaggedValue; 17*04fd306cSNickeau 18*04fd306cSNickeau/** 19*04fd306cSNickeau * Inline implements a YAML parser/dumper for the YAML inline syntax. 20*04fd306cSNickeau * 21*04fd306cSNickeau * @author Fabien Potencier <fabien@symfony.com> 22*04fd306cSNickeau * 23*04fd306cSNickeau * @internal 24*04fd306cSNickeau */ 25*04fd306cSNickeauclass Inline 26*04fd306cSNickeau{ 27*04fd306cSNickeau public const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+)"|\'([^\']*+(?:\'\'[^\']*+)*+)\')'; 28*04fd306cSNickeau 29*04fd306cSNickeau public static $parsedLineNumber = -1; 30*04fd306cSNickeau public static $parsedFilename; 31*04fd306cSNickeau 32*04fd306cSNickeau private static $exceptionOnInvalidType = false; 33*04fd306cSNickeau private static $objectSupport = false; 34*04fd306cSNickeau private static $objectForMap = false; 35*04fd306cSNickeau private static $constantSupport = false; 36*04fd306cSNickeau 37*04fd306cSNickeau public static function initialize(int $flags, int $parsedLineNumber = null, string $parsedFilename = null) 38*04fd306cSNickeau { 39*04fd306cSNickeau self::$exceptionOnInvalidType = (bool) (Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE & $flags); 40*04fd306cSNickeau self::$objectSupport = (bool) (Yaml::PARSE_OBJECT & $flags); 41*04fd306cSNickeau self::$objectForMap = (bool) (Yaml::PARSE_OBJECT_FOR_MAP & $flags); 42*04fd306cSNickeau self::$constantSupport = (bool) (Yaml::PARSE_CONSTANT & $flags); 43*04fd306cSNickeau self::$parsedFilename = $parsedFilename; 44*04fd306cSNickeau 45*04fd306cSNickeau if (null !== $parsedLineNumber) { 46*04fd306cSNickeau self::$parsedLineNumber = $parsedLineNumber; 47*04fd306cSNickeau } 48*04fd306cSNickeau } 49*04fd306cSNickeau 50*04fd306cSNickeau /** 51*04fd306cSNickeau * Converts a YAML string to a PHP value. 52*04fd306cSNickeau * 53*04fd306cSNickeau * @param string|null $value A YAML string 54*04fd306cSNickeau * @param int $flags A bit field of Yaml::PARSE_* constants to customize the YAML parser behavior 55*04fd306cSNickeau * @param array $references Mapping of variable names to values 56*04fd306cSNickeau * 57*04fd306cSNickeau * @return mixed 58*04fd306cSNickeau * 59*04fd306cSNickeau * @throws ParseException 60*04fd306cSNickeau */ 61*04fd306cSNickeau public static function parse(string $value = null, int $flags = 0, array &$references = []) 62*04fd306cSNickeau { 63*04fd306cSNickeau self::initialize($flags); 64*04fd306cSNickeau 65*04fd306cSNickeau $value = trim($value); 66*04fd306cSNickeau 67*04fd306cSNickeau if ('' === $value) { 68*04fd306cSNickeau return ''; 69*04fd306cSNickeau } 70*04fd306cSNickeau 71*04fd306cSNickeau if (2 /* MB_OVERLOAD_STRING */ & (int) \ini_get('mbstring.func_overload')) { 72*04fd306cSNickeau $mbEncoding = mb_internal_encoding(); 73*04fd306cSNickeau mb_internal_encoding('ASCII'); 74*04fd306cSNickeau } 75*04fd306cSNickeau 76*04fd306cSNickeau try { 77*04fd306cSNickeau $i = 0; 78*04fd306cSNickeau $tag = self::parseTag($value, $i, $flags); 79*04fd306cSNickeau switch ($value[$i]) { 80*04fd306cSNickeau case '[': 81*04fd306cSNickeau $result = self::parseSequence($value, $flags, $i, $references); 82*04fd306cSNickeau ++$i; 83*04fd306cSNickeau break; 84*04fd306cSNickeau case '{': 85*04fd306cSNickeau $result = self::parseMapping($value, $flags, $i, $references); 86*04fd306cSNickeau ++$i; 87*04fd306cSNickeau break; 88*04fd306cSNickeau default: 89*04fd306cSNickeau $result = self::parseScalar($value, $flags, null, $i, true, $references); 90*04fd306cSNickeau } 91*04fd306cSNickeau 92*04fd306cSNickeau // some comments are allowed at the end 93*04fd306cSNickeau if (preg_replace('/\s*#.*$/A', '', substr($value, $i))) { 94*04fd306cSNickeau throw new ParseException(sprintf('Unexpected characters near "%s".', substr($value, $i)), self::$parsedLineNumber + 1, $value, self::$parsedFilename); 95*04fd306cSNickeau } 96*04fd306cSNickeau 97*04fd306cSNickeau if (null !== $tag && '' !== $tag) { 98*04fd306cSNickeau return new TaggedValue($tag, $result); 99*04fd306cSNickeau } 100*04fd306cSNickeau 101*04fd306cSNickeau return $result; 102*04fd306cSNickeau } finally { 103*04fd306cSNickeau if (isset($mbEncoding)) { 104*04fd306cSNickeau mb_internal_encoding($mbEncoding); 105*04fd306cSNickeau } 106*04fd306cSNickeau } 107*04fd306cSNickeau } 108*04fd306cSNickeau 109*04fd306cSNickeau /** 110*04fd306cSNickeau * Dumps a given PHP variable to a YAML string. 111*04fd306cSNickeau * 112*04fd306cSNickeau * @param mixed $value The PHP variable to convert 113*04fd306cSNickeau * @param int $flags A bit field of Yaml::DUMP_* constants to customize the dumped YAML string 114*04fd306cSNickeau * 115*04fd306cSNickeau * @throws DumpException When trying to dump PHP resource 116*04fd306cSNickeau */ 117*04fd306cSNickeau public static function dump($value, int $flags = 0): string 118*04fd306cSNickeau { 119*04fd306cSNickeau switch (true) { 120*04fd306cSNickeau case \is_resource($value): 121*04fd306cSNickeau if (Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE & $flags) { 122*04fd306cSNickeau throw new DumpException(sprintf('Unable to dump PHP resources in a YAML file ("%s").', get_resource_type($value))); 123*04fd306cSNickeau } 124*04fd306cSNickeau 125*04fd306cSNickeau return self::dumpNull($flags); 126*04fd306cSNickeau case $value instanceof \DateTimeInterface: 127*04fd306cSNickeau return $value->format('c'); 128*04fd306cSNickeau case $value instanceof \UnitEnum: 129*04fd306cSNickeau return sprintf('!php/const %s::%s', \get_class($value), $value->name); 130*04fd306cSNickeau case \is_object($value): 131*04fd306cSNickeau if ($value instanceof TaggedValue) { 132*04fd306cSNickeau return '!'.$value->getTag().' '.self::dump($value->getValue(), $flags); 133*04fd306cSNickeau } 134*04fd306cSNickeau 135*04fd306cSNickeau if (Yaml::DUMP_OBJECT & $flags) { 136*04fd306cSNickeau return '!php/object '.self::dump(serialize($value)); 137*04fd306cSNickeau } 138*04fd306cSNickeau 139*04fd306cSNickeau if (Yaml::DUMP_OBJECT_AS_MAP & $flags && ($value instanceof \stdClass || $value instanceof \ArrayObject)) { 140*04fd306cSNickeau $output = []; 141*04fd306cSNickeau 142*04fd306cSNickeau foreach ($value as $key => $val) { 143*04fd306cSNickeau $output[] = sprintf('%s: %s', self::dump($key, $flags), self::dump($val, $flags)); 144*04fd306cSNickeau } 145*04fd306cSNickeau 146*04fd306cSNickeau return sprintf('{ %s }', implode(', ', $output)); 147*04fd306cSNickeau } 148*04fd306cSNickeau 149*04fd306cSNickeau if (Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE & $flags) { 150*04fd306cSNickeau throw new DumpException('Object support when dumping a YAML file has been disabled.'); 151*04fd306cSNickeau } 152*04fd306cSNickeau 153*04fd306cSNickeau return self::dumpNull($flags); 154*04fd306cSNickeau case \is_array($value): 155*04fd306cSNickeau return self::dumpArray($value, $flags); 156*04fd306cSNickeau case null === $value: 157*04fd306cSNickeau return self::dumpNull($flags); 158*04fd306cSNickeau case true === $value: 159*04fd306cSNickeau return 'true'; 160*04fd306cSNickeau case false === $value: 161*04fd306cSNickeau return 'false'; 162*04fd306cSNickeau case \is_int($value): 163*04fd306cSNickeau return $value; 164*04fd306cSNickeau case is_numeric($value) && false === strpbrk($value, "\f\n\r\t\v"): 165*04fd306cSNickeau $locale = setlocale(\LC_NUMERIC, 0); 166*04fd306cSNickeau if (false !== $locale) { 167*04fd306cSNickeau setlocale(\LC_NUMERIC, 'C'); 168*04fd306cSNickeau } 169*04fd306cSNickeau if (\is_float($value)) { 170*04fd306cSNickeau $repr = (string) $value; 171*04fd306cSNickeau if (is_infinite($value)) { 172*04fd306cSNickeau $repr = str_ireplace('INF', '.Inf', $repr); 173*04fd306cSNickeau } elseif (floor($value) == $value && $repr == $value) { 174*04fd306cSNickeau // Preserve float data type since storing a whole number will result in integer value. 175*04fd306cSNickeau if (false === strpos($repr, 'E')) { 176*04fd306cSNickeau $repr = $repr.'.0'; 177*04fd306cSNickeau } 178*04fd306cSNickeau } 179*04fd306cSNickeau } else { 180*04fd306cSNickeau $repr = \is_string($value) ? "'$value'" : (string) $value; 181*04fd306cSNickeau } 182*04fd306cSNickeau if (false !== $locale) { 183*04fd306cSNickeau setlocale(\LC_NUMERIC, $locale); 184*04fd306cSNickeau } 185*04fd306cSNickeau 186*04fd306cSNickeau return $repr; 187*04fd306cSNickeau case '' == $value: 188*04fd306cSNickeau return "''"; 189*04fd306cSNickeau case self::isBinaryString($value): 190*04fd306cSNickeau return '!!binary '.base64_encode($value); 191*04fd306cSNickeau case Escaper::requiresDoubleQuoting($value): 192*04fd306cSNickeau return Escaper::escapeWithDoubleQuotes($value); 193*04fd306cSNickeau case Escaper::requiresSingleQuoting($value): 194*04fd306cSNickeau case Parser::preg_match('{^[0-9]+[_0-9]*$}', $value): 195*04fd306cSNickeau case Parser::preg_match(self::getHexRegex(), $value): 196*04fd306cSNickeau case Parser::preg_match(self::getTimestampRegex(), $value): 197*04fd306cSNickeau return Escaper::escapeWithSingleQuotes($value); 198*04fd306cSNickeau default: 199*04fd306cSNickeau return $value; 200*04fd306cSNickeau } 201*04fd306cSNickeau } 202*04fd306cSNickeau 203*04fd306cSNickeau /** 204*04fd306cSNickeau * Check if given array is hash or just normal indexed array. 205*04fd306cSNickeau * 206*04fd306cSNickeau * @param array|\ArrayObject|\stdClass $value The PHP array or array-like object to check 207*04fd306cSNickeau */ 208*04fd306cSNickeau public static function isHash($value): bool 209*04fd306cSNickeau { 210*04fd306cSNickeau if ($value instanceof \stdClass || $value instanceof \ArrayObject) { 211*04fd306cSNickeau return true; 212*04fd306cSNickeau } 213*04fd306cSNickeau 214*04fd306cSNickeau $expectedKey = 0; 215*04fd306cSNickeau 216*04fd306cSNickeau foreach ($value as $key => $val) { 217*04fd306cSNickeau if ($key !== $expectedKey++) { 218*04fd306cSNickeau return true; 219*04fd306cSNickeau } 220*04fd306cSNickeau } 221*04fd306cSNickeau 222*04fd306cSNickeau return false; 223*04fd306cSNickeau } 224*04fd306cSNickeau 225*04fd306cSNickeau /** 226*04fd306cSNickeau * Dumps a PHP array to a YAML string. 227*04fd306cSNickeau * 228*04fd306cSNickeau * @param array $value The PHP array to dump 229*04fd306cSNickeau * @param int $flags A bit field of Yaml::DUMP_* constants to customize the dumped YAML string 230*04fd306cSNickeau */ 231*04fd306cSNickeau private static function dumpArray(array $value, int $flags): string 232*04fd306cSNickeau { 233*04fd306cSNickeau // array 234*04fd306cSNickeau if (($value || Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE & $flags) && !self::isHash($value)) { 235*04fd306cSNickeau $output = []; 236*04fd306cSNickeau foreach ($value as $val) { 237*04fd306cSNickeau $output[] = self::dump($val, $flags); 238*04fd306cSNickeau } 239*04fd306cSNickeau 240*04fd306cSNickeau return sprintf('[%s]', implode(', ', $output)); 241*04fd306cSNickeau } 242*04fd306cSNickeau 243*04fd306cSNickeau // hash 244*04fd306cSNickeau $output = []; 245*04fd306cSNickeau foreach ($value as $key => $val) { 246*04fd306cSNickeau $output[] = sprintf('%s: %s', self::dump($key, $flags), self::dump($val, $flags)); 247*04fd306cSNickeau } 248*04fd306cSNickeau 249*04fd306cSNickeau return sprintf('{ %s }', implode(', ', $output)); 250*04fd306cSNickeau } 251*04fd306cSNickeau 252*04fd306cSNickeau private static function dumpNull(int $flags): string 253*04fd306cSNickeau { 254*04fd306cSNickeau if (Yaml::DUMP_NULL_AS_TILDE & $flags) { 255*04fd306cSNickeau return '~'; 256*04fd306cSNickeau } 257*04fd306cSNickeau 258*04fd306cSNickeau return 'null'; 259*04fd306cSNickeau } 260*04fd306cSNickeau 261*04fd306cSNickeau /** 262*04fd306cSNickeau * Parses a YAML scalar. 263*04fd306cSNickeau * 264*04fd306cSNickeau * @return mixed 265*04fd306cSNickeau * 266*04fd306cSNickeau * @throws ParseException When malformed inline YAML string is parsed 267*04fd306cSNickeau */ 268*04fd306cSNickeau public static function parseScalar(string $scalar, int $flags = 0, array $delimiters = null, int &$i = 0, bool $evaluate = true, array &$references = [], bool &$isQuoted = null) 269*04fd306cSNickeau { 270*04fd306cSNickeau if (\in_array($scalar[$i], ['"', "'"], true)) { 271*04fd306cSNickeau // quoted scalar 272*04fd306cSNickeau $isQuoted = true; 273*04fd306cSNickeau $output = self::parseQuotedScalar($scalar, $i); 274*04fd306cSNickeau 275*04fd306cSNickeau if (null !== $delimiters) { 276*04fd306cSNickeau $tmp = ltrim(substr($scalar, $i), " \n"); 277*04fd306cSNickeau if ('' === $tmp) { 278*04fd306cSNickeau throw new ParseException(sprintf('Unexpected end of line, expected one of "%s".', implode('', $delimiters)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); 279*04fd306cSNickeau } 280*04fd306cSNickeau if (!\in_array($tmp[0], $delimiters)) { 281*04fd306cSNickeau throw new ParseException(sprintf('Unexpected characters (%s).', substr($scalar, $i)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); 282*04fd306cSNickeau } 283*04fd306cSNickeau } 284*04fd306cSNickeau } else { 285*04fd306cSNickeau // "normal" string 286*04fd306cSNickeau $isQuoted = false; 287*04fd306cSNickeau 288*04fd306cSNickeau if (!$delimiters) { 289*04fd306cSNickeau $output = substr($scalar, $i); 290*04fd306cSNickeau $i += \strlen($output); 291*04fd306cSNickeau 292*04fd306cSNickeau // remove comments 293*04fd306cSNickeau if (Parser::preg_match('/[ \t]+#/', $output, $match, \PREG_OFFSET_CAPTURE)) { 294*04fd306cSNickeau $output = substr($output, 0, $match[0][1]); 295*04fd306cSNickeau } 296*04fd306cSNickeau } elseif (Parser::preg_match('/^(.*?)('.implode('|', $delimiters).')/', substr($scalar, $i), $match)) { 297*04fd306cSNickeau $output = $match[1]; 298*04fd306cSNickeau $i += \strlen($output); 299*04fd306cSNickeau $output = trim($output); 300*04fd306cSNickeau } else { 301*04fd306cSNickeau throw new ParseException(sprintf('Malformed inline YAML string: "%s".', $scalar), self::$parsedLineNumber + 1, null, self::$parsedFilename); 302*04fd306cSNickeau } 303*04fd306cSNickeau 304*04fd306cSNickeau // a non-quoted string cannot start with @ or ` (reserved) nor with a scalar indicator (| or >) 305*04fd306cSNickeau if ($output && ('@' === $output[0] || '`' === $output[0] || '|' === $output[0] || '>' === $output[0] || '%' === $output[0])) { 306*04fd306cSNickeau throw new ParseException(sprintf('The reserved indicator "%s" cannot start a plain scalar; you need to quote the scalar.', $output[0]), self::$parsedLineNumber + 1, $output, self::$parsedFilename); 307*04fd306cSNickeau } 308*04fd306cSNickeau 309*04fd306cSNickeau if ($evaluate) { 310*04fd306cSNickeau $output = self::evaluateScalar($output, $flags, $references, $isQuoted); 311*04fd306cSNickeau } 312*04fd306cSNickeau } 313*04fd306cSNickeau 314*04fd306cSNickeau return $output; 315*04fd306cSNickeau } 316*04fd306cSNickeau 317*04fd306cSNickeau /** 318*04fd306cSNickeau * Parses a YAML quoted scalar. 319*04fd306cSNickeau * 320*04fd306cSNickeau * @throws ParseException When malformed inline YAML string is parsed 321*04fd306cSNickeau */ 322*04fd306cSNickeau private static function parseQuotedScalar(string $scalar, int &$i = 0): string 323*04fd306cSNickeau { 324*04fd306cSNickeau if (!Parser::preg_match('/'.self::REGEX_QUOTED_STRING.'/Au', substr($scalar, $i), $match)) { 325*04fd306cSNickeau throw new ParseException(sprintf('Malformed inline YAML string: "%s".', substr($scalar, $i)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); 326*04fd306cSNickeau } 327*04fd306cSNickeau 328*04fd306cSNickeau $output = substr($match[0], 1, -1); 329*04fd306cSNickeau 330*04fd306cSNickeau $unescaper = new Unescaper(); 331*04fd306cSNickeau if ('"' == $scalar[$i]) { 332*04fd306cSNickeau $output = $unescaper->unescapeDoubleQuotedString($output); 333*04fd306cSNickeau } else { 334*04fd306cSNickeau $output = $unescaper->unescapeSingleQuotedString($output); 335*04fd306cSNickeau } 336*04fd306cSNickeau 337*04fd306cSNickeau $i += \strlen($match[0]); 338*04fd306cSNickeau 339*04fd306cSNickeau return $output; 340*04fd306cSNickeau } 341*04fd306cSNickeau 342*04fd306cSNickeau /** 343*04fd306cSNickeau * Parses a YAML sequence. 344*04fd306cSNickeau * 345*04fd306cSNickeau * @throws ParseException When malformed inline YAML string is parsed 346*04fd306cSNickeau */ 347*04fd306cSNickeau private static function parseSequence(string $sequence, int $flags, int &$i = 0, array &$references = []): array 348*04fd306cSNickeau { 349*04fd306cSNickeau $output = []; 350*04fd306cSNickeau $len = \strlen($sequence); 351*04fd306cSNickeau ++$i; 352*04fd306cSNickeau 353*04fd306cSNickeau // [foo, bar, ...] 354*04fd306cSNickeau while ($i < $len) { 355*04fd306cSNickeau if (']' === $sequence[$i]) { 356*04fd306cSNickeau return $output; 357*04fd306cSNickeau } 358*04fd306cSNickeau if (',' === $sequence[$i] || ' ' === $sequence[$i]) { 359*04fd306cSNickeau ++$i; 360*04fd306cSNickeau 361*04fd306cSNickeau continue; 362*04fd306cSNickeau } 363*04fd306cSNickeau 364*04fd306cSNickeau $tag = self::parseTag($sequence, $i, $flags); 365*04fd306cSNickeau switch ($sequence[$i]) { 366*04fd306cSNickeau case '[': 367*04fd306cSNickeau // nested sequence 368*04fd306cSNickeau $value = self::parseSequence($sequence, $flags, $i, $references); 369*04fd306cSNickeau break; 370*04fd306cSNickeau case '{': 371*04fd306cSNickeau // nested mapping 372*04fd306cSNickeau $value = self::parseMapping($sequence, $flags, $i, $references); 373*04fd306cSNickeau break; 374*04fd306cSNickeau default: 375*04fd306cSNickeau $value = self::parseScalar($sequence, $flags, [',', ']'], $i, null === $tag, $references, $isQuoted); 376*04fd306cSNickeau 377*04fd306cSNickeau // the value can be an array if a reference has been resolved to an array var 378*04fd306cSNickeau if (\is_string($value) && !$isQuoted && false !== strpos($value, ': ')) { 379*04fd306cSNickeau // embedded mapping? 380*04fd306cSNickeau try { 381*04fd306cSNickeau $pos = 0; 382*04fd306cSNickeau $value = self::parseMapping('{'.$value.'}', $flags, $pos, $references); 383*04fd306cSNickeau } catch (\InvalidArgumentException $e) { 384*04fd306cSNickeau // no, it's not 385*04fd306cSNickeau } 386*04fd306cSNickeau } 387*04fd306cSNickeau 388*04fd306cSNickeau if (!$isQuoted && \is_string($value) && '' !== $value && '&' === $value[0] && Parser::preg_match(Parser::REFERENCE_PATTERN, $value, $matches)) { 389*04fd306cSNickeau $references[$matches['ref']] = $matches['value']; 390*04fd306cSNickeau $value = $matches['value']; 391*04fd306cSNickeau } 392*04fd306cSNickeau 393*04fd306cSNickeau --$i; 394*04fd306cSNickeau } 395*04fd306cSNickeau 396*04fd306cSNickeau if (null !== $tag && '' !== $tag) { 397*04fd306cSNickeau $value = new TaggedValue($tag, $value); 398*04fd306cSNickeau } 399*04fd306cSNickeau 400*04fd306cSNickeau $output[] = $value; 401*04fd306cSNickeau 402*04fd306cSNickeau ++$i; 403*04fd306cSNickeau } 404*04fd306cSNickeau 405*04fd306cSNickeau throw new ParseException(sprintf('Malformed inline YAML string: "%s".', $sequence), self::$parsedLineNumber + 1, null, self::$parsedFilename); 406*04fd306cSNickeau } 407*04fd306cSNickeau 408*04fd306cSNickeau /** 409*04fd306cSNickeau * Parses a YAML mapping. 410*04fd306cSNickeau * 411*04fd306cSNickeau * @return array|\stdClass 412*04fd306cSNickeau * 413*04fd306cSNickeau * @throws ParseException When malformed inline YAML string is parsed 414*04fd306cSNickeau */ 415*04fd306cSNickeau private static function parseMapping(string $mapping, int $flags, int &$i = 0, array &$references = []) 416*04fd306cSNickeau { 417*04fd306cSNickeau $output = []; 418*04fd306cSNickeau $len = \strlen($mapping); 419*04fd306cSNickeau ++$i; 420*04fd306cSNickeau $allowOverwrite = false; 421*04fd306cSNickeau 422*04fd306cSNickeau // {foo: bar, bar:foo, ...} 423*04fd306cSNickeau while ($i < $len) { 424*04fd306cSNickeau switch ($mapping[$i]) { 425*04fd306cSNickeau case ' ': 426*04fd306cSNickeau case ',': 427*04fd306cSNickeau case "\n": 428*04fd306cSNickeau ++$i; 429*04fd306cSNickeau continue 2; 430*04fd306cSNickeau case '}': 431*04fd306cSNickeau if (self::$objectForMap) { 432*04fd306cSNickeau return (object) $output; 433*04fd306cSNickeau } 434*04fd306cSNickeau 435*04fd306cSNickeau return $output; 436*04fd306cSNickeau } 437*04fd306cSNickeau 438*04fd306cSNickeau // key 439*04fd306cSNickeau $offsetBeforeKeyParsing = $i; 440*04fd306cSNickeau $isKeyQuoted = \in_array($mapping[$i], ['"', "'"], true); 441*04fd306cSNickeau $key = self::parseScalar($mapping, $flags, [':', ' '], $i, false); 442*04fd306cSNickeau 443*04fd306cSNickeau if ($offsetBeforeKeyParsing === $i) { 444*04fd306cSNickeau throw new ParseException('Missing mapping key.', self::$parsedLineNumber + 1, $mapping); 445*04fd306cSNickeau } 446*04fd306cSNickeau 447*04fd306cSNickeau if ('!php/const' === $key) { 448*04fd306cSNickeau $key .= ' '.self::parseScalar($mapping, $flags, [':'], $i, false); 449*04fd306cSNickeau $key = self::evaluateScalar($key, $flags); 450*04fd306cSNickeau } 451*04fd306cSNickeau 452*04fd306cSNickeau if (false === $i = strpos($mapping, ':', $i)) { 453*04fd306cSNickeau break; 454*04fd306cSNickeau } 455*04fd306cSNickeau 456*04fd306cSNickeau if (!$isKeyQuoted) { 457*04fd306cSNickeau $evaluatedKey = self::evaluateScalar($key, $flags, $references); 458*04fd306cSNickeau 459*04fd306cSNickeau if ('' !== $key && $evaluatedKey !== $key && !\is_string($evaluatedKey) && !\is_int($evaluatedKey)) { 460*04fd306cSNickeau throw new ParseException('Implicit casting of incompatible mapping keys to strings is not supported. Quote your evaluable mapping keys instead.', self::$parsedLineNumber + 1, $mapping); 461*04fd306cSNickeau } 462*04fd306cSNickeau } 463*04fd306cSNickeau 464*04fd306cSNickeau if (!$isKeyQuoted && (!isset($mapping[$i + 1]) || !\in_array($mapping[$i + 1], [' ', ',', '[', ']', '{', '}', "\n"], true))) { 465*04fd306cSNickeau throw new ParseException('Colons must be followed by a space or an indication character (i.e. " ", ",", "[", "]", "{", "}").', self::$parsedLineNumber + 1, $mapping); 466*04fd306cSNickeau } 467*04fd306cSNickeau 468*04fd306cSNickeau if ('<<' === $key) { 469*04fd306cSNickeau $allowOverwrite = true; 470*04fd306cSNickeau } 471*04fd306cSNickeau 472*04fd306cSNickeau while ($i < $len) { 473*04fd306cSNickeau if (':' === $mapping[$i] || ' ' === $mapping[$i] || "\n" === $mapping[$i]) { 474*04fd306cSNickeau ++$i; 475*04fd306cSNickeau 476*04fd306cSNickeau continue; 477*04fd306cSNickeau } 478*04fd306cSNickeau 479*04fd306cSNickeau $tag = self::parseTag($mapping, $i, $flags); 480*04fd306cSNickeau switch ($mapping[$i]) { 481*04fd306cSNickeau case '[': 482*04fd306cSNickeau // nested sequence 483*04fd306cSNickeau $value = self::parseSequence($mapping, $flags, $i, $references); 484*04fd306cSNickeau // Spec: Keys MUST be unique; first one wins. 485*04fd306cSNickeau // Parser cannot abort this mapping earlier, since lines 486*04fd306cSNickeau // are processed sequentially. 487*04fd306cSNickeau // But overwriting is allowed when a merge node is used in current block. 488*04fd306cSNickeau if ('<<' === $key) { 489*04fd306cSNickeau foreach ($value as $parsedValue) { 490*04fd306cSNickeau $output += $parsedValue; 491*04fd306cSNickeau } 492*04fd306cSNickeau } elseif ($allowOverwrite || !isset($output[$key])) { 493*04fd306cSNickeau if (null !== $tag) { 494*04fd306cSNickeau $output[$key] = new TaggedValue($tag, $value); 495*04fd306cSNickeau } else { 496*04fd306cSNickeau $output[$key] = $value; 497*04fd306cSNickeau } 498*04fd306cSNickeau } elseif (isset($output[$key])) { 499*04fd306cSNickeau throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), self::$parsedLineNumber + 1, $mapping); 500*04fd306cSNickeau } 501*04fd306cSNickeau break; 502*04fd306cSNickeau case '{': 503*04fd306cSNickeau // nested mapping 504*04fd306cSNickeau $value = self::parseMapping($mapping, $flags, $i, $references); 505*04fd306cSNickeau // Spec: Keys MUST be unique; first one wins. 506*04fd306cSNickeau // Parser cannot abort this mapping earlier, since lines 507*04fd306cSNickeau // are processed sequentially. 508*04fd306cSNickeau // But overwriting is allowed when a merge node is used in current block. 509*04fd306cSNickeau if ('<<' === $key) { 510*04fd306cSNickeau $output += $value; 511*04fd306cSNickeau } elseif ($allowOverwrite || !isset($output[$key])) { 512*04fd306cSNickeau if (null !== $tag) { 513*04fd306cSNickeau $output[$key] = new TaggedValue($tag, $value); 514*04fd306cSNickeau } else { 515*04fd306cSNickeau $output[$key] = $value; 516*04fd306cSNickeau } 517*04fd306cSNickeau } elseif (isset($output[$key])) { 518*04fd306cSNickeau throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), self::$parsedLineNumber + 1, $mapping); 519*04fd306cSNickeau } 520*04fd306cSNickeau break; 521*04fd306cSNickeau default: 522*04fd306cSNickeau $value = self::parseScalar($mapping, $flags, [',', '}', "\n"], $i, null === $tag, $references, $isValueQuoted); 523*04fd306cSNickeau // Spec: Keys MUST be unique; first one wins. 524*04fd306cSNickeau // Parser cannot abort this mapping earlier, since lines 525*04fd306cSNickeau // are processed sequentially. 526*04fd306cSNickeau // But overwriting is allowed when a merge node is used in current block. 527*04fd306cSNickeau if ('<<' === $key) { 528*04fd306cSNickeau $output += $value; 529*04fd306cSNickeau } elseif ($allowOverwrite || !isset($output[$key])) { 530*04fd306cSNickeau if (!$isValueQuoted && \is_string($value) && '' !== $value && '&' === $value[0] && Parser::preg_match(Parser::REFERENCE_PATTERN, $value, $matches)) { 531*04fd306cSNickeau $references[$matches['ref']] = $matches['value']; 532*04fd306cSNickeau $value = $matches['value']; 533*04fd306cSNickeau } 534*04fd306cSNickeau 535*04fd306cSNickeau if (null !== $tag) { 536*04fd306cSNickeau $output[$key] = new TaggedValue($tag, $value); 537*04fd306cSNickeau } else { 538*04fd306cSNickeau $output[$key] = $value; 539*04fd306cSNickeau } 540*04fd306cSNickeau } elseif (isset($output[$key])) { 541*04fd306cSNickeau throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), self::$parsedLineNumber + 1, $mapping); 542*04fd306cSNickeau } 543*04fd306cSNickeau --$i; 544*04fd306cSNickeau } 545*04fd306cSNickeau ++$i; 546*04fd306cSNickeau 547*04fd306cSNickeau continue 2; 548*04fd306cSNickeau } 549*04fd306cSNickeau } 550*04fd306cSNickeau 551*04fd306cSNickeau throw new ParseException(sprintf('Malformed inline YAML string: "%s".', $mapping), self::$parsedLineNumber + 1, null, self::$parsedFilename); 552*04fd306cSNickeau } 553*04fd306cSNickeau 554*04fd306cSNickeau /** 555*04fd306cSNickeau * Evaluates scalars and replaces magic values. 556*04fd306cSNickeau * 557*04fd306cSNickeau * @return mixed 558*04fd306cSNickeau * 559*04fd306cSNickeau * @throws ParseException when object parsing support was disabled and the parser detected a PHP object or when a reference could not be resolved 560*04fd306cSNickeau */ 561*04fd306cSNickeau private static function evaluateScalar(string $scalar, int $flags, array &$references = [], bool &$isQuotedString = null) 562*04fd306cSNickeau { 563*04fd306cSNickeau $isQuotedString = false; 564*04fd306cSNickeau $scalar = trim($scalar); 565*04fd306cSNickeau 566*04fd306cSNickeau if (0 === strpos($scalar, '*')) { 567*04fd306cSNickeau if (false !== $pos = strpos($scalar, '#')) { 568*04fd306cSNickeau $value = substr($scalar, 1, $pos - 2); 569*04fd306cSNickeau } else { 570*04fd306cSNickeau $value = substr($scalar, 1); 571*04fd306cSNickeau } 572*04fd306cSNickeau 573*04fd306cSNickeau // an unquoted * 574*04fd306cSNickeau if (false === $value || '' === $value) { 575*04fd306cSNickeau throw new ParseException('A reference must contain at least one character.', self::$parsedLineNumber + 1, $value, self::$parsedFilename); 576*04fd306cSNickeau } 577*04fd306cSNickeau 578*04fd306cSNickeau if (!\array_key_exists($value, $references)) { 579*04fd306cSNickeau throw new ParseException(sprintf('Reference "%s" does not exist.', $value), self::$parsedLineNumber + 1, $value, self::$parsedFilename); 580*04fd306cSNickeau } 581*04fd306cSNickeau 582*04fd306cSNickeau return $references[$value]; 583*04fd306cSNickeau } 584*04fd306cSNickeau 585*04fd306cSNickeau $scalarLower = strtolower($scalar); 586*04fd306cSNickeau 587*04fd306cSNickeau switch (true) { 588*04fd306cSNickeau case 'null' === $scalarLower: 589*04fd306cSNickeau case '' === $scalar: 590*04fd306cSNickeau case '~' === $scalar: 591*04fd306cSNickeau return null; 592*04fd306cSNickeau case 'true' === $scalarLower: 593*04fd306cSNickeau return true; 594*04fd306cSNickeau case 'false' === $scalarLower: 595*04fd306cSNickeau return false; 596*04fd306cSNickeau case '!' === $scalar[0]: 597*04fd306cSNickeau switch (true) { 598*04fd306cSNickeau case 0 === strpos($scalar, '!!str '): 599*04fd306cSNickeau $s = (string) substr($scalar, 6); 600*04fd306cSNickeau 601*04fd306cSNickeau if (\in_array($s[0] ?? '', ['"', "'"], true)) { 602*04fd306cSNickeau $isQuotedString = true; 603*04fd306cSNickeau $s = self::parseQuotedScalar($s); 604*04fd306cSNickeau } 605*04fd306cSNickeau 606*04fd306cSNickeau return $s; 607*04fd306cSNickeau case 0 === strpos($scalar, '! '): 608*04fd306cSNickeau return substr($scalar, 2); 609*04fd306cSNickeau case 0 === strpos($scalar, '!php/object'): 610*04fd306cSNickeau if (self::$objectSupport) { 611*04fd306cSNickeau if (!isset($scalar[12])) { 612*04fd306cSNickeau trigger_deprecation('symfony/yaml', '5.1', 'Using the !php/object tag without a value is deprecated.'); 613*04fd306cSNickeau 614*04fd306cSNickeau return false; 615*04fd306cSNickeau } 616*04fd306cSNickeau 617*04fd306cSNickeau return unserialize(self::parseScalar(substr($scalar, 12))); 618*04fd306cSNickeau } 619*04fd306cSNickeau 620*04fd306cSNickeau if (self::$exceptionOnInvalidType) { 621*04fd306cSNickeau throw new ParseException('Object support when parsing a YAML file has been disabled.', self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); 622*04fd306cSNickeau } 623*04fd306cSNickeau 624*04fd306cSNickeau return null; 625*04fd306cSNickeau case 0 === strpos($scalar, '!php/const'): 626*04fd306cSNickeau if (self::$constantSupport) { 627*04fd306cSNickeau if (!isset($scalar[11])) { 628*04fd306cSNickeau trigger_deprecation('symfony/yaml', '5.1', 'Using the !php/const tag without a value is deprecated.'); 629*04fd306cSNickeau 630*04fd306cSNickeau return ''; 631*04fd306cSNickeau } 632*04fd306cSNickeau 633*04fd306cSNickeau $i = 0; 634*04fd306cSNickeau if (\defined($const = self::parseScalar(substr($scalar, 11), 0, null, $i, false))) { 635*04fd306cSNickeau return \constant($const); 636*04fd306cSNickeau } 637*04fd306cSNickeau 638*04fd306cSNickeau throw new ParseException(sprintf('The constant "%s" is not defined.', $const), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); 639*04fd306cSNickeau } 640*04fd306cSNickeau if (self::$exceptionOnInvalidType) { 641*04fd306cSNickeau throw new ParseException(sprintf('The string "%s" could not be parsed as a constant. Did you forget to pass the "Yaml::PARSE_CONSTANT" flag to the parser?', $scalar), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); 642*04fd306cSNickeau } 643*04fd306cSNickeau 644*04fd306cSNickeau return null; 645*04fd306cSNickeau case 0 === strpos($scalar, '!!float '): 646*04fd306cSNickeau return (float) substr($scalar, 8); 647*04fd306cSNickeau case 0 === strpos($scalar, '!!binary '): 648*04fd306cSNickeau return self::evaluateBinaryScalar(substr($scalar, 9)); 649*04fd306cSNickeau } 650*04fd306cSNickeau 651*04fd306cSNickeau throw new ParseException(sprintf('The string "%s" could not be parsed as it uses an unsupported built-in tag.', $scalar), self::$parsedLineNumber, $scalar, self::$parsedFilename); 652*04fd306cSNickeau case preg_match('/^(?:\+|-)?0o(?P<value>[0-7_]++)$/', $scalar, $matches): 653*04fd306cSNickeau $value = str_replace('_', '', $matches['value']); 654*04fd306cSNickeau 655*04fd306cSNickeau if ('-' === $scalar[0]) { 656*04fd306cSNickeau return -octdec($value); 657*04fd306cSNickeau } 658*04fd306cSNickeau 659*04fd306cSNickeau return octdec($value); 660*04fd306cSNickeau case \in_array($scalar[0], ['+', '-', '.'], true) || is_numeric($scalar[0]): 661*04fd306cSNickeau if (Parser::preg_match('{^[+-]?[0-9][0-9_]*$}', $scalar)) { 662*04fd306cSNickeau $scalar = str_replace('_', '', $scalar); 663*04fd306cSNickeau } 664*04fd306cSNickeau 665*04fd306cSNickeau switch (true) { 666*04fd306cSNickeau case ctype_digit($scalar): 667*04fd306cSNickeau if (preg_match('/^0[0-7]+$/', $scalar)) { 668*04fd306cSNickeau trigger_deprecation('symfony/yaml', '5.1', 'Support for parsing numbers prefixed with 0 as octal numbers. They will be parsed as strings as of 6.0. Use "%s" to represent the octal number.', '0o'.substr($scalar, 1)); 669*04fd306cSNickeau 670*04fd306cSNickeau return octdec($scalar); 671*04fd306cSNickeau } 672*04fd306cSNickeau 673*04fd306cSNickeau $cast = (int) $scalar; 674*04fd306cSNickeau 675*04fd306cSNickeau return ($scalar === (string) $cast) ? $cast : $scalar; 676*04fd306cSNickeau case '-' === $scalar[0] && ctype_digit(substr($scalar, 1)): 677*04fd306cSNickeau if (preg_match('/^-0[0-7]+$/', $scalar)) { 678*04fd306cSNickeau trigger_deprecation('symfony/yaml', '5.1', 'Support for parsing numbers prefixed with 0 as octal numbers. They will be parsed as strings as of 6.0. Use "%s" to represent the octal number.', '-0o'.substr($scalar, 2)); 679*04fd306cSNickeau 680*04fd306cSNickeau return -octdec(substr($scalar, 1)); 681*04fd306cSNickeau } 682*04fd306cSNickeau 683*04fd306cSNickeau $cast = (int) $scalar; 684*04fd306cSNickeau 685*04fd306cSNickeau return ($scalar === (string) $cast) ? $cast : $scalar; 686*04fd306cSNickeau case is_numeric($scalar): 687*04fd306cSNickeau case Parser::preg_match(self::getHexRegex(), $scalar): 688*04fd306cSNickeau $scalar = str_replace('_', '', $scalar); 689*04fd306cSNickeau 690*04fd306cSNickeau return '0x' === $scalar[0].$scalar[1] ? hexdec($scalar) : (float) $scalar; 691*04fd306cSNickeau case '.inf' === $scalarLower: 692*04fd306cSNickeau case '.nan' === $scalarLower: 693*04fd306cSNickeau return -log(0); 694*04fd306cSNickeau case '-.inf' === $scalarLower: 695*04fd306cSNickeau return log(0); 696*04fd306cSNickeau case Parser::preg_match('/^(-|\+)?[0-9][0-9_]*(\.[0-9_]+)?$/', $scalar): 697*04fd306cSNickeau return (float) str_replace('_', '', $scalar); 698*04fd306cSNickeau case Parser::preg_match(self::getTimestampRegex(), $scalar): 699*04fd306cSNickeau // When no timezone is provided in the parsed date, YAML spec says we must assume UTC. 700*04fd306cSNickeau $time = new \DateTime($scalar, new \DateTimeZone('UTC')); 701*04fd306cSNickeau 702*04fd306cSNickeau if (Yaml::PARSE_DATETIME & $flags) { 703*04fd306cSNickeau return $time; 704*04fd306cSNickeau } 705*04fd306cSNickeau 706*04fd306cSNickeau try { 707*04fd306cSNickeau if (false !== $scalar = $time->getTimestamp()) { 708*04fd306cSNickeau return $scalar; 709*04fd306cSNickeau } 710*04fd306cSNickeau } catch (\ValueError $e) { 711*04fd306cSNickeau // no-op 712*04fd306cSNickeau } 713*04fd306cSNickeau 714*04fd306cSNickeau return $time->format('U'); 715*04fd306cSNickeau } 716*04fd306cSNickeau } 717*04fd306cSNickeau 718*04fd306cSNickeau return (string) $scalar; 719*04fd306cSNickeau } 720*04fd306cSNickeau 721*04fd306cSNickeau private static function parseTag(string $value, int &$i, int $flags): ?string 722*04fd306cSNickeau { 723*04fd306cSNickeau if ('!' !== $value[$i]) { 724*04fd306cSNickeau return null; 725*04fd306cSNickeau } 726*04fd306cSNickeau 727*04fd306cSNickeau $tagLength = strcspn($value, " \t\n[]{},", $i + 1); 728*04fd306cSNickeau $tag = substr($value, $i + 1, $tagLength); 729*04fd306cSNickeau 730*04fd306cSNickeau $nextOffset = $i + $tagLength + 1; 731*04fd306cSNickeau $nextOffset += strspn($value, ' ', $nextOffset); 732*04fd306cSNickeau 733*04fd306cSNickeau if ('' === $tag && (!isset($value[$nextOffset]) || \in_array($value[$nextOffset], [']', '}', ','], true))) { 734*04fd306cSNickeau throw new ParseException('Using the unquoted scalar value "!" is not supported. You must quote it.', self::$parsedLineNumber + 1, $value, self::$parsedFilename); 735*04fd306cSNickeau } 736*04fd306cSNickeau 737*04fd306cSNickeau // Is followed by a scalar and is a built-in tag 738*04fd306cSNickeau if ('' !== $tag && (!isset($value[$nextOffset]) || !\in_array($value[$nextOffset], ['[', '{'], true)) && ('!' === $tag[0] || 'str' === $tag || 'php/const' === $tag || 'php/object' === $tag)) { 739*04fd306cSNickeau // Manage in {@link self::evaluateScalar()} 740*04fd306cSNickeau return null; 741*04fd306cSNickeau } 742*04fd306cSNickeau 743*04fd306cSNickeau $i = $nextOffset; 744*04fd306cSNickeau 745*04fd306cSNickeau // Built-in tags 746*04fd306cSNickeau if ('' !== $tag && '!' === $tag[0]) { 747*04fd306cSNickeau throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag), self::$parsedLineNumber + 1, $value, self::$parsedFilename); 748*04fd306cSNickeau } 749*04fd306cSNickeau 750*04fd306cSNickeau if ('' !== $tag && !isset($value[$i])) { 751*04fd306cSNickeau throw new ParseException(sprintf('Missing value for tag "%s".', $tag), self::$parsedLineNumber + 1, $value, self::$parsedFilename); 752*04fd306cSNickeau } 753*04fd306cSNickeau 754*04fd306cSNickeau if ('' === $tag || Yaml::PARSE_CUSTOM_TAGS & $flags) { 755*04fd306cSNickeau return $tag; 756*04fd306cSNickeau } 757*04fd306cSNickeau 758*04fd306cSNickeau throw new ParseException(sprintf('Tags support is not enabled. Enable the "Yaml::PARSE_CUSTOM_TAGS" flag to use "!%s".', $tag), self::$parsedLineNumber + 1, $value, self::$parsedFilename); 759*04fd306cSNickeau } 760*04fd306cSNickeau 761*04fd306cSNickeau public static function evaluateBinaryScalar(string $scalar): string 762*04fd306cSNickeau { 763*04fd306cSNickeau $parsedBinaryData = self::parseScalar(preg_replace('/\s/', '', $scalar)); 764*04fd306cSNickeau 765*04fd306cSNickeau if (0 !== (\strlen($parsedBinaryData) % 4)) { 766*04fd306cSNickeau throw new ParseException(sprintf('The normalized base64 encoded data (data without whitespace characters) length must be a multiple of four (%d bytes given).', \strlen($parsedBinaryData)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); 767*04fd306cSNickeau } 768*04fd306cSNickeau 769*04fd306cSNickeau if (!Parser::preg_match('#^[A-Z0-9+/]+={0,2}$#i', $parsedBinaryData)) { 770*04fd306cSNickeau throw new ParseException(sprintf('The base64 encoded data (%s) contains invalid characters.', $parsedBinaryData), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); 771*04fd306cSNickeau } 772*04fd306cSNickeau 773*04fd306cSNickeau return base64_decode($parsedBinaryData, true); 774*04fd306cSNickeau } 775*04fd306cSNickeau 776*04fd306cSNickeau private static function isBinaryString(string $value): bool 777*04fd306cSNickeau { 778*04fd306cSNickeau return !preg_match('//u', $value) || preg_match('/[^\x00\x07-\x0d\x1B\x20-\xff]/', $value); 779*04fd306cSNickeau } 780*04fd306cSNickeau 781*04fd306cSNickeau /** 782*04fd306cSNickeau * Gets a regex that matches a YAML date. 783*04fd306cSNickeau * 784*04fd306cSNickeau * @see http://www.yaml.org/spec/1.2/spec.html#id2761573 785*04fd306cSNickeau */ 786*04fd306cSNickeau private static function getTimestampRegex(): string 787*04fd306cSNickeau { 788*04fd306cSNickeau return <<<EOF 789*04fd306cSNickeau ~^ 790*04fd306cSNickeau (?P<year>[0-9][0-9][0-9][0-9]) 791*04fd306cSNickeau -(?P<month>[0-9][0-9]?) 792*04fd306cSNickeau -(?P<day>[0-9][0-9]?) 793*04fd306cSNickeau (?:(?:[Tt]|[ \t]+) 794*04fd306cSNickeau (?P<hour>[0-9][0-9]?) 795*04fd306cSNickeau :(?P<minute>[0-9][0-9]) 796*04fd306cSNickeau :(?P<second>[0-9][0-9]) 797*04fd306cSNickeau (?:\.(?P<fraction>[0-9]*))? 798*04fd306cSNickeau (?:[ \t]*(?P<tz>Z|(?P<tz_sign>[-+])(?P<tz_hour>[0-9][0-9]?) 799*04fd306cSNickeau (?::(?P<tz_minute>[0-9][0-9]))?))?)? 800*04fd306cSNickeau $~x 801*04fd306cSNickeauEOF; 802*04fd306cSNickeau } 803*04fd306cSNickeau 804*04fd306cSNickeau /** 805*04fd306cSNickeau * Gets a regex that matches a YAML number in hexadecimal notation. 806*04fd306cSNickeau */ 807*04fd306cSNickeau private static function getHexRegex(): string 808*04fd306cSNickeau { 809*04fd306cSNickeau return '~^0x[0-9a-f_]++$~i'; 810*04fd306cSNickeau } 811*04fd306cSNickeau} 812