From 79a63d50a17594e40473b9835f23d7217f090083 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sat, 20 Feb 2016 10:54:12 +0100 Subject: [PATCH] [Yaml] add support for the !!binary tag --- src/Symfony/Component/Yaml/CHANGELOG.md | 6 ++ src/Symfony/Component/Yaml/Inline.php | 31 +++++++ src/Symfony/Component/Yaml/Parser.php | 11 ++- .../Component/Yaml/Tests/DumperTest.php | 16 ++++ .../Component/Yaml/Tests/Fixtures/arrow.gif | Bin 0 -> 185 bytes .../Component/Yaml/Tests/InlineTest.php | 37 ++++++++ .../Component/Yaml/Tests/ParserTest.php | 81 ++++++++++++++++++ src/Symfony/Component/Yaml/Yaml.php | 1 + 8 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 src/Symfony/Component/Yaml/Tests/Fixtures/arrow.gif diff --git a/src/Symfony/Component/Yaml/CHANGELOG.md b/src/Symfony/Component/Yaml/CHANGELOG.md index 63d68b0d5e6e5..75f46e9f6fba7 100644 --- a/src/Symfony/Component/Yaml/CHANGELOG.md +++ b/src/Symfony/Component/Yaml/CHANGELOG.md @@ -4,6 +4,12 @@ CHANGELOG 3.1.0 ----- + * Added support for parsing base64 encoded binary data when they are tagged + with the `!!binary` tag. + + * Added support for dumping binary data as base64 encoded strings by passing + the `Yaml::DUMP_BASE64_BINARY_DATA` flag. + * Added support for parsing timestamps as `\DateTime` objects: ```php diff --git a/src/Symfony/Component/Yaml/Inline.php b/src/Symfony/Component/Yaml/Inline.php index b11e03147cd75..268b36bedd1fa 100644 --- a/src/Symfony/Component/Yaml/Inline.php +++ b/src/Symfony/Component/Yaml/Inline.php @@ -197,6 +197,8 @@ public static function dump($value, $flags = 0) return $repr; case '' == $value: return "''"; + case Yaml::DUMP_BASE64_BINARY_DATA & $flags && self::isBinaryString($value): + return '!!binary '.base64_encode($value); case Escaper::requiresDoubleQuoting($value): return Escaper::escapeWithDoubleQuotes($value); case Escaper::requiresSingleQuoting($value): @@ -576,6 +578,8 @@ 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]+)?$/', $scalar): return (float) str_replace(',', '', $scalar); case preg_match(self::getTimestampRegex(), $scalar): @@ -595,6 +599,33 @@ private static function evaluateScalar($scalar, $flags, $references = array()) } } + /** + * @param string $scalar + * + * @return string + * + * @internal + */ + public static function evaluateBinaryScalar($scalar) + { + $parsedBinaryData = self::parseScalar(preg_replace('/\s/', '', $scalar)); + + 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))); + } + + if (!preg_match('#^[A-Z0-9+/]+={0,2}$#i', $parsedBinaryData)) { + throw new ParseException(sprintf('The base64 encoded data (%s) contains invalid characters.', $parsedBinaryData)); + } + + return base64_decode($parsedBinaryData, true); + } + + private static function isBinaryString($value) + { + return preg_match('/[^\x09-\x0d\x20-\xff]/', $value); + } + /** * Gets a regex that matches a YAML date. * diff --git a/src/Symfony/Component/Yaml/Parser.php b/src/Symfony/Component/Yaml/Parser.php index 45eaffbb3a98e..c45ba46b874fb 100644 --- a/src/Symfony/Component/Yaml/Parser.php +++ b/src/Symfony/Component/Yaml/Parser.php @@ -20,6 +20,7 @@ */ class Parser { + const TAG_PATTERN = '((?P![\w!.\/:-]+) +)?'; const BLOCK_SCALAR_HEADER_PATTERN = '(?P\||>)(?P\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P +#.*)?'; private $offset = 0; @@ -516,10 +517,16 @@ private function parseValue($value, $flags, $context) return $this->refs[$value]; } - if (preg_match('/^'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) { + if (preg_match('/^'.self::TAG_PATTERN.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) { $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : ''; - return $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers)); + $data = $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers)); + + if (isset($matches['tag']) && '!!binary' === $matches['tag']) { + return Inline::evaluateBinaryScalar($data); + } + + return $data; } try { diff --git a/src/Symfony/Component/Yaml/Tests/DumperTest.php b/src/Symfony/Component/Yaml/Tests/DumperTest.php index 7a110aa577940..9c8bb7fb8ec35 100644 --- a/src/Symfony/Component/Yaml/Tests/DumperTest.php +++ b/src/Symfony/Component/Yaml/Tests/DumperTest.php @@ -276,6 +276,22 @@ public function getEscapeSequences() 'paragraph-separator' => array("\t\\P", '"\t\\\\P"'), ); } + + public function testBinaryDataIsDumpedAsIsWithoutFlag() + { + $binaryData = file_get_contents(__DIR__.'/Fixtures/arrow.gif'); + $expected = "{ data: '".str_replace("'", "''", $binaryData)."' }"; + + $this->assertSame($expected, $this->dumper->dump(array('data' => $binaryData))); + } + + public function testBinaryDataIsDumpedBase64EncodedWithFlag() + { + $binaryData = file_get_contents(__DIR__.'/Fixtures/arrow.gif'); + $expected = '{ data: !!binary '.base64_encode($binaryData).' }'; + + $this->assertSame($expected, $this->dumper->dump(array('data' => $binaryData), 0, 0, Yaml::DUMP_BASE64_BINARY_DATA)); + } } class A diff --git a/src/Symfony/Component/Yaml/Tests/Fixtures/arrow.gif b/src/Symfony/Component/Yaml/Tests/Fixtures/arrow.gif new file mode 100644 index 0000000000000000000000000000000000000000..443aca422f7624b271903e5fbb577c7f99786c0e GIT binary patch literal 185 zcmZ?wbhEHb0d66k_assertSame('Hello world', Inline::parse($data)); + } + + public function getBinaryData() + { + return array( + 'enclosed with double quotes' => array('!!binary "SGVsbG8gd29ybGQ="'), + 'enclosed with single quotes' => array("!!binary 'SGVsbG8gd29ybGQ='"), + 'containing spaces' => array('!!binary "SGVs bG8gd 29ybGQ="'), + ); + } + + /** + * @dataProvider getInvalidBinaryData + */ + public function testParseInvalidBinaryData($data, $expectedMessage) + { + $this->setExpectedExceptionRegExp('\Symfony\Component\Yaml\Exception\ParseException', $expectedMessage); + + Inline::parse($data); + } + + public function getInvalidBinaryData() + { + return array( + 'length not a multiple of four' => array('!!binary "SGVsbG8d29ybGQ="', '/The normalized base64 encoded data \(data without whitespace characters\) length must be a multiple of four \(\d+ bytes given\)/'), + 'invalid characters' => array('!!binary "SGVsbG8#d29ybGQ="', '/The base64 encoded data \(.*\) contains invalid characters/'), + 'too many equals characters' => array('!!binary "SGVsbG8gd29yb==="', '/The base64 encoded data \(.*\) contains invalid characters/'), + 'misplaced equals character' => array('!!binary "SGVsbG8gd29ybG=Q"', '/The base64 encoded data \(.*\) contains invalid characters/'), + ); + } } diff --git a/src/Symfony/Component/Yaml/Tests/ParserTest.php b/src/Symfony/Component/Yaml/Tests/ParserTest.php index 3bbe02d85137b..bddf969744eeb 100644 --- a/src/Symfony/Component/Yaml/Tests/ParserTest.php +++ b/src/Symfony/Component/Yaml/Tests/ParserTest.php @@ -1120,6 +1120,87 @@ public function testAdditionallyIndentedLinesAreParsedAsNewLinesInFoldedBlocks() $this->parser->parse($yaml) ); } + + /** + * @dataProvider getBinaryData + */ + public function testParseBinaryData($data) + { + $this->assertSame(array('data' => 'Hello world'), $this->parser->parse($data)); + } + + public function getBinaryData() + { + return array( + 'enclosed with double quotes' => array('data: !!binary "SGVsbG8gd29ybGQ="'), + 'enclosed with single quotes' => array("data: !!binary 'SGVsbG8gd29ybGQ='"), + 'containing spaces' => array('data: !!binary "SGVs bG8gd 29ybGQ="'), + 'in block scalar' => array( + << array( + <<setExpectedExceptionRegExp('\Symfony\Component\Yaml\Exception\ParseException', $expectedMessage); + + $this->parser->parse($data); + } + + public function getInvalidBinaryData() + { + return array( + 'length not a multiple of four' => array('data: !!binary "SGVsbG8d29ybGQ="', '/The normalized base64 encoded data \(data without whitespace characters\) length must be a multiple of four \(\d+ bytes given\)/'), + 'invalid characters' => array('!!binary "SGVsbG8#d29ybGQ="', '/The base64 encoded data \(.*\) contains invalid characters/'), + 'too many equals characters' => array('data: !!binary "SGVsbG8gd29yb==="', '/The base64 encoded data \(.*\) contains invalid characters/'), + 'misplaced equals character' => array('data: !!binary "SGVsbG8gd29ybG=Q"', '/The base64 encoded data \(.*\) contains invalid characters/'), + 'length not a multiple of four in block scalar' => array( + << array( + << array( + << array( + <<