Skip to content

Commit ba52060

Browse files
[JsonPath] Handle special whitespaces in filters
1 parent 42e5ad5 commit ba52060

File tree

3 files changed

+80
-27
lines changed

3 files changed

+80
-27
lines changed

src/Symfony/Component/JsonPath/JsonCrawler.php

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -122,12 +122,34 @@ private function evaluateName(string $name, mixed $value): array
122122
return \array_key_exists($name, $value) ? [$value[$name]] : [];
123123
}
124124

125+
private function splitByOperator(string $expr, string $operator): array
126+
{
127+
$normalizedExpr = JsonPathUtils::normalizeWhitespace($expr);
128+
$pattern = '/\s*' . preg_quote($operator, '/') . '\s*/';
129+
$parts = preg_split($pattern, $normalizedExpr, 2);
130+
131+
if (2 === count($parts)) {
132+
return [trim($parts[0]), trim($parts[1])];
133+
}
134+
135+
return [];
136+
}
137+
138+
private function containsOperator(string $expr, string $operator): bool
139+
{
140+
$normalizedExpr = JsonPathUtils::normalizeWhitespace($expr);
141+
$pattern = '/\s*' . preg_quote($operator, '/') . '\s*/';
142+
143+
return 1 === preg_match($pattern, $normalizedExpr);
144+
}
145+
125146
private function evaluateBracket(string $expr, mixed $value): array
126147
{
127148
if (!\is_array($value)) {
128149
return [];
129150
}
130151

152+
$expr = JsonPathUtils::normalizeWhitespace($expr);
131153
if ('*' === $expr) {
132154
return array_values($value);
133155
}
@@ -150,8 +172,8 @@ private function evaluateBracket(string $expr, mixed $value): array
150172
}
151173

