diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f24caa613b..a5f1eb8c84 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -99,17 +99,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 @@ -128,6 +123,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')] ); diff --git a/src/Symfony/Component/HttpClient/Response/CurlResponse.php b/src/Symfony/Component/HttpClient/Response/CurlResponse.php index f0370e9c00..1812bf9c70 100644 --- a/src/Symfony/Component/HttpClient/Response/CurlResponse.php +++ b/src/Symfony/Component/HttpClient/Response/CurlResponse.php @@ -306,10 +306,7 @@ final class CurlResponse implements ResponseInterface, StreamableInterface 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; diff --git a/src/Symfony/Component/Yaml/Parser.php b/src/Symfony/Component/Yaml/Parser.php index f98fcd6979..83d80adb53 100644 --- a/src/Symfony/Component/Yaml/Parser.php +++ b/src/Symfony/Component/Yaml/Parser.php @@ -358,7 +358,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); @@ -371,7 +371,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()) { @@ -392,7 +392,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()) { @@ -667,6 +667,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. */ @@ -744,9 +749,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; @@ -1145,107 +1154,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) { - $trimmedLine = trim($lines[$i]); - if ('' === $trimmedLine) { + do { + if ($this->isCurrentLineBlank()) { $value .= "\n"; } elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) { $value .= ' '; } - if ('' !== $trimmedLine && '\\' === $lines[$i][-1]) { - $value .= ltrim(substr($lines[$i], 0, -1)); - } elseif ('' !== $trimmedLine) { - $value .= $trimmedLine; + 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 ('' === $trimmedLine) { + if ($this->isCurrentLineBlank()) { $previousLineWasNewline = true; $previousLineWasTerminatedWithBackslash = false; - } elseif ('\\' === $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 6e8b997313..1bbb70f39a 100644 --- a/src/Symfony/Component/Yaml/Tests/ParserTest.php +++ b/src/Symfony/Component/Yaml/Tests/ParserTest.php @@ -1659,6 +1659,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' => [ @@ -1823,6 +1896,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 */