bug #33763 [Yaml] fix lexing nested sequences/mappings (xabbuh)
This PR was merged into the 4.4 branch.
Discussion
----------
[Yaml] fix lexing nested sequences/mappings
| Q | A
| ------------- | ---
| Branch? | 4.4
| Bug fix? | yes
| New feature? | no
| Deprecations? | no
| Tickets | #34805, #37788, #37876, #39011, #39013, #39064
| License | MIT
| Doc PR |
Commits
-------
4c15f80d84
fix lexing nested sequences/mappings
This commit is contained in:
commit
308231aafb
@ -355,7 +355,7 @@ class Parser
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return Inline::parse($this->parseQuotedString($this->currentLine), $flags, $this->refs);
|
return Inline::parse($this->lexInlineQuotedString(), $flags, $this->refs);
|
||||||
} catch (ParseException $e) {
|
} catch (ParseException $e) {
|
||||||
$e->setParsedLine($this->getRealCurrentLineNb() + 1);
|
$e->setParsedLine($this->getRealCurrentLineNb() + 1);
|
||||||
$e->setSnippet($this->currentLine);
|
$e->setSnippet($this->currentLine);
|
||||||
@ -368,7 +368,7 @@ class Parser
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$parsedMapping = Inline::parse($this->lexInlineMapping($this->currentLine), $flags, $this->refs);
|
$parsedMapping = Inline::parse($this->lexInlineMapping(), $flags, $this->refs);
|
||||||
|
|
||||||
while ($this->moveToNextLine()) {
|
while ($this->moveToNextLine()) {
|
||||||
if (!$this->isCurrentLineEmpty()) {
|
if (!$this->isCurrentLineEmpty()) {
|
||||||
@ -389,7 +389,7 @@ class Parser
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$parsedSequence = Inline::parse($this->lexInlineSequence($this->currentLine), $flags, $this->refs);
|
$parsedSequence = Inline::parse($this->lexInlineSequence(), $flags, $this->refs);
|
||||||
|
|
||||||
while ($this->moveToNextLine()) {
|
while ($this->moveToNextLine()) {
|
||||||
if (!$this->isCurrentLineEmpty()) {
|
if (!$this->isCurrentLineEmpty()) {
|
||||||
@ -659,6 +659,11 @@ class Parser
|
|||||||
return implode("\n", $data);
|
return implode("\n", $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function hasMoreLines(): bool
|
||||||
|
{
|
||||||
|
return (\count($this->lines) - 1) > $this->currentLineNb;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moves the parser to the next line.
|
* Moves the parser to the next line.
|
||||||
*/
|
*/
|
||||||
@ -736,9 +741,13 @@ class Parser
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if ('' !== $value && '{' === $value[0]) {
|
if ('' !== $value && '{' === $value[0]) {
|
||||||
return Inline::parse($this->lexInlineMapping($value), $flags, $this->refs);
|
$cursor = \strlen($this->currentLine) - \strlen($value);
|
||||||
|
|
||||||
|
return Inline::parse($this->lexInlineMapping($cursor), $flags, $this->refs);
|
||||||
} elseif ('' !== $value && '[' === $value[0]) {
|
} elseif ('' !== $value && '[' === $value[0]) {
|
||||||
return Inline::parse($this->lexInlineSequence($value), $flags, $this->refs);
|
$cursor = \strlen($this->currentLine) - \strlen($value);
|
||||||
|
|
||||||
|
return Inline::parse($this->lexInlineSequence($cursor), $flags, $this->refs);
|
||||||
}
|
}
|
||||||
|
|
||||||
$quotation = '' !== $value && ('"' === $value[0] || "'" === $value[0]) ? $value[0] : null;
|
$quotation = '' !== $value && ('"' === $value[0] || "'" === $value[0]) ? $value[0] : null;
|
||||||
@ -1137,106 +1146,148 @@ class Parser
|
|||||||
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);
|
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(string $yaml): ?string
|
private function lexInlineQuotedString(int &$cursor = 0): string
|
||||||
{
|
{
|
||||||
if ('' === $yaml || ('"' !== $yaml[0] && "'" !== $yaml[0])) {
|
$quotation = $this->currentLine[$cursor];
|
||||||
throw new \InvalidArgumentException(sprintf('"%s" is not a quoted string.', $yaml));
|
$value = $quotation;
|
||||||
}
|
++$cursor;
|
||||||
|
|
||||||
$lines = [$yaml];
|
$previousLineWasNewline = true;
|
||||||
|
$previousLineWasTerminatedWithBackslash = false;
|
||||||
|
|
||||||
while ($this->moveToNextLine()) {
|
do {
|
||||||
$lines[] = $this->currentLine;
|
if ($this->isCurrentLineBlank()) {
|
||||||
|
|
||||||
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";
|
$value .= "\n";
|
||||||
} elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) {
|
} elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) {
|
||||||
$value .= ' ';
|
$value .= ' ';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('' !== trim($lines[$i]) && '\\' === substr($lines[$i], -1)) {
|
for (; \strlen($this->currentLine) > $cursor; ++$cursor) {
|
||||||
$value .= ltrim(substr($lines[$i], 0, -1));
|
switch ($this->currentLine[$cursor]) {
|
||||||
} elseif ('' !== trim($lines[$i])) {
|
case '\\':
|
||||||
$value .= trim($lines[$i]);
|
if (isset($this->currentLine[++$cursor])) {
|
||||||
|
$value .= '\\'.$this->currentLine[$cursor];
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case $quotation:
|
||||||
|
++$cursor;
|
||||||
|
|
||||||
|
if ("'" === $quotation && isset($this->currentLine[$cursor]) && "'" === $this->currentLine[$cursor]) {
|
||||||
|
$value .= "''";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value.$quotation;
|
||||||
|
default:
|
||||||
|
$value .= $this->currentLine[$cursor];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('' === trim($lines[$i])) {
|
if ($this->isCurrentLineBlank()) {
|
||||||
$previousLineWasNewline = true;
|
$previousLineWasNewline = true;
|
||||||
$previousLineWasTerminatedWithBackslash = false;
|
$previousLineWasTerminatedWithBackslash = false;
|
||||||
} elseif ('\\' === substr($lines[$i], -1)) {
|
} elseif ('\\' === $this->currentLine[-1]) {
|
||||||
$previousLineWasNewline = false;
|
$previousLineWasNewline = false;
|
||||||
$previousLineWasTerminatedWithBackslash = true;
|
$previousLineWasTerminatedWithBackslash = true;
|
||||||
} else {
|
} else {
|
||||||
$previousLineWasNewline = false;
|
$previousLineWasNewline = false;
|
||||||
$previousLineWasTerminatedWithBackslash = false;
|
$previousLineWasTerminatedWithBackslash = false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return $value;
|
if ($this->hasMoreLines()) {
|
||||||
|
$cursor = 0;
|
||||||
|
}
|
||||||
|
} while ($this->moveToNextLine());
|
||||||
|
|
||||||
|
throw new ParseException('Malformed inline YAML string');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function lexInlineMapping(string $yaml): string
|
private function lexUnquotedString(int &$cursor): string
|
||||||
{
|
{
|
||||||
if ('' === $yaml || '{' !== $yaml[0]) {
|
$offset = $cursor;
|
||||||
throw new \InvalidArgumentException(sprintf('"%s" is not a sequence.', $yaml));
|
$cursor += strcspn($this->currentLine, '[]{},: ', $cursor);
|
||||||
}
|
|
||||||
|
|
||||||
for ($i = 1; isset($yaml[$i]) && '}' !== $yaml[$i]; ++$i) {
|
return substr($this->currentLine, $offset, $cursor - $offset);
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($yaml[$i]) && '}' === $yaml[$i]) {
|
|
||||||
return $yaml;
|
|
||||||
}
|
|
||||||
|
|
||||||
$lines = [$yaml];
|
|
||||||
|
|
||||||
while ($this->moveToNextLine()) {
|
|
||||||
$lines[] = $this->currentLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
return implode("\n", $lines);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function lexInlineSequence(string $yaml): string
|
private function lexInlineMapping(int &$cursor = 0): string
|
||||||
{
|
{
|
||||||
if ('' === $yaml || '[' !== $yaml[0]) {
|
return $this->lexInlineStructure($cursor, '}');
|
||||||
throw new \InvalidArgumentException(sprintf('"%s" is not a sequence.', $yaml));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for ($i = 1; isset($yaml[$i]) && ']' !== $yaml[$i]; ++$i) {
|
private function lexInlineSequence(int &$cursor = 0): string
|
||||||
}
|
{
|
||||||
|
return $this->lexInlineStructure($cursor, ']');
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($yaml[$i]) && ']' === $yaml[$i]) {
|
private function lexInlineStructure(int &$cursor, string $closingTag): string
|
||||||
return $yaml;
|
{
|
||||||
}
|
$value = $this->currentLine[$cursor];
|
||||||
|
++$cursor;
|
||||||
|
|
||||||
$value = $yaml;
|
do {
|
||||||
|
$this->consumeWhitespaces($cursor);
|
||||||
|
|
||||||
while ($this->moveToNextLine()) {
|
while (isset($this->currentLine[$cursor])) {
|
||||||
for ($i = 1; isset($this->currentLine[$i]) && ']' !== $this->currentLine[$i]; ++$i) {
|
switch ($this->currentLine[$cursor]) {
|
||||||
|
case '"':
|
||||||
|
case "'":
|
||||||
|
$value .= $this->lexInlineQuotedString($cursor);
|
||||||
|
break;
|
||||||
|
case ':':
|
||||||
|
case ',':
|
||||||
|
$value .= $this->currentLine[$cursor];
|
||||||
|
++$cursor;
|
||||||
|
break;
|
||||||
|
case '{':
|
||||||
|
$value .= $this->lexInlineMapping($cursor);
|
||||||
|
break;
|
||||||
|
case '[':
|
||||||
|
$value .= $this->lexInlineSequence($cursor);
|
||||||
|
break;
|
||||||
|
case $closingTag:
|
||||||
|
$value .= $this->currentLine[$cursor];
|
||||||
|
++$cursor;
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
case '#':
|
||||||
|
break 2;
|
||||||
|
default:
|
||||||
|
$value .= $this->lexUnquotedString($cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->consumeWhitespaces($cursor)) {
|
||||||
|
$value .= ' ';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$trimmedValue = trim($this->currentLine);
|
if ($this->hasMoreLines()) {
|
||||||
|
$cursor = 0;
|
||||||
|
}
|
||||||
|
} while ($this->moveToNextLine());
|
||||||
|
|
||||||
if ('' !== $trimmedValue && '#' === $trimmedValue[0]) {
|
throw new ParseException('Malformed inline YAML string');
|
||||||
continue;
|
}
|
||||||
|
|
||||||
|
private function consumeWhitespaces(int &$cursor): bool
|
||||||
|
{
|
||||||
|
$whitespacesConsumed = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
$whitespaceOnlyTokenLength = strspn($this->currentLine, ' ', $cursor);
|
||||||
|
$whitespacesConsumed += $whitespaceOnlyTokenLength;
|
||||||
|
$cursor += $whitespaceOnlyTokenLength;
|
||||||
|
|
||||||
|
if (isset($this->currentLine[$cursor])) {
|
||||||
|
return 0 < $whitespacesConsumed;
|
||||||
}
|
}
|
||||||
|
|
||||||
$value .= $trimmedValue;
|
if ($this->hasMoreLines()) {
|
||||||
|
$cursor = 0;
|
||||||
if (isset($this->currentLine[$i]) && ']' === $this->currentLine[$i]) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
} while ($this->moveToNextLine());
|
||||||
|
|
||||||
return $value;
|
return 0 < $whitespacesConsumed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1660,6 +1660,16 @@ EOF;
|
|||||||
'foo': 'bar',
|
'foo': 'bar',
|
||||||
'bar': 'baz'
|
'bar': 'baz'
|
||||||
}
|
}
|
||||||
|
YAML
|
||||||
|
,
|
||||||
|
],
|
||||||
|
'mapping with unquoted strings and values' => [
|
||||||
|
['foo' => 'bar', 'bar' => 'baz'],
|
||||||
|
<<<YAML
|
||||||
|
{
|
||||||
|
foo: bar,
|
||||||
|
bar: baz
|
||||||
|
}
|
||||||
YAML
|
YAML
|
||||||
,
|
,
|
||||||
],
|
],
|
||||||
@ -1673,6 +1683,53 @@ YAML
|
|||||||
YAML
|
YAML
|
||||||
,
|
,
|
||||||
],
|
],
|
||||||
|
'sequence with unquoted items' => [
|
||||||
|
['foo', 'bar'],
|
||||||
|
<<<YAML
|
||||||
|
[
|
||||||
|
foo,
|
||||||
|
bar
|
||||||
|
]
|
||||||
|
YAML
|
||||||
|
,
|
||||||
|
],
|
||||||
|
'nested mapping terminating at end of line' => [
|
||||||
|
[
|
||||||
|
'foo' => [
|
||||||
|
'bar' => 'foobar',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
<<<YAML
|
||||||
|
{ foo: { bar: foobar }
|
||||||
|
}
|
||||||
|
YAML
|
||||||
|
,
|
||||||
|
],
|
||||||
|
'nested sequence terminating at end of line' => [
|
||||||
|
[
|
||||||
|
'foo',
|
||||||
|
[
|
||||||
|
'bar',
|
||||||
|
'baz',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
<<<YAML
|
||||||
|
[ foo, [bar, baz]
|
||||||
|
]
|
||||||
|
YAML
|
||||||
|
],
|
||||||
|
'nested sequence spanning multiple lines' => [
|
||||||
|
[
|
||||||
|
['entry1', []],
|
||||||
|
['entry2'],
|
||||||
|
],
|
||||||
|
<<<YAML
|
||||||
|
[
|
||||||
|
['entry1', {}],
|
||||||
|
['entry2']
|
||||||
|
]
|
||||||
|
YAML
|
||||||
|
],
|
||||||
'sequence nested in mapping' => [
|
'sequence nested in mapping' => [
|
||||||
['foo' => ['bar', 'foobar'], 'bar' => ['baz']],
|
['foo' => ['bar', 'foobar'], 'bar' => ['baz']],
|
||||||
<<<YAML
|
<<<YAML
|
||||||
@ -1699,6 +1756,22 @@ foobar: [foo,
|
|||||||
YAML
|
YAML
|
||||||
,
|
,
|
||||||
],
|
],
|
||||||
|
'sequence spanning multiple lines nested in mapping with a following mapping' => [
|
||||||
|
[
|
||||||
|
'foobar' => [
|
||||||
|
'foo',
|
||||||
|
'bar',
|
||||||
|
],
|
||||||
|
'bar' => 'baz',
|
||||||
|
],
|
||||||
|
<<<YAML
|
||||||
|
foobar: [
|
||||||
|
foo,
|
||||||
|
bar,
|
||||||
|
]
|
||||||
|
bar: baz
|
||||||
|
YAML
|
||||||
|
],
|
||||||
'nested sequence nested in mapping starting on the same line' => [
|
'nested sequence nested in mapping starting on the same line' => [
|
||||||
[
|
[
|
||||||
'foo' => [
|
'foo' => [
|
||||||
@ -1824,6 +1897,110 @@ YAML
|
|||||||
foo: 'bar
|
foo: 'bar
|
||||||
|
|
||||||
baz'
|
baz'
|
||||||
|
YAML
|
||||||
|
],
|
||||||
|
'mixed mapping with inline notation having separated lines' => [
|
||||||
|
[
|
||||||
|
'map' => [
|
||||||
|
'key' => 'value',
|
||||||
|
'a' => 'b',
|
||||||
|
],
|
||||||
|
'param' => 'some',
|
||||||
|
],
|
||||||
|
<<<YAML
|
||||||
|
map: {
|
||||||
|
key: "value",
|
||||||
|
a: "b"
|
||||||
|
}
|
||||||
|
param: "some"
|
||||||
|
YAML
|
||||||
|
],
|
||||||
|
'mixed mapping with inline notation on one line' => [
|
||||||
|
[
|
||||||
|
'map' => [
|
||||||
|
'key' => 'value',
|
||||||
|
'a' => 'b',
|
||||||
|
],
|
||||||
|
'param' => 'some',
|
||||||
|
],
|
||||||
|
<<<YAML
|
||||||
|
map: {key: "value", a: "b"}
|
||||||
|
param: "some"
|
||||||
|
YAML
|
||||||
|
],
|
||||||
|
'mixed mapping with compact inline notation on one line' => [
|
||||||
|
[
|
||||||
|
'map' => [
|
||||||
|
'key' => 'value',
|
||||||
|
'a' => 'b',
|
||||||
|
],
|
||||||
|
'param' => 'some',
|
||||||
|
],
|
||||||
|
<<<YAML
|
||||||
|
map: {key: "value",
|
||||||
|
a: "b"}
|
||||||
|
param: "some"
|
||||||
|
YAML
|
||||||
|
],
|
||||||
|
'nested collections containing strings with bracket chars' => [
|
||||||
|
[
|
||||||
|
[']'],
|
||||||
|
['}'],
|
||||||
|
['ba[r'],
|
||||||
|
['[ba]r'],
|
||||||
|
['bar]'],
|
||||||
|
['foo' => 'bar{'],
|
||||||
|
['foo' => 'b{ar}'],
|
||||||
|
['foo' => 'bar}'],
|
||||||
|
],
|
||||||
|
<<<YAML
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"]"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"ba[r"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'[ba]r'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"bar]"
|
||||||
|
],
|
||||||
|
{
|
||||||
|
foo: "bar{"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foo: "b{ar}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foo: 'bar}'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
YAML
|
||||||
|
],
|
||||||
|
'escaped characters in quoted strings' => [
|
||||||
|
[
|
||||||
|
['te"st'],
|
||||||
|
['test'],
|
||||||
|
["te'st"],
|
||||||
|
['te"st]'],
|
||||||
|
['te"st'],
|
||||||
|
['test'],
|
||||||
|
["te'st"],
|
||||||
|
['te"st]'],
|
||||||
|
],
|
||||||
|
<<<YAML
|
||||||
|
[
|
||||||
|
["te\"st"],["test"],['te''st'],["te\"st]"],
|
||||||
|
["te\"st"],
|
||||||
|
["test"],
|
||||||
|
['te''st'],
|
||||||
|
["te\"st]"]
|
||||||
|
]
|
||||||
YAML
|
YAML
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
@ -1847,6 +2024,13 @@ YAML;
|
|||||||
$this->assertEquals(new TaggedValue('foo', ['foo' => 'bar']), $this->parser->parse('!foo {foo: bar}', Yaml::PARSE_CUSTOM_TAGS));
|
$this->assertEquals(new TaggedValue('foo', ['foo' => 'bar']), $this->parser->parse('!foo {foo: bar}', Yaml::PARSE_CUSTOM_TAGS));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testInvalidInlineSequenceContainingStringWithEscapedQuotationCharacter()
|
||||||
|
{
|
||||||
|
$this->expectException(ParseException::class);
|
||||||
|
|
||||||
|
$this->parser->parse('["\\"]');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dataProvider taggedValuesProvider
|
* @dataProvider taggedValuesProvider
|
||||||
*/
|
*/
|
||||||
|
Reference in New Issue
Block a user