From 4c15f80d8439ab12327e60f3432a231c8477c611 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 24 Sep 2019 00:08:17 +0200 Subject: [PATCH 1/3] fix lexing nested sequences/mappings --- src/Symfony/Component/Yaml/Parser.php | 189 +++++++++++------- .../Component/Yaml/Tests/ParserTest.php | 184 +++++++++++++++++ 2 files changed, 304 insertions(+), 69 deletions(-) diff --git a/src/Symfony/Component/Yaml/Parser.php b/src/Symfony/Component/Yaml/Parser.php index e8f951a1ec..259e3e6b49 100644 --- a/src/Symfony/Component/Yaml/Parser.php +++ b/src/Symfony/Component/Yaml/Parser.php @@ -355,7 +355,7 @@ class Parser } try { - return Inline::parse($this->parseQuotedString($this->currentLine), $flags, $this->refs); + return Inline::parse($this->lexInlineQuotedString(), $flags, $this->refs); } catch (ParseException $e) { $e->setParsedLine($this->getRealCurrentLineNb() + 1); $e->setSnippet($this->currentLine); @@ -368,7 +368,7 @@ class Parser } try { - $parsedMapping = Inline::parse($this->lexInlineMapping($this->currentLine), $flags, $this->refs); + $parsedMapping = Inline::parse($this->lexInlineMapping(), $flags, $this->refs); while ($this->moveToNextLine()) { if (!$this->isCurrentLineEmpty()) { @@ -389,7 +389,7 @@ class Parser } try { - $parsedSequence = Inline::parse($this->lexInlineSequence($this->currentLine), $flags, $this->refs); + $parsedSequence = Inline::parse($this->lexInlineSequence(), $flags, $this->refs); while ($this->moveToNextLine()) { if (!$this->isCurrentLineEmpty()) { @@ -659,6 +659,11 @@ class Parser return implode("\n", $data); } + private function hasMoreLines(): bool + { + return (\count($this->lines) - 1) > $this->currentLineNb; + } + /** * Moves the parser to the next line. */ @@ -736,9 +741,13 @@ class Parser try { 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]) { - 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; @@ -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); } - private function parseQuotedString(string $yaml): ?string + private function lexInlineQuotedString(int &$cursor = 0): string { - if ('' === $yaml || ('"' !== $yaml[0] && "'" !== $yaml[0])) { - throw new \InvalidArgumentException(sprintf('"%s" is not a quoted string.', $yaml)); - } + $quotation = $this->currentLine[$cursor]; + $value = $quotation; + ++$cursor; - $lines = [$yaml]; + $previousLineWasNewline = true; + $previousLineWasTerminatedWithBackslash = false; - 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])) { + do { + if ($this->isCurrentLineBlank()) { $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]); + for (; \strlen($this->currentLine) > $cursor; ++$cursor) { + switch ($this->currentLine[$cursor]) { + case '\\': + 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; $previousLineWasTerminatedWithBackslash = false; - } elseif ('\\' === substr($lines[$i], -1)) { + } elseif ('\\' === $this->currentLine[-1]) { $previousLineWasNewline = false; $previousLineWasTerminatedWithBackslash = true; } else { $previousLineWasNewline = 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]) { - throw new \InvalidArgumentException(sprintf('"%s" is not a sequence.', $yaml)); - } + $offset = $cursor; + $cursor += strcspn($this->currentLine, '[]{},: ', $cursor); - 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); + return substr($this->currentLine, $offset, $cursor - $offset); } - private function lexInlineSequence(string $yaml): string + private function lexInlineMapping(int &$cursor = 0): string { - if ('' === $yaml || '[' !== $yaml[0]) { - throw new \InvalidArgumentException(sprintf('"%s" is not a sequence.', $yaml)); - } + return $this->lexInlineStructure($cursor, '}'); + } - 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]) { - return $yaml; - } + private function lexInlineStructure(int &$cursor, string $closingTag): string + { + $value = $this->currentLine[$cursor]; + ++$cursor; - $value = $yaml; + do { + $this->consumeWhitespaces($cursor); - while ($this->moveToNextLine()) { - for ($i = 1; isset($this->currentLine[$i]) && ']' !== $this->currentLine[$i]; ++$i) { + while (isset($this->currentLine[$cursor])) { + 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]) { - continue; + throw new ParseException('Malformed inline YAML string'); + } + + 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 (isset($this->currentLine[$i]) && ']' === $this->currentLine[$i]) { - break; + if ($this->hasMoreLines()) { + $cursor = 0; } - } + } while ($this->moveToNextLine()); - return $value; + return 0 < $whitespacesConsumed; } } diff --git a/src/Symfony/Component/Yaml/Tests/ParserTest.php b/src/Symfony/Component/Yaml/Tests/ParserTest.php index 5f3b8f311d..f9e4765a5d 100644 --- a/src/Symfony/Component/Yaml/Tests/ParserTest.php +++ b/src/Symfony/Component/Yaml/Tests/ParserTest.php @@ -1660,6 +1660,16 @@ EOF; 'foo': 'bar', 'bar': 'baz' } +YAML + , + ], + 'mapping with unquoted strings and values' => [ + ['foo' => 'bar', 'bar' => 'baz'], + << [ + ['foo', 'bar'], + << [ + [ + 'foo' => [ + 'bar' => 'foobar', + ], + ], + << [ + [ + 'foo', + [ + 'bar', + 'baz', + ], + ], + << [ + [ + ['entry1', []], + ['entry2'], + ], + << [ ['foo' => ['bar', 'foobar'], 'bar' => ['baz']], << [ + [ + 'foobar' => [ + 'foo', + 'bar', + ], + 'bar' => 'baz', + ], + << [ [ 'foo' => [ @@ -1824,6 +1897,110 @@ YAML foo: 'bar baz' +YAML + ], + 'mixed mapping with inline notation having separated lines' => [ + [ + 'map' => [ + 'key' => 'value', + 'a' => 'b', + ], + 'param' => 'some', + ], + << [ + [ + 'map' => [ + 'key' => 'value', + 'a' => 'b', + ], + 'param' => 'some', + ], + << [ + [ + 'map' => [ + 'key' => 'value', + 'a' => 'b', + ], + 'param' => 'some', + ], + << [ + [ + [']'], + ['}'], + ['ba[r'], + ['[ba]r'], + ['bar]'], + ['foo' => 'bar{'], + ['foo' => 'b{ar}'], + ['foo' => 'bar}'], + ], + << [ + [ + ['te"st'], + ['test'], + ["te'st"], + ['te"st]'], + ['te"st'], + ['test'], + ["te'st"], + ['te"st]'], + ], + <<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 */ From 0c92bc5a8324d5c0a0e247b5f4b8fa90551362b5 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 19 Nov 2020 01:16:02 +0100 Subject: [PATCH 2/3] [HttpClient] don't fallback to HTTP/1.1 when HTTP/2 streams break --- src/Symfony/Component/HttpClient/Response/CurlResponse.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Symfony/Component/HttpClient/Response/CurlResponse.php b/src/Symfony/Component/HttpClient/Response/CurlResponse.php index 435f6402c7..9709a189f5 100644 --- a/src/Symfony/Component/HttpClient/Response/CurlResponse.php +++ b/src/Symfony/Component/HttpClient/Response/CurlResponse.php @@ -283,10 +283,7 @@ final class CurlResponse implements ResponseInterface curl_multi_remove_handle($multi->handle, $ch); $waitFor[1] = (string) ((int) $waitFor[1] - 1); // decrement the retry counter curl_setopt($ch, \CURLOPT_PRIVATE, $waitFor); - - if ('1' === $waitFor[1]) { - curl_setopt($ch, \CURLOPT_HTTP_VERSION, \CURL_HTTP_VERSION_1_1); - } + curl_setopt($ch, \CURLOPT_FORBID_REUSE, true); if (0 === curl_multi_add_handle($multi->handle, $ch)) { continue; From fcbf0bf76ea165cb66c7a9d4b84bd4441994d8be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Deruss=C3=A9?= Date: Fri, 20 Nov 2020 22:20:21 +0100 Subject: [PATCH 3/3] Display debug info --- .github/workflows/tests.yml | 12 ++++++------ .../Cache/Adapter/CouchbaseBucketAdapter.php | 6 +++--- .../Tests/Adapter/CouchbaseBucketAdapterTest.php | 7 ++++++- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1387e5ac9c..b9f5438e2e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -93,17 +93,12 @@ jobs: - name: Install system dependencies run: | - echo "::group::add apt sources" - sudo wget -O - http://packages.couchbase.com/ubuntu/couchbase.key | sudo apt-key add - - echo "deb http://packages.couchbase.com/ubuntu bionic bionic/main" | sudo tee /etc/apt/sources.list.d/couchbase.list - echo "::endgroup::" - echo "::group::apt-get update" sudo apt-get update echo "::endgroup::" echo "::group::install tools & libraries" - sudo apt-get install libcouchbase-dev librdkafka-dev + sudo apt-get install librdkafka-dev echo "::endgroup::" - name: Configure Couchbase @@ -122,6 +117,11 @@ jobs: php-version: "${{ matrix.php }}" tools: pecl + - name: Display versions + run: | + php -r 'foreach (get_loaded_extensions() as $extension) echo $extension . " " . phpversion($extension) . PHP_EOL;' + php -i + - name: Load fixtures uses: docker://bitnami/openldap with: diff --git a/src/Symfony/Component/Cache/Adapter/CouchbaseBucketAdapter.php b/src/Symfony/Component/Cache/Adapter/CouchbaseBucketAdapter.php index a0e8f40271..36667f2a0d 100644 --- a/src/Symfony/Component/Cache/Adapter/CouchbaseBucketAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/CouchbaseBucketAdapter.php @@ -42,7 +42,7 @@ class CouchbaseBucketAdapter extends AbstractAdapter public function __construct(\CouchbaseBucket $bucket, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null) { if (!static::isSupported()) { - throw new CacheException('Couchbase >= 2.6.0 is required.'); + throw new CacheException('Couchbase >= 2.6.0 < 3.0.0 is required.'); } $this->maxIdLength = static::MAX_KEY_LENGTH; @@ -66,7 +66,7 @@ class CouchbaseBucketAdapter extends AbstractAdapter } if (!static::isSupported()) { - throw new CacheException('Couchbase >= 2.6.0 is required.'); + throw new CacheException('Couchbase >= 2.6.0 < 3.0.0 is required.'); } set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); }); @@ -125,7 +125,7 @@ class CouchbaseBucketAdapter extends AbstractAdapter public static function isSupported(): bool { - return \extension_loaded('couchbase') && version_compare(phpversion('couchbase'), '2.6.0', '>='); + return \extension_loaded('couchbase') && version_compare(phpversion('couchbase'), '2.6.0', '>=') && version_compare(phpversion('couchbase'), '3.0', '<'); } private static function getOptions(string $options): array diff --git a/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseBucketAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseBucketAdapterTest.php index 120d0d94c0..c72d6710f2 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseBucketAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseBucketAdapterTest.php @@ -16,7 +16,8 @@ use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\Adapter\CouchbaseBucketAdapter; /** - * @requires extension couchbase 2.6.0 + * @requires extension couchbase <3.0.0 + * @requires extension couchbase >=2.6.0 * @group integration * * @author Antonio Jose Cerezo Aranda @@ -32,6 +33,10 @@ class CouchbaseBucketAdapterTest extends AdapterTestCase public static function setupBeforeClass(): void { + if (!CouchbaseBucketAdapter::isSupported()) { + self::markTestSkipped('Couchbase >= 2.6.0 < 3.0.0 is required.'); + } + self::$client = AbstractAdapter::createConnection('couchbase://'.getenv('COUCHBASE_HOST').'/cache', ['username' => getenv('COUCHBASE_USER'), 'password' => getenv('COUCHBASE_PASS')] );