152174
$result = [];
153-
foreach (explode(',', $expr) as $index) {
154-
$index = (int) trim($index);
175+
foreach (preg_split('/\s*,\s*/', $expr) as $indexStr) {
176+
$index = (int) trim($indexStr);
155177
if ($index < 0) {
156178
$index = \count($value) + $index;
157179
}
@@ -163,13 +185,14 @@ private function evaluateBracket(string $expr, mixed $value): array
163185
return $result;
164186
}
165187

166-
// start, end and step
167-
if (preg_match('/^(-?\d*):(-?\d*)(?::(-?\d+))?$/', $expr, $matches)) {
188+
if (preg_match('/^(-?\d*)\s*:\s*(-?\d*)(?:\s*:\s*(-?\d+))?$/', $expr, $matches)) {
168189
if (!array_is_list($value)) {
169190
return [];
170191
}
171192

172193
$length = \count($value);
194+
$matches = array_map('trim', $matches);
195+
173196
$start = '' !== $matches[1] ? (int) $matches[1] : null;
174197
$end = '' !== $matches[2] ? (int) $matches[2] : null;
175198
$step = isset($matches[3]) && '' !== $matches[3] ? (int) $matches[3] : 1;
@@ -211,8 +234,8 @@ private function evaluateBracket(string $expr, mixed $value): array
211234
}
212235

213236
// filter expressions
214-
if (preg_match('/^\?(.*)$/', $expr, $matches)) {
215-
$filterExpr = $matches[1];
237+
if (preg_match('/^\?\s*(.*)$/', $expr, $matches)) {
238+
$filterExpr = trim($matches[1]);
216239

217240
if (preg_match('/^(\w+)\s*\([^()]*\)\s*([<>=!]+.*)?$/', $filterExpr)) {
218241
$filterExpr = "($filterExpr)";
@@ -260,37 +283,39 @@ private function evaluateFilter(string $expr, mixed $value): array
260283

261284
private function evaluateFilterExpression(string $expr, array $context): bool
262285
{
263-
$expr = trim($expr);
286+
$expr = JsonPathUtils::normalizeWhitespace($expr);
264287

265-
if (str_contains($expr, '&&')) {
266-
$parts = array_map('trim', explode('&&', $expr));
288+
if ($this->containsOperator($expr, '&&')) {
289+
$parts = preg_split('/\s*&&\s*/', $expr);
267290
foreach ($parts as $part) {
268-
if (!$this->evaluateFilterExpression($part, $context)) {
291+
if (!$this->evaluateFilterExpression(trim($part), $context)) {
269292
return false;
270293
}
271294
}
272295

273296
return true;
274297
}
275298

276-
if (str_contains($expr, '||')) {
277-
$parts = array_map('trim', explode('||', $expr));
299+
if ($this->containsOperator($expr, '||')) {
300+
$parts = preg_split('/\s*\|\|\s*/', $expr);
278301
$result = false;
279302
foreach ($parts as $part) {
280-
$result = $result || $this->evaluateFilterExpression($part, $context);
303+
$result = $result || $this->evaluateFilterExpression(trim($part), $context);
281304
}
282305

283306
return $result;
284307
}
285308

286309
$operators = ['!=', '==', '>=', '<=', '>', '<'];
287310
foreach ($operators as $op) {
288-
if (str_contains($expr, $op)) {
289-
[$left, $right] = array_map('trim', explode($op, $expr, 2));
290-
$leftValue = $this->evaluateScalar($left, $context);
291-
$rightValue = $this->evaluateScalar($right, $context);
311+
if ($this->containsOperator($expr, $op)) {
312+
$parts = $this->splitByOperator($expr, $op);
313+
if (2 === count($parts)) {
314+
$leftValue = $this->evaluateScalar($parts[0], $context);
315+
$rightValue = $this->evaluateScalar($parts[1], $context);
292316

293-
return $this->compare($leftValue, $rightValue, $op);
317+
return $this->compare($leftValue, $rightValue, $op);
318+
}
294319
}
295320
}
296321

@@ -301,7 +326,7 @@ private function evaluateFilterExpression(string $expr, array $context): bool
301326
}
302327

303328
// function calls
304-
if (preg_match('/^(\w+)\((.*)\)$/', $expr, $matches)) {
329+
if (preg_match('/^(\w+)\s*\(\s*(.*)\s*\)$/', $expr, $matches)) {
305330
$functionName = $matches[1];
306331
if (!isset(self::RFC9535_FUNCTIONS[$functionName])) {
307332
throw new JsonCrawlerException($expr, \sprintf('invalid function "%s"', $functionName));
@@ -317,6 +342,8 @@ private function evaluateFilterExpression(string $expr, array $context): bool
317342

318343
private function evaluateScalar(string $expr, array $context): mixed
319344
{
345+
$expr = JsonPathUtils::normalizeWhitespace($expr);
346+
320347
if (is_numeric($expr)) {
321348
return str_contains($expr, '.') ? (float) $expr : (int) $expr;
322349
}
@@ -346,7 +373,7 @@ private function evaluateScalar(string $expr, array $context): mixed
346373
}
347374

348375
// function calls
349-
if (preg_match('/^(\w+)\((.*)\)$/', $expr, $matches)) {
376+
if (preg_match('/^(\w+)\s*\(\s*(.*)\s*\)$/', $expr, $matches)) {
350377
$functionName = $matches[1];
351378
if (!isset(self::RFC9535_FUNCTIONS[$functionName])) {
352379
throw new JsonCrawlerException($expr, \sprintf('invalid function "%s"', $functionName));
@@ -360,12 +387,15 @@ private function evaluateScalar(string $expr, array $context): mixed
360387

361388
private function evaluateFunction(string $name, string $args, array $context): mixed
362389
{
363-
$args = array_map(
364-
fn ($arg) => $this->evaluateScalar(trim($arg), $context),
365-
explode(',', $args)
366-
);
390+
$argList = [];
391+
if (trim($args)) {
392+
$argList = array_map(
393+
fn ($arg) => $this->evaluateScalar(trim($arg), $context),
394+
preg_split('/\s*,\s*/', trim($args))
395+
);
396+
}
367397

368-
$value = $args[0] ?? null;
398+
$value = $argList[0] ?? null;
369399

370400
return match ($name) {
371401
'length' => match (true) {
@@ -375,11 +405,11 @@ private function evaluateFunction(string $name, string $args, array $context): m
375405
},
376406
'count' => \is_array($value) ? \count($value) : 0,
377407
'match' => match (true) {
378-
\is_string($value) && \is_string($args[1] ?? null) => (bool) @preg_match(\sprintf('/^%s$/', $args[1]), $value),
408+
\is_string($value) && \is_string($argList[1] ?? null) => (bool) @preg_match(\sprintf('/^%s$/', $argList[1]), $value),
379409
default => false,
380410
},
381411
'search' => match (true) {
382-
\is_string($value) && \is_string($args[1] ?? null) => (bool) @preg_match("/$args[1]/", $value),
412+
\is_string($value) && \is_string($argList[1] ?? null) => (bool) @preg_match("/{$argList[1]}/", $value),
383413
default => false,
384414
},
385415
'value' => $value,

src/Symfony/Component/JsonPath/JsonPathUtils.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,18 @@ public static function findSmallestDeserializableStringAndPath(array $tokens, mi
8585
'tokens' => $remainingTokens,
8686
];
8787
}
88+
89+
/**
90+
* @see https://datatracker.ietf.org/doc/rfc9535/, section 2.1.1
91+
*/
92+
public static function normalizeWhitespace(string $input): string
93+
{
94+
$normalized = strtr($input, [
95+
"\t" => ' ',
96+
"\n" => ' ',
97+
"\r" => ' ',
98+
]);
99+
100+
return trim(preg_replace('/\s+/', ' ', $normalized));
101+
}
88102
}

src/Symfony/Component/JsonPath/Tests/JsonCrawlerTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,15 @@ public function testLengthFunctionWithOuterParentheses()
419419
$this->assertSame('J. R. R. Tolkien', $result[1]['author']);
420420
}
421421

422+
public function testFilterWithSpecialWhitespaces()
423+
{
424+
$result = self::getBookstoreCrawler()->find("$.store.book[?(length\n(@.author\t)>\r\n12)]");
425+
426+
$this->assertCount(2, $result);
427+
$this->assertSame('Herman Melville', $result[0]['author']);
428+
$this->assertSame('J. R. R. Tolkien', $result[1]['author']);
429+
}
430+
422431
public function testCountFunction()
423432
{
424433
$result = self::getBookstoreCrawler()->find('$.store.book[?count(@.extra) != 0]');

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy