From 85a5c31e0526d79ed3980640d720ab5fd351a520 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 5 Dec 2017 08:15:46 +0100 Subject: [PATCH] fix parsing inline YAML spanning multiple lines --- src/Symfony/Component/Yaml/CHANGELOG.md | 1 + src/Symfony/Component/Yaml/Inline.php | 9 +- src/Symfony/Component/Yaml/Parser.php | 179 +++++++++++++++ .../Component/Yaml/Tests/InlineTest.php | 2 +- .../Component/Yaml/Tests/ParserTest.php | 203 +++++++++++++++++- 5 files changed, 388 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Yaml/CHANGELOG.md b/src/Symfony/Component/Yaml/CHANGELOG.md index d80aba4b326cf..ff78af2feb2ac 100644 --- a/src/Symfony/Component/Yaml/CHANGELOG.md +++ b/src/Symfony/Component/Yaml/CHANGELOG.md @@ -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. diff --git a/src/Symfony/Component/Yaml/Inline.php b/src/Symfony/Component/Yaml/Inline.php index d2481c4869715..2d0c0b253d5e2 100644 --- a/src/Symfony/Component/Yaml/Inline.php +++ b/src/Symfony/Component/Yaml/Inline.php @@ -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); } @@ -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 '}': @@ -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); } @@ -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; @@ -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. diff --git a/src/Symfony/Component/Yaml/Parser.php b/src/Symfony/Component/Yaml/Parser.php index ef53f2a5e6e8c..c34e264b669ae 100644 --- a/src/Symfony/Component/Yaml/Parser.php +++ b/src/Symfony/Component/Yaml/Parser.php @@ -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) { @@ -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 @@ -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; + } } diff --git a/src/Symfony/Component/Yaml/Tests/InlineTest.php b/src/Symfony/Component/Yaml/Tests/InlineTest.php index 74dc8ff19a5c9..5942f6918d922 100644 --- a/src/Symfony/Component/Yaml/Tests/InlineTest.php +++ b/src/Symfony/Component/Yaml/Tests/InlineTest.php @@ -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'"); } } diff --git a/src/Symfony/Component/Yaml/Tests/ParserTest.php b/src/Symfony/Component/Yaml/Tests/ParserTest.php index 366902a8647e4..9842dc982eaef 100644 --- a/src/Symfony/Component/Yaml/Tests/ParserTest.php +++ b/src/Symfony/Component/Yaml/Tests/ParserTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Yaml\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Parser; use Symfony\Component\Yaml\Tag\TaggedValue; use Symfony\Component\Yaml\Yaml; @@ -1614,6 +1615,206 @@ public function multiLineDataProvider() return $tests; } + /** + * @dataProvider inlineNotationSpanningMultipleLinesProvider + */ + public function testInlineNotationSpanningMultipleLines($expected, string $yaml) + { + $this->assertEquals($expected, $this->parser->parse($yaml)); + } + + public function inlineNotationSpanningMultipleLinesProvider(): array + { + return [ + 'mapping' => [ + ['foo' => 'bar', 'bar' => 'baz'], + << [ + ['foo', 'bar'], + << [ + ['foo' => ['bar', 'foobar'], 'bar' => ['baz']], + << [ + [ + 'foobar' => [ + 'foo', + 'bar', + 'baz', + ], + ], + << [ + [ + 'foo' => [ + 'foobar', + [ + 'bar', + 'baz', + ], + ], + ], + << [ + [ + 'foo' => [ + 'foobar', + [ + 'bar', + 'baz', + ], + ], + ], + << [ + ['foo', ['bar' => 'baz']], + << [ + [ + [ + 'foo' => 'bar', + 'bar' => 'baz', + ], + ], + << [ + [ + [ + 'foo' => [ + 'bar' => 'foobar', + ], + 'bar' => 'baz', + ], + ], + << [ + [ + [ + 'foo' => [ + 'bar' => 'foobar', + ], + 'bar' => 'baz', + ], + ], + << [ + "foo\nbar", + << [ + "foo\nbar", + << [ + ['foo' => "bar\nbaz"], + <<expectException(ParseException::class); + $this->expectExceptionMessage('Unable to parse at line 2 (near "foobar").'); + + $yaml = <<parser->parse($yaml); + } + public function testTaggedInlineMapping() { $this->assertEquals(new TaggedValue('foo', ['foo' => 'bar']), $this->parser->parse('!foo {foo: bar}', Yaml::PARSE_CUSTOM_TAGS)); @@ -1749,7 +1950,7 @@ public function testComplexMappingNestedInSequenceThrowsParseException() public function testParsingIniThrowsException() { $this->expectException('Symfony\Component\Yaml\Exception\ParseException'); - $this->expectExceptionMessage('Unable to parse at line 1 (near "[parameters]").'); + $this->expectExceptionMessage('Unable to parse at line 2 (near " foo = bar").'); $ini = << pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy