diff --git a/src/Symfony/Component/Yaml/Inline.php b/src/Symfony/Component/Yaml/Inline.php index 8ad3b1e1b2fd5..1195bbfa6a40d 100644 --- a/src/Symfony/Component/Yaml/Inline.php +++ b/src/Symfony/Component/Yaml/Inline.php @@ -11,8 +11,9 @@ namespace Symfony\Component\Yaml; -use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Exception\DumpException; +use Symfony\Component\Yaml\Exception\ParseException; +use Symfony\Component\Yaml\Util\StringReader; /** * Inline implements a YAML parser/dumper for the YAML inline syntax. @@ -35,15 +36,15 @@ class Inline /** * Converts a YAML string to a PHP value. * - * @param string $value A YAML string - * @param int $flags A bit field of PARSE_* constants to customize the YAML parser behavior - * @param array $references Mapping of variable names to values + * @param string|StringReader $reader A YAML string + * @param int $flags A bit field of PARSE_* constants to customize the YAML parser behavior + * @param array $references Mapping of variable names to values * - * @return mixed A PHP value + * @return mixed A PHP value representing the YAML string * * @throws ParseException */ - public static function parse($value, $flags = 0, $references = array()) + public static function parse($reader, $flags = 0, $references = array()) { if (is_bool($flags)) { @trigger_error('Passing a boolean flag to toggle exception handling is deprecated since version 3.1 and will be removed in 4.0. Use the Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE flag instead.', E_USER_DEPRECATED); @@ -82,34 +83,29 @@ public static function parse($value, $flags = 0, $references = array()) self::$objectForMap = (bool) (Yaml::PARSE_OBJECT_FOR_MAP & $flags); self::$constantSupport = (bool) (Yaml::PARSE_CONSTANT & $flags); - $value = trim($value); - - if ('' === $value) { - return ''; - } - if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) { $mbEncoding = mb_internal_encoding(); mb_internal_encoding('ASCII'); } - $i = 0; - switch ($value[0]) { - case '[': - $result = self::parseSequence($value, $flags, $i, $references); - ++$i; - break; - case '{': - $result = self::parseMapping($value, $flags, $i, $references); - ++$i; - break; - default: - $result = self::parseScalar($value, $flags, null, array('"', "'"), $i, true, $references); + if (!$reader instanceof StringReader) { + $reader = new StringReader($reader); } + $reader->consumeWhiteSpace(); + + if ($reader->isFullyConsumed()) { + return ''; + } + + $result = self::parseValue($reader, $flags, null, true, $references); // some comments are allowed at the end - if (preg_replace('/\s+#.*$/A', '', substr($value, $i))) { - throw new ParseException(sprintf('Unexpected characters near "%s".', substr($value, $i))); + if (0 !== $reader->consumeWhiteSpace() && $reader->readChar('#')) { + $reader->readCSpan("\n"); + } + + if (!$reader->isFullyConsumed()) { + throw new ParseException(sprintf('Unexpected characters near "%s".', $reader->readToEnd())); } if (isset($mbEncoding)) { @@ -125,7 +121,7 @@ public static function parse($value, $flags = 0, $references = array()) * @param mixed $value The PHP variable to convert * @param int $flags A bit field of Yaml::DUMP_* constants to customize the dumped YAML string * - * @return string The YAML string representing the PHP value + * @return string The YAML string representing the PHP array * * @throws DumpException When trying to dump PHP resource */ @@ -270,260 +266,264 @@ private static function dumpArray($value, $flags) return sprintf('{ %s }', implode(', ', $output)); } - /** - * Parses a YAML scalar. - * - * @param string $scalar - * @param int $flags - * @param string $delimiters - * @param array $stringDelimiters - * @param int &$i - * @param bool $evaluate - * @param array $references - * - * @return string - * - * @throws ParseException When malformed inline YAML string is parsed - * - * @internal - */ - public static function parseScalar($scalar, $flags = 0, $delimiters = null, $stringDelimiters = array('"', "'"), &$i = 0, $evaluate = true, $references = array()) + private static function parseValue(StringReader $reader, &$flags = 0, $delimiters = null, $evaluate = true, &$references = array()) { - if (in_array($scalar[$i], $stringDelimiters)) { - // quoted scalar - $output = self::parseQuotedScalar($scalar, $i); - - if (null !== $delimiters) { - $tmp = ltrim(substr($scalar, $i), ' '); - if (!in_array($tmp[0], $delimiters)) { - throw new ParseException(sprintf('Unexpected characters (%s).', substr($scalar, $i))); + $reader->consumeWhiteSpace(); + + // Reference + if ($reader->readChar('*')) { + $value = $reader->readCSpan(' #'.$delimiters); + // an unquoted * + if ('' === $value) { + throw new ParseException('A reference must contain at least one character.'); + } + if (!array_key_exists($value, $references)) { + throw new ParseException(sprintf('Reference "%s" does not exist.', $value)); + } + + return $references[$value]; + } + + // Tagged value + if ('!' === $reader->peek()) { + if ($reader->readString('!str ')) { + $reader->consumeWhiteSpace(); + $value = self::parseValueInner($reader, $flags, $delimiters, false, $references); + if (!is_scalar($value)) { + throw new ParseException('Value of type "%s" can\'t be casted to a string.', gettype($value)); } + + return (string) $value; } - } else { - // "normal" string - if (!$delimiters) { - $output = substr($scalar, $i); - $i += strlen($output); - - // remove comments - if (preg_match('/[ \t]+#/', $output, $match, PREG_OFFSET_CAPTURE)) { - $output = substr($output, 0, $match[0][1]); + // Non-specific tag + if ($reader->readString('! ')) { + $value = self::parseValueInner($reader, $flags, $delimiters, $evaluate, $references); + if (is_scalar($value)) { + // @todo deprecate the int conversion as it + // should be a string + // @see http://www.yaml.org/spec/1.2/spec.html#tag/non-specific/ + return (int) $value; } - } elseif (preg_match('/^(.+?)('.implode('|', $delimiters).')/', substr($scalar, $i), $match)) { - $output = $match[1]; - $i += strlen($output); - } else { - throw new ParseException(sprintf('Malformed inline YAML string: %s.', $scalar)); + + return $value; } + if ($tag = $reader->readAny(array('!php/object:', '!!php/object:'))) { + $serializedObject = self::parseScalar($reader, $flags, $delimiters, $evaluate, $references); + if (self::$objectSupport) { + if ('!!php/object:' === $tag) { + @trigger_error('The !!php/object tag to indicate dumped PHP objects is deprecated since version 3.1 and will be removed in 4.0. Use the !php/object tag instead.', E_USER_DEPRECATED); + } + + return unserialize($serializedObject); + } + + if (self::$exceptionOnInvalidType) { + throw new ParseException('Object support when parsing a YAML file has been disabled.'); + } - // a non-quoted string cannot start with @ or ` (reserved) nor with a scalar indicator (| or >) - if ($output && ('@' === $output[0] || '`' === $output[0] || '|' === $output[0] || '>' === $output[0])) { - throw new ParseException(sprintf('The reserved indicator "%s" cannot start a plain scalar; you need to quote the scalar.', $output[0])); + return; } + if ($reader->readString('!php/const:')) { + $constant = self::parseScalar($reader, $flags, $delimiters, $evaluate, $references); + if (self::$constantSupport) { + if (defined($constant)) { + return constant($constant); + } - if ($output && '%' === $output[0]) { - @trigger_error(sprintf('Not quoting the scalar "%s" starting with the "%%" indicator character is deprecated since Symfony 3.1 and will throw a ParseException in 4.0.', $output), E_USER_DEPRECATED); + throw new ParseException(sprintf('The constant "%s" is not defined.', $constant)); + } + if (self::$exceptionOnInvalidType) { + throw new ParseException(sprintf('The string "!php/const:%s" could not be parsed as a constant. Have you forgotten to pass the "Yaml::PARSE_CONSTANT" flag to the parser?', $constant)); + } + + return; } + if ($reader->readString('!!float ')) { + $reader->consumeWhiteSpace(); - if ($evaluate) { - $output = self::evaluateScalar($output, $flags, $references); + return (float) self::parseScalar($reader, $flags, $delimiters, $evaluate, $references); } + if ($reader->readString('!!binary ')) { + $reader->consumeWhiteSpace(); + + return self::evaluateBinaryScalar(self::parseScalar($reader, $flags, $delimiters, $evaluate, $references)); + } + + // @todo deprecate using non-supported tags } - return $output; + return self::parseValueInner($reader, $flags, $delimiters, $evaluate, $references); + } + + private static function parseValueInner(StringReader $reader, &$flags = 0, &$delimiters = null, $evaluate = true, &$references = array()) + { + $reader->consumeWhiteSpace(); + + if ($reader->readChar('[')) { + return self::parseSequence($reader, $flags, $references); + } + if ($reader->readChar('{')) { + return self::parseMapping($reader, $flags, $references); + } + + return self::parseScalar($reader, $flags, $delimiters, $evaluate, $references); } /** - * Parses a YAML quoted scalar. + * Parses a YAML scalar. * - * @param string $scalar - * @param int &$i + * @param StringReader $reader + * @param int &$flags + * @param string &$delimiters + * @param bool $evaluate + * @param array &$references * * @return string * * @throws ParseException When malformed inline YAML string is parsed + * + * @internal */ - private static function parseQuotedScalar($scalar, &$i) + public static function parseScalar(StringReader $reader, &$flags = 0, &$delimiters = null, $evaluate = true, array &$references = array()) { - if (!preg_match('/'.self::REGEX_QUOTED_STRING.'/Au', substr($scalar, $i), $match)) { - throw new ParseException(sprintf('Malformed inline YAML string: %s.', substr($scalar, $i))); + $unescaper = new Unescaper(); + if ($reader->readChar('"')) { + return $unescaper->unescapeDoubleQuotedString($reader); + } + if ($reader->readChar('\'')) { + return $unescaper->unescapeSingleQuotedString($reader); } - $output = substr($match[0], 1, strlen($match[0]) - 2); + // "normal" string + if (null === $delimiters) { + // remove comments + $scalar = $reader->readCSpan("\n"); + if (preg_match('/[ \t]+#/', $scalar, $match, PREG_OFFSET_CAPTURE)) { + $scalar = substr($scalar, 0, $match[0][1]); + } + } elseif ('' === $scalar = $reader->readCSpan($delimiters)) { + throw new ParseException(sprintf('Malformed inline YAML string (%s).', $reader->readToEnd())); + } - $unescaper = new Unescaper(); - if ('"' == $scalar[$i]) { - $output = $unescaper->unescapeDoubleQuotedString($output); - } else { - $output = $unescaper->unescapeSingleQuotedString($output); + // a non-quoted string cannot start with @ or ` (reserved) nor with a scalar indicator (| or >) + if (1 === strspn($scalar, '@`|>', 0, 1)) { + throw new ParseException(sprintf('The reserved indicator "%s" cannot start a plain scalar; you need to quote the scalar.', $scalar[0])); } - $i += strlen($match[0]); + // @todo deprecate any reserved indicators + // @see http://www.yaml.org/spec/1.2/spec.html#c-indicator + if (1 === strspn($scalar, '%', 0, 1)) { + @trigger_error(sprintf('Not quoting the scalar "%s" starting with the "%s" indicator character is deprecated since Symfony 3.1 and will throw a ParseException in 4.0.', $scalar, $scalar[0]), E_USER_DEPRECATED); + } + $scalar = trim($scalar, "\t "); - return $output; + if ($evaluate) { + return self::evaluateScalar($scalar, $flags); + } + + return $scalar; } /** * Parses a YAML sequence. * - * @param string $sequence - * @param int $flags - * @param int &$i - * @param array $references + * @param StringReader $reader + * @param int &$flags + * @param array &$references * * @return array * * @throws ParseException When malformed inline YAML string is parsed */ - private static function parseSequence($sequence, $flags, &$i = 0, $references = array()) + private static function parseSequence(StringReader $reader, &$flags, array &$references = array()) { - $output = array(); - $len = strlen($sequence); - ++$i; - - // [foo, bar, ...] - while ($i < $len) { - switch ($sequence[$i]) { - case '[': - // nested sequence - $output[] = self::parseSequence($sequence, $flags, $i, $references); - break; - case '{': - // nested mapping - $output[] = self::parseMapping($sequence, $flags, $i, $references); - break; - case ']': - return $output; - case ',': - case ' ': - break; - default: - $isQuoted = in_array($sequence[$i], array('"', "'")); - $value = self::parseScalar($sequence, $flags, array(',', ']'), array('"', "'"), $i, true, $references); - - // the value can be an array if a reference has been resolved to an array var - if (is_string($value) && !$isQuoted && false !== strpos($value, ': ')) { - // embedded mapping? - try { - $pos = 0; - $value = self::parseMapping('{'.$value.'}', $flags, $pos, $references); - } catch (\InvalidArgumentException $e) { - // no, it's not - } - } + $sequence = array(); + self::parseStructure($reader, ']', function () use ($reader, &$sequence, $flags, $references) { + $isQuoted = in_array($reader->peek(), array('"', "'")); - $output[] = $value; + $value = self::parseValue($reader, $flags, '],', true, $references); - --$i; + if (is_string($value) && !$isQuoted && false !== strpos($value, ': ')) { + try { + $value = self::parseMapping(new StringReader($value.'}'), $flags, $references); + } catch (ParseException $e) { + } } - ++$i; - } + $sequence[] = $value; + }); - throw new ParseException(sprintf('Malformed inline YAML string: %s.', $sequence)); + return $sequence; } /** * Parses a YAML mapping. * - * @param string $mapping - * @param int $flags - * @param int &$i - * @param array $references + * @param StringReader $reader + * @param int &$flags + * @param array &$references * * @return array|\stdClass * * @throws ParseException When malformed inline YAML string is parsed */ - private static function parseMapping($mapping, $flags, &$i = 0, $references = array()) + private static function parseMapping(StringReader $reader, &$flags, &$references = array()) { - $output = array(); - $len = strlen($mapping); - ++$i; - - // {foo: bar, bar:foo, ...} - while ($i < $len) { - switch ($mapping[$i]) { - case ' ': - case ',': - ++$i; - continue 2; - case '}': - if (self::$objectForMap) { - return (object) $output; - } - - return $output; + $mapping = array(); + self::parseStructure($reader, '}', function () use ($reader, &$mapping, $flags, $references) { + $key = self::parseValue($reader, $flags, '},:', false, $references); + // @todo deprecate using special values without + // non-specific tag (false, true, null, ...) + if (!is_string($key) && !is_int($key)) { + throw new ParseException(sprintf('Mapping keys must be strings or integers, type "%s" provided.', gettype($key))); } + if ($reader->readChar(':')) { + if (!in_array($reader->peek(), array(' ', '[', ']', '{', '}'), true)) { + @trigger_error('Using a colon that is not followed by an indication character (i.e. " ", ",", "[", "]", "{", "}" is deprecated since version 3.2 and will throw a ParseException in 4.0.', E_USER_DEPRECATED); + } - // key - $key = self::parseScalar($mapping, $flags, array(':', ' '), array('"', "'"), $i, false); - - if (false === $i = strpos($mapping, ':', $i)) { - break; + $value = self::parseValue($reader, $flags, '},', true, $references); + } else { + $value = null; } - if (!isset($mapping[$i + 1]) || !in_array($mapping[$i + 1], array(' ', '[', ']', '{', '}'), true)) { - @trigger_error('Using a colon that is not followed by an indication character (i.e. " ", ",", "[", "]", "{", "}" is deprecated since version 3.2 and will throw a ParseException in 4.0.', E_USER_DEPRECATED); + if (isset($mapping[$key])) { + @trigger_error(sprintf('Duplicate key "%s" detected on line %d whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key, self::$parsedLineNumber + 1), E_USER_DEPRECATED); + } else { + $mapping[$key] = $value; } + }); - // value - $done = false; - - while ($i < $len) { - switch ($mapping[$i]) { - case '[': - // nested sequence - $value = self::parseSequence($mapping, $flags, $i, $references); - // Spec: Keys MUST be unique; first one wins. - // Parser cannot abort this mapping earlier, since lines - // are processed sequentially. - if (!isset($output[$key])) { - $output[$key] = $value; - } else { - @trigger_error(sprintf('Duplicate key "%s" detected on line %d whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key, self::$parsedLineNumber + 1), E_USER_DEPRECATED); - } - $done = true; - break; - case '{': - // nested mapping - $value = self::parseMapping($mapping, $flags, $i, $references); - // Spec: Keys MUST be unique; first one wins. - // Parser cannot abort this mapping earlier, since lines - // are processed sequentially. - if (!isset($output[$key])) { - $output[$key] = $value; - } else { - @trigger_error(sprintf('Duplicate key "%s" detected on line %d whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key, self::$parsedLineNumber + 1), E_USER_DEPRECATED); - } - $done = true; - break; - case ':': - case ' ': - break; - default: - $value = self::parseScalar($mapping, $flags, array(',', '}'), array('"', "'"), $i, true, $references); - // Spec: Keys MUST be unique; first one wins. - // Parser cannot abort this mapping earlier, since lines - // are processed sequentially. - if (!isset($output[$key])) { - $output[$key] = $value; - } else { - @trigger_error(sprintf('Duplicate key "%s" detected on line %d whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key, self::$parsedLineNumber + 1), E_USER_DEPRECATED); - } - $done = true; - --$i; - } + if (self::$objectForMap) { + return (object) $mapping; + } - ++$i; + return $mapping; + } - if ($done) { - continue 2; - } - } + private static function parseStructure(StringReader $reader, $closingMarker, callable $parseItem) + { + $reader->consumeWhiteSpace(); + // @todo deprecate support of consecutive comma + while ($reader->readChar(',')) { + $reader->consumeWhiteSpace(); } - throw new ParseException(sprintf('Malformed inline YAML string: %s.', $mapping)); + while (!$reader->readChar($closingMarker)) { + $parseItem(); + + $reader->consumeWhiteSpace(); + if (!$reader->readChar(',')) { + $reader->expectChar($closingMarker); + break; + } + + do { + $reader->consumeWhiteSpace(); + + // bc support of [ foo, , ] + } while ($reader->readChar(',')); + } } /** @@ -537,83 +537,22 @@ private static function parseMapping($mapping, $flags, &$i = 0, $references = ar * * @throws ParseException when object parsing support was disabled and the parser detected a PHP object or when a reference could not be resolved */ - private static function evaluateScalar($scalar, $flags, $references = array()) + private static function evaluateScalar($scalar, &$flags) { - $scalar = trim($scalar); + $scalar = rtrim($scalar); $scalarLower = strtolower($scalar); - - if (0 === strpos($scalar, '*')) { - if (false !== $pos = strpos($scalar, '#')) { - $value = substr($scalar, 1, $pos - 2); - } else { - $value = substr($scalar, 1); - } - - // an unquoted * - if (false === $value || '' === $value) { - throw new ParseException('A reference must contain at least one character.'); - } - - if (!array_key_exists($value, $references)) { - throw new ParseException(sprintf('Reference "%s" does not exist.', $value)); - } - - return $references[$value]; - } - switch (true) { case 'null' === $scalarLower: - case '' === $scalar: case '~' === $scalar: + case '' === $scalar: return; case 'true' === $scalarLower: return true; case 'false' === $scalarLower: return false; // Optimise for returning strings. - case $scalar[0] === '+' || $scalar[0] === '-' || $scalar[0] === '.' || $scalar[0] === '!' || is_numeric($scalar[0]): + case $scalar[0] === '+' || $scalar[0] === '-' || $scalar[0] === '.' || is_numeric($scalar[0]): switch (true) { - case 0 === strpos($scalar, '!str'): - return (string) substr($scalar, 5); - case 0 === strpos($scalar, '! '): - return (int) self::parseScalar(substr($scalar, 2), $flags); - case 0 === strpos($scalar, '!php/object:'): - if (self::$objectSupport) { - return unserialize(substr($scalar, 12)); - } - - if (self::$exceptionOnInvalidType) { - throw new ParseException('Object support when parsing a YAML file has been disabled.'); - } - - return; - case 0 === strpos($scalar, '!!php/object:'): - if (self::$objectSupport) { - @trigger_error('The !!php/object tag to indicate dumped PHP objects is deprecated since version 3.1 and will be removed in 4.0. Use the !php/object tag instead.', E_USER_DEPRECATED); - - return unserialize(substr($scalar, 13)); - } - - if (self::$exceptionOnInvalidType) { - throw new ParseException('Object support when parsing a YAML file has been disabled.'); - } - - return; - case 0 === strpos($scalar, '!php/const:'): - if (self::$constantSupport) { - if (defined($const = substr($scalar, 11))) { - return constant($const); - } - - throw new ParseException(sprintf('The constant "%s" is not defined.', $const)); - } - if (self::$exceptionOnInvalidType) { - throw new ParseException(sprintf('The string "%s" could not be parsed as a constant. Have you forgotten to pass the "Yaml::PARSE_CONSTANT" flag to the parser?', $scalar)); - } - - return; - case 0 === strpos($scalar, '!!float '): - return (float) substr($scalar, 8); case preg_match('{^[+-]?[0-9][0-9_]*$}', $scalar): $scalar = str_replace('_', '', (string) $scalar); // omitting the break / return as integers are handled in the next case @@ -637,8 +576,6 @@ private static function evaluateScalar($scalar, $flags, $references = array()) return -log(0); case '-.inf' === $scalarLower: return log(0); - case 0 === strpos($scalar, '!!binary '): - return self::evaluateBinaryScalar(substr($scalar, 9)); case preg_match('/^(-|\+)?[0-9][0-9,]*(\.[0-9_]+)?$/', $scalar): case preg_match('/^(-|\+)?[0-9][0-9_]*(\.[0-9_]+)?$/', $scalar): if (false !== strpos($scalar, ',')) { @@ -671,9 +608,9 @@ private static function evaluateScalar($scalar, $flags, $references = array()) * * @internal */ - public static function evaluateBinaryScalar($scalar) + public static function evaluateBinaryScalar($binaryData) { - $parsedBinaryData = self::parseScalar(preg_replace('/\s/', '', $scalar)); + $parsedBinaryData = preg_replace('/\s/', '', $binaryData); if (0 !== (strlen($parsedBinaryData) % 4)) { 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))); diff --git a/src/Symfony/Component/Yaml/Parser.php b/src/Symfony/Component/Yaml/Parser.php index 55beabea44934..b30590fb5161f 100644 --- a/src/Symfony/Component/Yaml/Parser.php +++ b/src/Symfony/Component/Yaml/Parser.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Yaml; use Symfony\Component\Yaml\Exception\ParseException; +use Symfony\Component\Yaml\Util\StringReader; /** * Parser parses YAML strings to convert them to PHP arrays. @@ -154,10 +155,10 @@ public function parse($value, $flags = 0) $context = 'mapping'; // force correct settings - Inline::parse(null, $flags, $this->refs); + Inline::parse(null); try { Inline::$parsedLineNumber = $this->getRealCurrentLineNb(); - $key = Inline::parseScalar($values['key']); + $key = Inline::parseScalar(new StringReader($values['key'])); } catch (ParseException $e) { $e->setParsedLine($this->getRealCurrentLineNb() + 1); $e->setSnippet($this->currentLine); @@ -544,7 +545,8 @@ private function moveToPreviousLine() */ private function parseValue($value, $flags, $context) { - if (0 === strpos($value, '*')) { + $reader = new StringReader($value); + if ($reader->readChar('*')) { if (false !== $pos = strpos($value, '#')) { $value = substr($value, 1, $pos - 2); } else { @@ -575,7 +577,7 @@ private function parseValue($value, $flags, $context) // do not take following lines into account when the current line is a quoted single line value if (null !== $quotation && preg_match('/^'.$quotation.'.*'.$quotation.'(\s*#.*)?$/', $value)) { - return Inline::parse($value, $flags, $this->refs); + return Inline::parse($reader, $flags, $this->refs); } while ($this->moveToNextLine()) { @@ -799,30 +801,27 @@ private function cleanup($value) $value = str_replace(array("\r\n", "\r"), "\n", $value); // strip YAML header - $count = 0; - $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count); - $this->offset += $count; + $reader = new StringReader($value); + while ($reader->readString('%yaml', true) || $reader->readChar('#')) { + $reader->readCSpan("\n"); + $reader->readChar("\n"); - // remove leading comments - $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count); - if ($count == 1) { - // items have been removed, update the offset - $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n"); - $value = $trimmedValue; + ++$this->offset; } // remove start of the document marker (---) - $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count); - if ($count == 1) { - // items have been removed, update the offset - $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n"); - $value = $trimmedValue; + if ($reader->readString('---')) { + $reader->readCSpan("\n"); + $reader->readChar("\n"); + $yaml = $reader->read(max(0, $reader->getRemainingByteCount() - 3)); // remove end of the document marker (...) - $value = preg_replace('#\.\.\.\s*$#', '', $value); + $reader->readString('...'); + + return $yaml.$reader->readToEnd(); } - return $value; + return $reader->readToEnd(); } /** @@ -864,7 +863,7 @@ private function isNextLineUnIndentedCollection() */ private function isStringUnIndentedCollectionItem() { - return '-' === rtrim($this->currentLine) || 0 === strpos($this->currentLine, '- '); + return $this->currentLine && '-' === $this->currentLine[0]; } /** diff --git a/src/Symfony/Component/Yaml/Tests/InlineTest.php b/src/Symfony/Component/Yaml/Tests/InlineTest.php index 355deb454af06..63447dc525732 100644 --- a/src/Symfony/Component/Yaml/Tests/InlineTest.php +++ b/src/Symfony/Component/Yaml/Tests/InlineTest.php @@ -34,6 +34,35 @@ public function testParseWithMapObjects($yaml, $value) $this->assertSame(serialize($value), serialize($actual)); } + public function testUnsupportedTag() + { + $this->assertEquals('!unsupported', Inline::parse('!unsupported')); + } + + public function testTagSupport() + { + $this->assertEquals(array('foo' => 'bar'), Inline::parse('{!str foo: bar}')); + $this->assertEquals(array(4 => 3.3), Inline::parse('{4: !!float 3.3}')); + + // Non-specific tag + $this->assertEquals(array('foo' => 'bar'), Inline::parse('! {foo: bar}')); + $this->assertEquals(2, Inline::parse('! "2f"')); + } + + /** + * @expectedException \Symfony\Component\Yaml\Exception\ParseException + * @expectedExceptionMessage Expected "]", got "d". + */ + public function testUnexpectedCharactersAfterQuotes() + { + $this->assertEquals(array('foo', 'bar'), Inline::parse('["foo"d]')); + } + + public function testParseConsecutiveCommas() + { + $this->assertEquals(array('foo', 'bar'), Inline::parse('[foo,, bar,]')); + } + /** * @dataProvider getTestsForParsePhpConstants */ @@ -195,7 +224,7 @@ public function testParseScalarWithCorrectlyQuotedStringShouldReturnString() $value = "'don''t do somthin'' like that'"; $expect = "don't do somthin' like that"; - $this->assertSame($expect, Inline::parseScalar($value)); + $this->assertSame($expect, Inline::parse($value)); } /** @@ -459,6 +488,9 @@ public function getTestsForParseWithMapObjects() array('{\'foo\'\'\': \'bar\', "bar\"": \'foo: bar\'}', (object) array('foo\'' => 'bar', 'bar"' => 'foo: bar')), array('{\'foo: \': \'bar\', "bar: ": \'foo: bar\'}', (object) array('foo: ' => 'bar', 'bar: ' => 'foo: bar')), + // mapping with implicit values + array('{fo o, bar, q uz : bar}', (object) array('fo o' => null, 'bar' => null, 'q uz' => 'bar')), + // nested sequences and mappings array('[foo, [bar, foo]]', array('foo', array('bar', 'foo'))), array('[foo, {bar: foo}]', array('foo', (object) array('bar' => 'foo'))), @@ -668,12 +700,8 @@ public function getInvalidBinaryData() ); } - /** - * @expectedException \Symfony\Component\Yaml\Exception\ParseException - * @expectedExceptionMessage Malformed inline YAML string: {this, is not, supported}. - */ - public function testNotSupportedMissingValue() + public function testMappingWithMissingValues() { - Inline::parse('{this, is not, supported}'); + $this->assertEquals(array('values' => null, 'are' => null, 'guessed' => null), Inline::parse('{values, are, guessed}')); } } diff --git a/src/Symfony/Component/Yaml/Unescaper.php b/src/Symfony/Component/Yaml/Unescaper.php index 6e863e12f2ad4..a775cf43cffcf 100644 --- a/src/Symfony/Component/Yaml/Unescaper.php +++ b/src/Symfony/Component/Yaml/Unescaper.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Yaml; use Symfony\Component\Yaml\Exception\ParseException; +use Symfony\Component\Yaml\Util\StringReader; /** * Unescaper encapsulates unescaping rules for single and double-quoted @@ -23,38 +24,45 @@ */ class Unescaper { - /** - * Regex fragment that matches an escaped character in a double quoted string. - */ - const REGEX_ESCAPED_CHARACTER = '\\\\(x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}|.)'; - /** * Unescapes a single quoted string. * - * @param string $value A single quoted string - * * @return string The unescaped string */ - public function unescapeSingleQuotedString($value) + public function unescapeSingleQuotedString(StringReader $reader) { - return str_replace('\'\'', '\'', $value); + $value = $reader->readCSpan('\''); + $reader->expectChar('\''); + + while ($reader->readChar('\'')) { + $value .= '\''; + $value .= $reader->readCSpan('\''); + $reader->expectChar('\''); + } + + return $value; } /** * Unescapes a double quoted string. * - * @param string $value A double quoted string - * * @return string The unescaped string */ - public function unescapeDoubleQuotedString($value) + public function unescapeDoubleQuotedString(StringReader $reader) { - $callback = function ($match) { - return $this->unescapeCharacter($match[0]); - }; + $value = $reader->readCSpan('"\\'); + while (true) { + if ($reader->readChar('\\')) { + $value .= $this->unescapeCharacter($reader); + } else { + $reader->expectChar('"'); + break; + } + + $value .= $reader->readCSpan('"\\'); + } - // evaluate the string - return preg_replace_callback('/'.self::REGEX_ESCAPED_CHARACTER.'/u', $callback, $value); + return $value; } /** @@ -64,9 +72,10 @@ public function unescapeDoubleQuotedString($value) * * @return string The unescaped character */ - private function unescapeCharacter($value) + private function unescapeCharacter(StringReader $reader) { - switch ($value[1]) { + $character = $reader->read(1); + switch ($character) { case '0': return "\x0"; case 'a': @@ -108,13 +117,13 @@ private function unescapeCharacter($value) // U+2029 PARAGRAPH SEPARATOR return "\xE2\x80\xA9"; case 'x': - return self::utf8chr(hexdec(substr($value, 2, 2))); + return self::utf8chr(hexdec($reader->read(2))); case 'u': - return self::utf8chr(hexdec(substr($value, 2, 4))); + return self::utf8chr(hexdec($reader->read(4))); case 'U': - return self::utf8chr(hexdec(substr($value, 2, 8))); + return self::utf8chr(hexdec($reader->read(8))); default: - throw new ParseException(sprintf('Found unknown escape character "%s".', $value)); + throw new ParseException(sprintf('Found unknown escape character "\\%s".', $character)); } } diff --git a/src/Symfony/Component/Yaml/Util/StringReader.php b/src/Symfony/Component/Yaml/Util/StringReader.php new file mode 100644 index 0000000000000..d424300741014 --- /dev/null +++ b/src/Symfony/Component/Yaml/Util/StringReader.php @@ -0,0 +1,197 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Yaml\Util; + +use Symfony\Component\Yaml\Exception\ParseException; + +/** + * @author Guilhem N. + * @author Nicolas "Exter-N" L. + * + * @internal + */ +class StringReader +{ + /** + * @var string + */ + const WHITE_SPACE_MASK = "\t "; + + private $data; + private $start = 0; + private $end; + private $offset = 0; + + /** + * @param string $data + */ + public function __construct($data) + { + $this->data = $data; + $this->end = strlen($data); + } + + /** + * @param string $char + * + * @return bool + */ + public function readChar($char) + { + if (isset($this->data[$this->offset]) && $char === $this->data[$this->offset]) { + ++$this->offset; + + return true; + } + + return false; + } + + public function expectChar($char) + { + if (!$this->readChar($char)) { + throw new ParseException(sprintf('Expected "%s", got "%s".', $char, $this->peek())); + } + } + + /** + * @param string[]|\Traversable $strings + * @param bool $caseInsensitive + */ + public function readAny($strings, $caseInsensitive = false) + { + foreach ($strings as $string) { + if ($this->readString($string, $caseInsensitive)) { + return $string; + } + } + } + + /** + * @param string $string + * @param bool $caseInsensitive + * + * @return bool + */ + public function readString($string, $caseInsensitive = false) + { + $length = strlen($string); + if (!isset($this->data[$this->offset + $length - 1])) { + return false; + } + + if (0 !== substr_compare($this->data, $string, $this->offset, $length, $caseInsensitive)) { + return false; + } + + $this->offset += $length; + + return true; + } + + /** + * @param string $mask + * + * @return string + */ + public function readSpan($mask) + { + return $this->internalRead(strspn($this->data, $mask, $this->offset)); + } + + /** + * @param string $mask + * + * @return string + */ + public function readCSpan($mask) + { + return $this->internalRead(strcspn($this->data, $mask, $this->offset)); + } + + /** + * @return int + */ + public function consumeWhiteSpace() + { + $length = strspn($this->data, self::WHITE_SPACE_MASK, $this->offset); + $this->offset += $length; + + return $length; + } + + /** + * @return int + */ + public function getRemainingByteCount() + { + return $this->end - $this->offset; + } + + public function isFullyConsumed() + { + return 0 === $this->getRemainingByteCount(); + } + + /** + * Returns the next byte. + * + * @return string|null null if not enough data + */ + public function peek() + { + if (isset($this->data[$this->offset])) { + return $this->data[$this->offset]; + } + } + + /** + * @param int $byteCount Number of bytes to read + * + * @return string + */ + public function read($byteCount) + { + $maxByteCount = $this->getRemainingByteCount(); + $byteCount = min($byteCount, $maxByteCount); + + return $this->internalRead($byteCount); + } + + /** + * @return string + */ + public function readToEnd() + { + return $this->internalRead($this->getRemainingByteCount()); + } + + /** + * No checks are performed, used internally when the source is sure. + */ + private function internalRead($byteCount) + { + if (0 === $byteCount) { + return ''; + } + + $substr = substr($this->data, $this->offset, $byteCount); + $this->offset += $byteCount; + + return $substr; + } + + public function __toString() + { + return $this->data; + } +}