From 434aa7258b727b57e7d8b2d5907099238764dfd2 Mon Sep 17 00:00:00 2001 From: Michael Beck Date: Fri, 16 Oct 2026 05:00:54 -0400 Subject: [PATCH] chore(xoops_lib): update symfony/var-dumper and symfony/yaml to 7.4.21 - symfony/var-dumper v7.4.18 => v7.4.21 - symfony/yaml v7.4.20 => v7.4.21 Refreshes composer.dist.lock and the bundled vendor tree, and records the new versions in the 2.7.4 changelog entry. --- docs/changelog.270.txt | 2 +- htdocs/xoops_lib/composer.dist.lock | 24 +++--- .../xoops_lib/vendor/composer/installed.json | 28 +++---- .../xoops_lib/vendor/composer/installed.php | 16 ++-- .../var-dumper/Caster/ExceptionCaster.php | 5 +- .../xoops_lib/vendor/symfony/yaml/Dumper.php | 75 ++++++++++--------- .../xoops_lib/vendor/symfony/yaml/Escaper.php | 2 +- .../xoops_lib/vendor/symfony/yaml/Inline.php | 38 ++++++++-- .../xoops_lib/vendor/symfony/yaml/Parser.php | 48 +++++++----- 9 files changed, 134 insertions(+), 104 deletions(-) diff --git a/docs/changelog.270.txt b/docs/changelog.270.txt index 9cff7db27..afcf49e91 100644 --- a/docs/changelog.270.txt +++ b/docs/changelog.270.txt @@ -15,7 +15,7 @@ Bug fixes: Libraries and assets: - update the bundled XMF library to 1.3.3: the proxy_env header is only honoured when the direct peer is listed in the new $xoopsConfig['proxy_trusted'] (addresses or CIDR ranges, documented in xoops_data/configs/xoopsconfig.dist.php) — sites that set proxy_env must add proxy_trusted, until then the peer address is used; Forwarded headers are parsed per RFC 7239; Xmf\Mail\SendmailRunner has a delivery timeout; Xmf\Database\Tables keeps CURRENT_TIMESTAMP defaults and refuses tables with functional index parts; Xmf\Security\Serializer accepts every legacy payload; Xmf\Jwt\TokenReader accepts Bearer tokens on custom headers again (xoops_lib/composer.dist.json, xoops_lib/vendor/xoops/xmf) -- HTML Purifier 4.19.1, firebase/php-jwt 7.2.1, Monolog 3.12.1, Punic 3.8.2, Smarty 4.5.8, symfony/polyfill-mbstring 1.43.0, symfony/var-dumper 7.4.18, symfony/yaml 7.4.20, TCPDF 6.11.4, webmozart/assert 2.5.0 (xoops_lib/composer.dist.lock, xoops_lib/vendor) (#265) +- HTML Purifier 4.19.1, firebase/php-jwt 7.2.1, Monolog 3.12.1, Punic 3.8.2, Smarty 4.5.8, symfony/polyfill-mbstring 1.43.0, symfony/var-dumper 7.4.21, symfony/yaml 7.4.21, TCPDF 6.11.4, webmozart/assert 2.5.0 (xoops_lib/composer.dist.lock, xoops_lib/vendor) (#265) Tooling and CI: - Qodana (jetbrains/qodana-php 2026.2, qodana.recommended profile) scans pull requests and master again, as it does on XoopsCore26; the scan installs xoops_lib/composer.dist.lock inside the linter container and skips the vendor trees, the installer, xoops_data, extras and tests; it runs only when the QODANA_TOKEN repository secret is set (qodana.yaml, .github/workflows/qodana.yml) (mamba) diff --git a/htdocs/xoops_lib/composer.dist.lock b/htdocs/xoops_lib/composer.dist.lock index b6d29c33f..bfd006469 100644 --- a/htdocs/xoops_lib/composer.dist.lock +++ b/htdocs/xoops_lib/composer.dist.lock @@ -1312,16 +1312,16 @@ }, { "name": "symfony/var-dumper", - "version": "v7.4.18", + "version": "v7.4.21", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "e088da50b813f32473a76871616cbb8fa54653a8" + "reference": "55d06b02f49eca9bff61828548988e6f8763e8a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/e088da50b813f32473a76871616cbb8fa54653a8", - "reference": "e088da50b813f32473a76871616cbb8fa54653a8", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/55d06b02f49eca9bff61828548988e6f8763e8a6", + "reference": "55d06b02f49eca9bff61828548988e6f8763e8a6", "shasum": "" }, "require": { @@ -1375,7 +1375,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.4.18" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.21" }, "funding": [ { @@ -1395,20 +1395,20 @@ "type": "tidelift" } ], - "time": "2026-08-30T20:10:52+00:00" + "time": "2026-10-13T05:48:33+00:00" }, { "name": "symfony/yaml", - "version": "v7.4.20", + "version": "v7.4.21", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "0222bf4dfaa1e3ceb8fff579635d655f2950d5a7" + "reference": "044907929c7999814378be22da34722c7a86bad4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/0222bf4dfaa1e3ceb8fff579635d655f2950d5a7", - "reference": "0222bf4dfaa1e3ceb8fff579635d655f2950d5a7", + "url": "https://api.github.com/repos/symfony/yaml/zipball/044907929c7999814378be22da34722c7a86bad4", + "reference": "044907929c7999814378be22da34722c7a86bad4", "shasum": "" }, "require": { @@ -1451,7 +1451,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.4.20" + "source": "https://github.com/symfony/yaml/tree/v7.4.21" }, "funding": [ { @@ -1471,7 +1471,7 @@ "type": "tidelift" } ], - "time": "2026-09-26T06:31:10+00:00" + "time": "2026-10-12T12:34:30+00:00" }, { "name": "tecnickcom/tcpdf", diff --git a/htdocs/xoops_lib/vendor/composer/installed.json b/htdocs/xoops_lib/vendor/composer/installed.json index eac3dcc7a..daa59b8ce 100644 --- a/htdocs/xoops_lib/vendor/composer/installed.json +++ b/htdocs/xoops_lib/vendor/composer/installed.json @@ -1360,17 +1360,17 @@ }, { "name": "symfony/var-dumper", - "version": "v7.4.18", - "version_normalized": "7.4.18.0", + "version": "v7.4.21", + "version_normalized": "7.4.21.0", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "e088da50b813f32473a76871616cbb8fa54653a8" + "reference": "55d06b02f49eca9bff61828548988e6f8763e8a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/e088da50b813f32473a76871616cbb8fa54653a8", - "reference": "e088da50b813f32473a76871616cbb8fa54653a8", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/55d06b02f49eca9bff61828548988e6f8763e8a6", + "reference": "55d06b02f49eca9bff61828548988e6f8763e8a6", "shasum": "" }, "require": { @@ -1388,7 +1388,7 @@ "symfony/uid": "^6.4|^7.0|^8.0", "twig/twig": "^3.12|^4.0" }, - "time": "2026-08-30T20:10:52+00:00", + "time": "2026-10-13T05:48:33+00:00", "bin": [ "Resources/bin/var-dump-server" ], @@ -1426,7 +1426,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.4.18" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.21" }, "funding": [ { @@ -1450,17 +1450,17 @@ }, { "name": "symfony/yaml", - "version": "v7.4.20", - "version_normalized": "7.4.20.0", + "version": "v7.4.21", + "version_normalized": "7.4.21.0", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "0222bf4dfaa1e3ceb8fff579635d655f2950d5a7" + "reference": "044907929c7999814378be22da34722c7a86bad4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/0222bf4dfaa1e3ceb8fff579635d655f2950d5a7", - "reference": "0222bf4dfaa1e3ceb8fff579635d655f2950d5a7", + "url": "https://api.github.com/repos/symfony/yaml/zipball/044907929c7999814378be22da34722c7a86bad4", + "reference": "044907929c7999814378be22da34722c7a86bad4", "shasum": "" }, "require": { @@ -1474,7 +1474,7 @@ "require-dev": { "symfony/console": "^6.4|^7.0|^8.0" }, - "time": "2026-09-26T06:31:10+00:00", + "time": "2026-10-12T12:34:30+00:00", "bin": [ "Resources/bin/yaml-lint" ], @@ -1505,7 +1505,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.4.20" + "source": "https://github.com/symfony/yaml/tree/v7.4.21" }, "funding": [ { diff --git a/htdocs/xoops_lib/vendor/composer/installed.php b/htdocs/xoops_lib/vendor/composer/installed.php index 79a9e69c0..de97e9e9a 100644 --- a/htdocs/xoops_lib/vendor/composer/installed.php +++ b/htdocs/xoops_lib/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'xoopscore27/libraries', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '3b9bf84a8f6fe8b170148626b32942afc9088d0b', + 'reference' => 'cfbac97abb8e5bb981a588b3ca6918bd99209fe4', 'type' => 'metapackage', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -197,18 +197,18 @@ 'dev_requirement' => false, ), 'symfony/var-dumper' => array( - 'pretty_version' => 'v7.4.18', - 'version' => '7.4.18.0', - 'reference' => 'e088da50b813f32473a76871616cbb8fa54653a8', + 'pretty_version' => 'v7.4.21', + 'version' => '7.4.21.0', + 'reference' => '55d06b02f49eca9bff61828548988e6f8763e8a6', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/var-dumper', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/yaml' => array( - 'pretty_version' => 'v7.4.20', - 'version' => '7.4.20.0', - 'reference' => '0222bf4dfaa1e3ceb8fff579635d655f2950d5a7', + 'pretty_version' => 'v7.4.21', + 'version' => '7.4.21.0', + 'reference' => '044907929c7999814378be22da34722c7a86bad4', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/yaml', 'aliases' => array(), @@ -271,7 +271,7 @@ 'xoopscore27/libraries' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '3b9bf84a8f6fe8b170148626b32942afc9088d0b', + 'reference' => 'cfbac97abb8e5bb981a588b3ca6918bd99209fe4', 'type' => 'metapackage', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), diff --git a/htdocs/xoops_lib/vendor/symfony/var-dumper/Caster/ExceptionCaster.php b/htdocs/xoops_lib/vendor/symfony/var-dumper/Caster/ExceptionCaster.php index 30b1a995e..9726a99ed 100644 --- a/htdocs/xoops_lib/vendor/symfony/var-dumper/Caster/ExceptionCaster.php +++ b/htdocs/xoops_lib/vendor/symfony/var-dumper/Caster/ExceptionCaster.php @@ -196,10 +196,7 @@ public static function castFrameStub(FrameStub $frame, array $a, Stub $stub, boo $prefix = Caster::PREFIX_VIRTUAL; if (isset($f['file'], $f['line'])) { - $cacheKey = $f; - unset($cacheKey['object'], $cacheKey['args']); - $cacheKey[] = self::$srcContext; - $cacheKey = implode('-', $cacheKey); + $cacheKey = "{$f['file']}-{$f['line']}-".($f['class'] ?? '').'-'.($f['type'] ?? '').'-'.($f['function'] ?? '').'-'.self::$srcContext; if (isset(self::$framesCache[$cacheKey])) { $a[$prefix.'src'] = self::$framesCache[$cacheKey]; diff --git a/htdocs/xoops_lib/vendor/symfony/yaml/Dumper.php b/htdocs/xoops_lib/vendor/symfony/yaml/Dumper.php index cd5a1f6cb..4ba2c38dc 100644 --- a/htdocs/xoops_lib/vendor/symfony/yaml/Dumper.php +++ b/htdocs/xoops_lib/vendor/symfony/yaml/Dumper.php @@ -77,25 +77,8 @@ private function doDump(mixed $input, int $inline = 0, int $indent = 0, int $fla } if (Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value) && str_contains($value, "\n") && !str_contains($value, "\r")) { - $blockIndentationIndicator = $this->getBlockIndentationIndicator($value); - - if (isset($value[-2]) && "\n" === $value[-2] && "\n" === $value[-1]) { - $blockChompingIndicator = '+'; - } elseif ("\n" === $value[-1]) { - $blockChompingIndicator = ''; - } else { - $blockChompingIndicator = '-'; - } - - $output .= \sprintf('%s%s%s |%s%s', $prefix, $dumpAsMap ? Inline::dump($key, $flags).':' : '-', '', $blockIndentationIndicator, $blockChompingIndicator); - - foreach (explode("\n", $value) as $row) { - if ('' === $row) { - $output .= "\n"; - } else { - $output .= \sprintf("\n%s%s%s", $prefix, str_repeat(' ', $this->indentation), $row); - } - } + $output .= \sprintf('%s%s%s |%s%s', $prefix, $dumpAsMap ? Inline::dump($key, $flags).':' : '-', '', $this->getBlockIndentationIndicator($value), $this->getBlockChompingIndicator($value)); + $output .= $this->dumpBlockScalarLines($value, $prefix); continue; } @@ -103,13 +86,9 @@ private function doDump(mixed $input, int $inline = 0, int $indent = 0, int $fla if ($value instanceof TaggedValue) { $output .= \sprintf('%s%s !%s', $prefix, $dumpAsMap ? Inline::dump($key, $flags).':' : '-', $value->getTag()); - if (Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value->getValue()) && str_contains($value->getValue(), "\n") && !str_contains($value->getValue(), "\r\n")) { - $blockIndentationIndicator = $this->getBlockIndentationIndicator($value->getValue()); - $output .= \sprintf(' |%s', $blockIndentationIndicator); - - foreach (explode("\n", $value->getValue()) as $row) { - $output .= \sprintf("\n%s%s%s", $prefix, str_repeat(' ', $this->indentation), $row); - } + if (Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value->getValue()) && str_contains($value->getValue(), "\n") && !str_contains($value->getValue(), "\r")) { + $output .= \sprintf(' |%s%s', $this->getBlockIndentationIndicator($value->getValue()), $this->getBlockChompingIndicator($value->getValue())); + $output .= $this->dumpBlockScalarLines($value->getValue(), $prefix); continue; } @@ -148,13 +127,9 @@ private function dumpTaggedValue(TaggedValue $value, int $inline, int $indent, i { $output = \sprintf('%s!%s', $prefix ? $prefix.' ' : '', $value->getTag()); - if (Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value->getValue()) && str_contains($value->getValue(), "\n") && !str_contains($value->getValue(), "\r\n")) { - $blockIndentationIndicator = $this->getBlockIndentationIndicator($value->getValue()); - $output .= \sprintf(' |%s', $blockIndentationIndicator); - - foreach (explode("\n", $value->getValue()) as $row) { - $output .= \sprintf("\n%s%s%s", $prefix, str_repeat(' ', $this->indentation), $row); - } + if (Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value->getValue()) && str_contains($value->getValue(), "\n") && !str_contains($value->getValue(), "\r")) { + $output .= \sprintf(' |%s%s', $this->getBlockIndentationIndicator($value->getValue()), $this->getBlockChompingIndicator($value->getValue())); + $output .= $this->dumpBlockScalarLines($value->getValue(), $prefix); return $output; } @@ -166,15 +141,43 @@ private function dumpTaggedValue(TaggedValue $value, int $inline, int $indent, i return $output."\n".$this->doDump($value->getValue(), $inline - 1, $indent, $flags, $nestingLevel + 1); } + private function getBlockChompingIndicator(string $value): string + { + // a block made of line breaks only has no content, so only the keep indicator preserves them + if ("\n" === $value || isset($value[-2]) && "\n" === $value[-2] && "\n" === $value[-1]) { + return '+'; + } + + if ("\n" === $value[-1]) { + return ''; + } + + return '-'; + } + + private function dumpBlockScalarLines(string $value, string $prefix): string + { + $output = ''; + + foreach (explode("\n", $value) as $row) { + if ('' === $row) { + $output .= "\n"; + } else { + $output .= \sprintf("\n%s%s%s", $prefix, str_repeat(' ', $this->indentation), $row); + } + } + + return $output; + } + private function getBlockIndentationIndicator(string $value): string { $lines = explode("\n", $value); - // If the first line (that is neither empty nor contains only spaces) - // starts with a space character, the spec requires a block indentation indicator + // The spec requires a block indentation indicator when the first line that is not empty starts with a space, even if that line holds only spaces // http://www.yaml.org/spec/1.2/spec.html#id2793979 foreach ($lines as $line) { - if ('' !== trim($line, ' ')) { + if ('' !== $line) { return str_starts_with($line, ' ') ? (string) $this->indentation : ''; } } diff --git a/htdocs/xoops_lib/vendor/symfony/yaml/Escaper.php b/htdocs/xoops_lib/vendor/symfony/yaml/Escaper.php index 921d62ffa..fcc8334be 100644 --- a/htdocs/xoops_lib/vendor/symfony/yaml/Escaper.php +++ b/htdocs/xoops_lib/vendor/symfony/yaml/Escaper.php @@ -76,7 +76,7 @@ public static function requiresSingleQuoting(string $value): bool { // Determines if a PHP value is entirely composed of a value that would // require single quoting in YAML. - if (\in_array(strtolower($value), ['null', '~', 'true', 'false', 'y', 'n', 'yes', 'no', 'on', 'off'], true)) { + if (\in_array(strtolower($value), ['null', '~', 'true', 'false', 'y', 'n', 'yes', 'no', 'on', 'off', '.inf', '.nan', '...'], true)) { return true; } diff --git a/htdocs/xoops_lib/vendor/symfony/yaml/Inline.php b/htdocs/xoops_lib/vendor/symfony/yaml/Inline.php index 3c24c20f7..828dbe44e 100644 --- a/htdocs/xoops_lib/vendor/symfony/yaml/Inline.php +++ b/htdocs/xoops_lib/vendor/symfony/yaml/Inline.php @@ -198,7 +198,7 @@ public static function dump(mixed $value, int $flags = 0, bool $rootLevel = fals $doubleQuoted = Escaper::escapeWithDoubleQuotes($value); return \strlen($doubleQuoted) < \strlen($singleQuoted) ? $doubleQuoted : $singleQuoted; - case Parser::preg_match('{^[0-9]+[_0-9]*$}', $value): + case Parser::preg_match('{^[+-]?[0-9][0-9_]*(\.[0-9_]+)?$}', $value): case Parser::preg_match('{^[+-]?0o[0-7_]++$}', $value): case Parser::preg_match(self::getHexRegex(), $value): case Parser::preg_match(self::getTimestampRegex(), $value): @@ -529,6 +529,10 @@ private static function parseMapping(ParserState $state, string $mapping, int $f $key = self::evaluateScalar($state, $key, $flags); } + if (self::$objectForMap && \is_string($key) && str_starts_with($key, "\0")) { + throw new ParseException('Mapping keys starting with a NUL byte are not supported with the "Yaml::PARSE_OBJECT_FOR_MAP" flag.', self::$parsedLineNumber + 1, $mapping); + } + if (false === $i = strpos($mapping, ':', $i)) { break; } @@ -545,7 +549,9 @@ private static function parseMapping(ParserState $state, string $mapping, int $f throw new ParseException('Colons must be followed by a space or an indication character (i.e. " ", ",", "[", "]", "{", "}").', self::$parsedLineNumber + 1, $mapping); } - if ('<<' === $key) { + $isMergeKey = !$isKeyQuoted && '<<' === $key; + + if ($isMergeKey) { $allowOverwrite = true; } @@ -565,7 +571,7 @@ private static function parseMapping(ParserState $state, string $mapping, int $f if (null !== $anchorRef && (!isset($mapping[$i]) || \in_array($mapping[$i], [',', '}', "\n"], true))) { $value = null; $references[$anchorRef] = $value; - if ('<<' !== $key) { + if (!$isMergeKey) { if ($allowOverwrite || !isset($output[$key])) { $output[$key] = null !== $tag ? new TaggedValue($tag, $value) : $value; } elseif (isset($output[$key])) { @@ -590,8 +596,16 @@ private static function parseMapping(ParserState $state, string $mapping, int $f // Parser cannot abort this mapping earlier, since lines // are processed sequentially. // But overwriting is allowed when a merge node is used in current block. - if ('<<' === $key) { + if ($isMergeKey) { foreach ($value as $parsedValue) { + if (self::$objectForMap && $parsedValue instanceof \stdClass) { + $parsedValue = (array) $parsedValue; + } + + if (!\is_array($parsedValue)) { + throw new ParseException('Merge items must be arrays.', self::$parsedLineNumber + 1, $mapping); + } + $output += $parsedValue; } } elseif ($allowOverwrite || !isset($output[$key])) { @@ -614,8 +628,8 @@ private static function parseMapping(ParserState $state, string $mapping, int $f // Parser cannot abort this mapping earlier, since lines // are processed sequentially. // But overwriting is allowed when a merge node is used in current block. - if ('<<' === $key) { - $output += $value; + if ($isMergeKey) { + $output += (array) $value; } elseif ($allowOverwrite || !isset($output[$key])) { if (null !== $tag) { $output[$key] = new TaggedValue($tag, $value); @@ -635,7 +649,15 @@ private static function parseMapping(ParserState $state, string $mapping, int $f // Parser cannot abort this mapping earlier, since lines // are processed sequentially. // But overwriting is allowed when a merge node is used in current block. - if ('<<' === $key) { + if ($isMergeKey) { + if (self::$objectForMap && $value instanceof \stdClass) { + $value = (array) $value; + } + + if (!\is_array($value)) { + throw new ParseException('YAML merge keys used with a scalar value instead of an array.', self::$parsedLineNumber + 1, $mapping); + } + $output += $value; } elseif ($allowOverwrite || !isset($output[$key])) { if (null !== $tag) { @@ -853,7 +875,7 @@ private static function evaluateScalar(ParserState $state, string $scalar, int $ } if ('' !== rtrim($time->format('u'), '0')) { - return (float) $time->format('U.u'); + return $time->format('U') + $time->format('u') / 1e6; } try { diff --git a/htdocs/xoops_lib/vendor/symfony/yaml/Parser.php b/htdocs/xoops_lib/vendor/symfony/yaml/Parser.php index c35b18187..5f7fd1ca6 100644 --- a/htdocs/xoops_lib/vendor/symfony/yaml/Parser.php +++ b/htdocs/xoops_lib/vendor/symfony/yaml/Parser.php @@ -273,12 +273,19 @@ private function doParse(string $value, int $flags): mixed throw new ParseException((is_numeric($key) ? 'Numeric' : 'Non-string').' keys are not supported. Quote your evaluable mapping keys instead.', $this->getRealCurrentLineNb() + 1, $this->currentLine); } + if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && \is_string($key) && str_starts_with($key, "\0")) { + throw new ParseException('Mapping keys starting with a NUL byte are not supported with the "Yaml::PARSE_OBJECT_FOR_MAP" flag.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); + } + // Convert float keys to strings, to avoid being converted to integers by PHP if (\is_float($key)) { $key = (string) $key; } - if ('<<' === $key && (!isset($values['value']) || '&' !== $values['value'][0] || !self::preg_match('#^&(?P[^ ]+)#u', $values['value'], $refMatches))) { + // Only a plain (unquoted) "<<" key is a merge key + $isMergeKey = '<<' === rtrim($values['key']); + + if ($isMergeKey && (!isset($values['value']) || '&' !== $values['value'][0] || !self::preg_match('#^&(?P[^ ]+)#u', $values['value'], $refMatches))) { $mergeNode = true; $allowOverwrite = true; if (isset($values['value'][0]) && '*' === $values['value'][0]) { @@ -349,7 +356,7 @@ private function doParse(string $value, int $flags): mixed $data += $parsed; // array union } } - } elseif ('<<' !== $key && isset($values['value']) && '&' === $values['value'][0] && self::preg_match(self::REFERENCE_PATTERN, $values['value'], $matches)) { + } elseif (!$isMergeKey && isset($values['value']) && '&' === $values['value'][0] && self::preg_match(self::REFERENCE_PATTERN, $values['value'], $matches)) { $isRef = $matches['ref']; $this->refsBeingParsed[] = $isRef; $values['value'] = $matches['value']; @@ -358,7 +365,7 @@ private function doParse(string $value, int $flags): mixed $subTag = null; if ($mergeNode) { // Merge keys - } elseif (!isset($values['value']) || '' === $values['value'] || str_starts_with($values['value'], '#') || (null !== $subTag = $this->getLineTag($values['value'], $flags)) || '<<' === $key) { + } elseif (!isset($values['value']) || '' === $values['value'] || str_starts_with($values['value'], '#') || (null !== $subTag = $this->getLineTag($values['value'], $flags)) || $isMergeKey) { // hash // if next line is less indented or equal, then it means that the current value is null if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) { @@ -381,7 +388,7 @@ private function doParse(string $value, int $flags): mixed // remember the parsed line number here in case we need it to provide some contexts in error messages below $realCurrentLineNbKey = $this->getRealCurrentLineNb(); $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $flags); - if ('<<' === $key) { + if ($isMergeKey) { $this->refs[$refMatches['ref']] = $value; if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $value instanceof \stdClass) { @@ -905,12 +912,9 @@ private function parseBlockScalar(string $style, string $chomping = '', int $ind $isCurrentLineBlank = $this->isCurrentLineBlank(); $blockLines = []; - $longestBlankLine = 0; // leading blank lines are consumed before determining indentation while ($notEOF && $isCurrentLineBlank && (!$indentation || \strlen($this->currentLine) <= $indentation)) { - $longestBlankLine = max($longestBlankLine, \strlen($this->currentLine)); - // newline only if not EOF if ($notEOF = $this->moveToNextLine()) { $blockLines[] = ''; @@ -925,11 +929,6 @@ private function parseBlockScalar(string $style, string $chomping = '', int $ind for ($i = 0; $i < $currentLineLength && ' ' === $this->currentLine[$i]; ++$i) { ++$indentation; } - - // a comment less indented than the leading empty lines is not content, the indentation is then the one of the longest empty line - if ($indentation < $longestBlankLine && $this->isCurrentLineComment()) { - $indentation = $longestBlankLine; - } } if (!$notEOF || !$indentation || strspn($this->currentLine, ' ') < $indentation) { @@ -1249,22 +1248,26 @@ private function lexInlineQuotedString(int &$cursor = 0): string do { if (++$lineNumber > 1) { - $cursor += strspn($this->currentLine, ' ', $cursor); + $cursor += strspn($this->currentLine, " \t", $cursor); } - if ($this->isCurrentLineBlank()) { + if ($isBlankLine = '' === trim($this->currentLine, " \t")) { $value .= "\n"; } elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) { $value .= ' '; } + $line = ''; + $escapedLength = 0; + for (; \strlen($this->currentLine) > $cursor; ++$cursor) { switch ($this->currentLine[$cursor]) { case '\\': if ("'" === $quotation) { - $value .= '\\'; + $line .= '\\'; } elseif (isset($this->currentLine[++$cursor])) { - $value .= '\\'.$this->currentLine[$cursor]; + $line .= '\\'.$this->currentLine[$cursor]; + $escapedLength = \strlen($line); } break; @@ -1272,17 +1275,17 @@ private function lexInlineQuotedString(int &$cursor = 0): string ++$cursor; if ("'" === $quotation && isset($this->currentLine[$cursor]) && "'" === $this->currentLine[$cursor]) { - $value .= "''"; + $line .= "''"; break; } - return $value.$quotation; + return $value.$line.$quotation; default: - $value .= $this->currentLine[$cursor]; + $line .= $this->currentLine[$cursor]; } } - if ($this->isCurrentLineBlank()) { + if ($isBlankLine) { $previousLineWasNewline = true; $previousLineWasTerminatedWithBackslash = false; } elseif ('"' === $quotation && 1 === (\strlen($this->currentLine) - \strlen(rtrim($this->currentLine, '\\'))) % 2) { @@ -1291,8 +1294,13 @@ private function lexInlineQuotedString(int &$cursor = 0): string } else { $previousLineWasNewline = false; $previousLineWasTerminatedWithBackslash = false; + + // trailing white space is removed when the line is folded, unless it is escaped + $line = substr($line, 0, $escapedLength).rtrim(substr($line, $escapedLength), " \t"); } + $value .= $line; + if ($this->hasMoreLines()) { $cursor = 0; }