Skip to content

Commit

Permalink
feature #33658 [Yaml] fix parsing inline YAML spanning multiple lines…
Browse files Browse the repository at this point in the history
… (xabbuh)

This PR was merged into the 4.4 branch.

Discussion
----------

[Yaml] fix parsing inline YAML spanning multiple lines

| Q             | A
| ------------- | ---
| Branch?       | 4.4
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | Fix #25239 #25379 #31333
| License       | MIT
| Doc PR        |

Commits
-------

85a5c31 fix parsing inline YAML spanning multiple lines
  • Loading branch information
fabpot committed Sep 25, 2019
2 parents b180208 + 85a5c31 commit 4cf7ec1
Show file tree
Hide file tree
Showing 5 changed files with 387 additions and 6 deletions.
1 change: 1 addition & 0 deletions src/Symfony/Component/Yaml/CHANGELOG.md
Expand Up @@ -4,6 +4,7 @@ CHANGELOG
4.4.0
-----

* Added support for parsing the inline notation spanning multiple lines.
* Added support to dump `null` as `~` by using the `Yaml::DUMP_NULL_AS_TILDE` flag.
* deprecated accepting STDIN implicitly when using the `lint:yaml` command, use `lint:yaml -` (append a dash) instead to make it explicit.

Expand Down
9 changes: 5 additions & 4 deletions src/Symfony/Component/Yaml/Inline.php
Expand Up @@ -274,7 +274,7 @@ public static function parseScalar(string $scalar, int $flags = 0, array $delimi
$output = self::parseQuotedScalar($scalar, $i);

if (null !== $delimiters) {
$tmp = ltrim(substr($scalar, $i), ' ');
$tmp = ltrim(substr($scalar, $i), " \n");
if ('' === $tmp) {
throw new ParseException(sprintf('Unexpected end of line, expected one of "%s".', implode('', $delimiters)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
}
Expand Down Expand Up @@ -419,6 +419,7 @@ private static function parseMapping(string $mapping, int $flags, int &$i = 0, a
switch ($mapping[$i]) {
case ' ':
case ',':
case "\n":
++$i;
continue 2;
case '}':
Expand Down Expand Up @@ -450,7 +451,7 @@ private static function parseMapping(string $mapping, int $flags, int &$i = 0, a
}
}

if (!$isKeyQuoted && (!isset($mapping[$i + 1]) || !\in_array($mapping[$i + 1], [' ', ',', '[', ']', '{', '}'], true))) {
if (!$isKeyQuoted && (!isset($mapping[$i + 1]) || !\in_array($mapping[$i + 1], [' ', ',', '[', ']', '{', '}', "\n"], true))) {
throw new ParseException('Colons must be followed by a space or an indication character (i.e. " ", ",", "[", "]", "{", "}").', self::$parsedLineNumber + 1, $mapping);
}

Expand All @@ -459,7 +460,7 @@ private static function parseMapping(string $mapping, int $flags, int &$i = 0, a
}

while ($i < $len) {
if (':' === $mapping[$i] || ' ' === $mapping[$i]) {
if (':' === $mapping[$i] || ' ' === $mapping[$i] || "\n" === $mapping[$i]) {
++$i;

continue;
Expand Down Expand Up @@ -508,7 +509,7 @@ private static function parseMapping(string $mapping, int $flags, int &$i = 0, a
}
break;
default:
$value = self::parseScalar($mapping, $flags, [',', '}'], $i, null === $tag, $references);
$value = self::parseScalar($mapping, $flags, [',', '}', "\n"], $i, null === $tag, $references);
// Spec: Keys MUST be unique; first one wins.
// Parser cannot abort this mapping earlier, since lines
// are processed sequentially.
Expand Down
179 changes: 179 additions & 0 deletions src/Symfony/Component/Yaml/Parser.php
Expand Up @@ -353,6 +353,61 @@ private function doParse(string $value, int $flags)
$this->refs[$isRef] = $data[$key];
array_pop($this->refsBeingParsed);
}
} elseif ('"' === $this->currentLine[0] || "'" === $this->currentLine[0]) {
if (null !== $context) {
throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
}

try {
return Inline::parse($this->parseQuotedString($this->currentLine), $flags, $this->refs);
} catch (ParseException $e) {
$e->setParsedLine($this->getRealCurrentLineNb() + 1);
$e->setSnippet($this->currentLine);

throw $e;
}
} elseif ('{' === $this->currentLine[0]) {
if (null !== $context) {
throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
}

try {
$parsedMapping = Inline::parse($this->lexInlineMapping($this->currentLine), $flags, $this->refs);

while ($this->moveToNextLine()) {
if (!$this->isCurrentLineEmpty()) {
throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
}
}

return $parsedMapping;
} catch (ParseException $e) {
$e->setParsedLine($this->getRealCurrentLineNb() + 1);
$e->setSnippet($this->currentLine);

throw $e;
}
} elseif ('[' === $this->currentLine[0]) {
if (null !== $context) {
throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
}

try {
$parsedSequence = Inline::parse($this->lexInlineSequence($this->currentLine), $flags, $this->refs);

while ($this->moveToNextLine()) {
if (!$this->isCurrentLineEmpty()) {
throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
}
}

return $parsedSequence;
} catch (ParseException $e) {
$e->setParsedLine($this->getRealCurrentLineNb() + 1);
$e->setSnippet($this->currentLine);

throw $e;
}
} else {
// multiple documents are not supported
if ('---' === $this->currentLine) {
Expand Down Expand Up @@ -678,6 +733,12 @@ private function parseValue(string $value, int $flags, string $context)
}

try {
if ('' !== $value && '{' === $value[0]) {
return Inline::parse($this->lexInlineMapping($value), $flags, $this->refs);
} elseif ('' !== $value && '[' === $value[0]) {
return Inline::parse($this->lexInlineSequence($value), $flags, $this->refs);
}

$quotation = '' !== $value && ('"' === $value[0] || "'" === $value[0]) ? $value[0] : null;

// do not take following lines into account when the current line is a quoted single line value
Expand Down Expand Up @@ -1072,4 +1133,122 @@ private function getLineTag(string $value, int $flags, bool $nextLineCheck = tru

throw new ParseException(sprintf('Tags support is not enabled. You must use the flag "Yaml::PARSE_CUSTOM_TAGS" to use "%s".', $matches['tag']), $this->getRealCurrentLineNb() + 1, $value, $this->filename);
}

private function parseQuotedString($yaml)
{
if ('' === $yaml || ('"' !== $yaml[0] && "'" !== $yaml[0])) {
throw new \InvalidArgumentException(sprintf('"%s" is not a quoted string.', $yaml));
}

$lines = [$yaml];

while ($this->moveToNextLine()) {
$lines[] = $this->currentLine;

if (!$this->isCurrentLineEmpty() && $yaml[0] === $this->currentLine[-1]) {
break;
}
}

$value = '';

for ($i = 0, $linesCount = \count($lines), $previousLineWasNewline = false, $previousLineWasTerminatedWithBackslash = false; $i < $linesCount; ++$i) {
if ('' === trim($lines[$i])) {
$value .= "\n";
} elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) {
$value .= ' ';
}

if ('' !== trim($lines[$i]) && '\\' === substr($lines[$i], -1)) {
$value .= ltrim(substr($lines[$i], 0, -1));
} elseif ('' !== trim($lines[$i])) {
$value .= trim($lines[$i]);
}

if ('' === trim($lines[$i])) {
$previousLineWasNewline = true;
$previousLineWasTerminatedWithBackslash = false;
} elseif ('\\' === substr($lines[$i], -1)) {
$previousLineWasNewline = false;
$previousLineWasTerminatedWithBackslash = true;
} else {
$previousLineWasNewline = false;
$previousLineWasTerminatedWithBackslash = false;
}
}

return $value;

for ($i = 1; isset($yaml[$i]) && $quotation !== $yaml[$i]; ++$i) {
}

// quoted single line string
if (isset($yaml[$i]) && $quotation === $yaml[$i]) {
return $yaml;
}

$lines = [$yaml];

while ($this->moveToNextLine()) {
for ($i = 1; isset($this->currentLine[$i]) && $quotation !== $this->currentLine[$i]; ++$i) {
}

$lines[] = trim($this->currentLine);

if (isset($this->currentLine[$i]) && $quotation === $this->currentLine[$i]) {
break;
}
}
}

private function lexInlineMapping(string $yaml): string
{
if ('' === $yaml || '{' !== $yaml[0]) {
throw new \InvalidArgumentException(sprintf('"%s" is not a sequence.', $yaml));
}

for ($i = 1; isset($yaml[$i]) && '}' !== $yaml[$i]; ++$i) {
}

if (isset($yaml[$i]) && '}' === $yaml[$i]) {
return $yaml;
}

$lines = [$yaml];

while ($this->moveToNextLine()) {
$lines[] = $this->currentLine;
}

return implode("\n", $lines);
}

private function lexInlineSequence($yaml)
{
if ('' === $yaml || '[' !== $yaml[0]) {
throw new \InvalidArgumentException(sprintf('"%s" is not a sequence.', $yaml));
}

for ($i = 1; isset($yaml[$i]) && ']' !== $yaml[$i]; ++$i) {
}

if (isset($yaml[$i]) && ']' === $yaml[$i]) {
return $yaml;
}

$value = $yaml;

while ($this->moveToNextLine()) {
for ($i = 1; isset($this->currentLine[$i]) && ']' !== $this->currentLine[$i]; ++$i) {
}

$value .= trim($this->currentLine);

if (isset($this->currentLine[$i]) && ']' === $this->currentLine[$i]) {
break;
}
}

return $value;
}
}
2 changes: 1 addition & 1 deletion src/Symfony/Component/Yaml/Tests/InlineTest.php
Expand Up @@ -711,7 +711,7 @@ public function testTagWithEmptyValueInMapping()
public function testUnfinishedInlineMap()
{
$this->expectException('Symfony\Component\Yaml\Exception\ParseException');
$this->expectExceptionMessage('Unexpected end of line, expected one of ",}" at line 1 (near "{abc: \'def\'").');
$this->expectExceptionMessage("Unexpected end of line, expected one of \",}\n\" at line 1 (near \"{abc: 'def'\").");
Inline::parse("{abc: 'def'");
}
}

0 comments on commit 4cf7ec1

Please sign in to comment.