From 951acca387fe4d06684a10eee5787e9d1524d923 Mon Sep 17 00:00:00 2001 From: sun Date: Thu, 15 May 2014 02:05:21 +0200 Subject: [PATCH] Fixed YAML Parser does not ignore duplicate keys, violating YAML spec. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The current [YAML 1.2] specification clearly states: > JSON's RFC4627 requires that mappings keys merely “SHOULD” be unique, while YAML insists they “MUST” be. The outdated [YAML 1.1] spec contained a crystal clear note on how the error of duplicate keys is to be handled by parsers, which is (sadly) no longer contained in the latest 1.2 spec (only leaving the requirement): > It is an error for two equal keys to appear in the same mapping node. In such a case the YAML processor may continue, ignoring the second `key: value` pair and issuing an appropriate warning. This strategy preserves a consistent information model for one-pass and random access applications. [YAML 1.2]: http://yaml.org/spec/1.2/spec.html#id2759572 [YAML 1.1]: http://yaml.org/spec/1.1/#id932806 --- src/Symfony/Component/Yaml/Inline.php | 24 ++++++++-- src/Symfony/Component/Yaml/Parser.php | 23 +++++++-- .../Component/Yaml/Tests/ParserTest.php | 47 +++++++++++++++++++ 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Yaml/Inline.php b/src/Symfony/Component/Yaml/Inline.php index b0d6a031f8..2242d12c7b 100644 --- a/src/Symfony/Component/Yaml/Inline.php +++ b/src/Symfony/Component/Yaml/Inline.php @@ -350,19 +350,37 @@ class Inline switch ($mapping[$i]) { case '[': // nested sequence - $output[$key] = self::parseSequence($mapping, $i); + $value = self::parseSequence($mapping, $i); + // 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; + } $done = true; break; case '{': // nested mapping - $output[$key] = self::parseMapping($mapping, $i); + $value = self::parseMapping($mapping, $i); + // 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; + } $done = true; break; case ':': case ' ': break; default: - $output[$key] = self::parseScalar($mapping, array(',', '}'), array('"', "'"), $i); + $value = self::parseScalar($mapping, array(',', '}'), array('"', "'"), $i); + // 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; + } $done = true; --$i; } diff --git a/src/Symfony/Component/Yaml/Parser.php b/src/Symfony/Component/Yaml/Parser.php index 9bde67b57b..9539e36aa3 100644 --- a/src/Symfony/Component/Yaml/Parser.php +++ b/src/Symfony/Component/Yaml/Parser.php @@ -178,18 +178,35 @@ class Parser } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) { // if next line is less indented or equal, then it means that the current value is null if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) { - $data[$key] = null; + // Spec: Keys MUST be unique; first one wins. + // Parser cannot abort this mapping earlier, since lines + // are processed sequentially. + if (!isset($data[$key])) { + $data[$key] = null; + } } else { $c = $this->getRealCurrentLineNb() + 1; $parser = new Parser($c); $parser->refs =& $this->refs; - $data[$key] = $parser->parse($this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport); + $value = $parser->parse($this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport); + // Spec: Keys MUST be unique; first one wins. + // Parser cannot abort this mapping earlier, since lines + // are processed sequentially. + if (!isset($data[$key])) { + $data[$key] = $value; + } } } else { if ($isInPlace) { $data = $this->refs[$isInPlace]; } else { - $data[$key] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport); + $value = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport);; + // Spec: Keys MUST be unique; first one wins. + // Parser cannot abort this mapping earlier, since lines + // are processed sequentially. + if (!isset($data[$key])) { + $data[$key] = $value; + } } } } else { diff --git a/src/Symfony/Component/Yaml/Tests/ParserTest.php b/src/Symfony/Component/Yaml/Tests/ParserTest.php index 2335efc559..a4db960729 100644 --- a/src/Symfony/Component/Yaml/Tests/ParserTest.php +++ b/src/Symfony/Component/Yaml/Tests/ParserTest.php @@ -508,6 +508,53 @@ EOF ); } + /** + * > It is an error for two equal keys to appear in the same mapping node. + * > In such a case the YAML processor may continue, ignoring the second + * > `key: value` pair and issuing an appropriate warning. This strategy + * > preserves a consistent information model for one-pass and random access + * > applications. + * + * @see http://yaml.org/spec/1.2/spec.html#id2759572 + * @see http://yaml.org/spec/1.1/#id932806 + * + * @covers \Symfony\Component\Yaml\Parser::parse + */ + public function testMappingDuplicateKeyBlock() + { + $input = << array( + 'child' => 'first', + ), + ); + $this->assertSame($expected, Yaml::parse($input)); + } + + /** + * @covers \Symfony\Component\Yaml\Inline::parseMapping + */ + public function testMappingDuplicateKeyFlow() + { + $input = << array( + 'child' => 'first', + ), + ); + $this->assertSame($expected, Yaml::parse($input)); + } + public function testEmptyValue() { $input = <<