Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit2c412e1

Browse files
[JsonPath] Handle special whitespaces in filters
1 parentcf3b527 commit2c412e1

File tree

5 files changed

+143
-33
lines changed

5 files changed

+143
-33
lines changed

‎src/Symfony/Component/JsonPath/JsonCrawler.php‎

Lines changed: 56 additions & 26 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+
privatefunctionsplitByOperator(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+
privatefunctioncontainsOperator(string$expr,string$operator):bool
139+
{
140+
$normalizedExpr = JsonPathUtils::normalizeWhitespace($expr);
141+
$pattern ='/\s*'.preg_quote($operator,'/').'\s*/';
142+
143+
return1 ===preg_match($pattern,$normalizedExpr);
144+
}
145+
125146
privatefunctionevaluateBracket(string$expr,mixed$value):array
126147
{
127148
if (!\is_array($value)) {
128149
return [];
129150
}
130151

152+
$expr = JsonPathUtils::normalizeWhitespace($expr);
131153
if ('*' ===$expr) {
132154
returnarray_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;
@@ -212,7 +235,7 @@ private function evaluateBracket(string $expr, mixed $value): array
212235

213236
// filter expressions
214237
if (preg_match('/^\?(.*)$/',$expr,$matches)) {
215-
$filterExpr =$matches[1];
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
privatefunctionevaluateFilterExpression(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 ($partsas$part) {
268-
if (!$this->evaluateFilterExpression($part,$context)) {
291+
if (!$this->evaluateFilterExpression(trim($part),$context)) {
269292
returnfalse;
270293
}
271294
}
272295

273296
returntrue;
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 ($partsas$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 ($operatorsas$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
thrownewJsonCrawlerException($expr,\sprintf('invalid function "%s"',$functionName));
@@ -317,6 +342,8 @@ private function evaluateFilterExpression(string $expr, array $context): bool
317342

318343
privatefunctionevaluateScalar(string$expr,array$context):mixed
319344
{
345+
$expr = JsonPathUtils::normalizeWhitespace($expr);
346+
320347
if (is_numeric($expr)) {
321348
returnstr_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*\((.*)\)$/',$expr,$matches)) {
350377
$functionName =$matches[1];
351378
if (!isset(self::RFC9535_FUNCTIONS[$functionName])) {
352379
thrownewJsonCrawlerException($expr,\sprintf('invalid function "%s"',$functionName));
@@ -360,12 +387,15 @@ private function evaluateScalar(string $expr, array $context): mixed
360387

361388
privatefunctionevaluateFunction(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
returnmatch ($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
@@ -159,4 +159,18 @@ private static function unescapeUnicodeSequence(string $str, int $length, int &$
159159

160160
returnmb_chr($codepoint,'UTF-8');
161161
}
162+
163+
/**
164+
* @see https://datatracker.ietf.org/doc/rfc9535/, section 2.1.1
165+
*/
166+
publicstaticfunctionnormalizeWhitespace(string$input):string
167+
{
168+
$normalized =strtr($input, [
169+
"\t" =>'',
170+
"\n" =>'',
171+
"\r" =>'',
172+
]);
173+
174+
returntrim(preg_replace('/\s+/','',$normalized));
175+
}
162176
}

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

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

422+
publicfunctiontestFilterWithSpecialWhitespaces()
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+
431+
publicfunctiontestFilterMultiline()
432+
{
433+
$result =self::getBookstoreCrawler()->find(
434+
'$
435+
.store
436+
.book[?
437+
length(@.author)>12
438+
]'
439+
);
440+
441+
$this->assertCount(2,$result);
442+
$this->assertSame('Herman Melville',$result[0]['author']);
443+
$this->assertSame('J. R. R. Tolkien',$result[1]['author']);
444+
}
445+
422446
publicfunctiontestCountFunction()
423447
{
424448
$result =self::getBookstoreCrawler()->find('$.store.book[?count(@.extra) != 0]');

‎src/Symfony/Component/JsonPath/Tests/Tokenizer/JsonPathTokenizerTest.php‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,9 +355,7 @@ public static function provideInvalidUtf8PropertyName(): array
355355
'special char first' => ['#test'],
356356
'start with digit' => ['123test'],
357357
'asterisk' => ['test*test'],
358-
'space not allowed' => [' test'],
359358
'at sign not allowed' => ['@test'],
360-
'start control char' => ["\0test"],
361359
'ending control char' => ["test\xFF\xFA"],
362360
'dash sign' => ['-test'],
363361
];

‎src/Symfony/Component/JsonPath/Tokenizer/JsonPathTokenizer.php‎

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
*/
2222
finalclass JsonPathTokenizer
2323
{
24+
privateconstRFC9535_WHITESPACE_CHARS = ['',"\t","\n","\r"];
25+
2426
/**
2527
* @return JsonPathToken[]
2628
*/
@@ -42,14 +44,26 @@ public static function tokenize(JsonPath $query): array
4244
thrownewInvalidJsonPathException('empty JSONPath expression.');
4345
}
4446

45-
if ('$' !==$chars[0]) {
47+
$i =self::skipWhitespace($chars,0,$length);
48+
if ($i >=$length ||'$' !==$chars[$i]) {
4649
thrownewInvalidJsonPathException('expression must start with $.');
4750
}
4851

4952
for ($i =0;$i <$length; ++$i) {
5053
$char =$chars[$i];
5154
$position =$i;
5255

56+
if (!$inQuote && !$inBracket &&self::isWhitespace($char)) {
57+
if ('' !==$current) {
58+
$tokens[] =newJsonPathToken(TokenType::Name,$current);
59+
$current ='';
60+
}
61+
62+
$i =self::skipWhitespace($chars,$i,$length) -1;// -1 because loop will increment
63+
64+
continue;
65+
}
66+
5367
if (('"' ===$char ||"'" ===$char) && !$inQuote) {
5468
$inQuote =true;
5569
$quoteChar =$char;
@@ -59,7 +73,7 @@ public static function tokenize(JsonPath $query): array
5973

6074
if ($inQuote) {
6175
$current .=$char;
62-
if ($char ===$quoteChar &&'\\' !==$chars[$i -1]) {
76+
if ($char ===$quoteChar &&(0 ===$i ||'\\' !==$chars[$i -1])) {
6377
$inQuote =false;
6478
}
6579
if ($i ===$length -1 &&$inQuote) {
@@ -80,6 +94,8 @@ public static function tokenize(JsonPath $query): array
8094

8195
$inBracket =true;
8296
++$bracketDepth;
97+
$i =self::skipWhitespace($chars,$i +1,$length) -1;// -1 because loop will increment
98+
8399
continue;
84100
}
85101

@@ -94,11 +110,11 @@ public static function tokenize(JsonPath $query): array
94110
}
95111

96112
if (0 ===$bracketDepth) {
97-
if ('' ===$current) {
113+
if ('' ===trim($current)) {
98114
thrownewInvalidJsonPathException('empty brackets are not allowed.',$position);
99115
}
100116

101-
$tokens[] =newJsonPathToken(TokenType::Bracket,$current);
117+
$tokens[] =newJsonPathToken(TokenType::Bracket,trim($current));
102118
$current ='';
103119
$inBracket =false;
104120
$inFilter =false;
@@ -108,11 +124,15 @@ public static function tokenize(JsonPath $query): array
108124
}
109125

110126
if ('?' ===$char &&$inBracket && !$inFilter) {
111-
if ('' !==$current) {
127+
if ('' !==trim($current)) {
112128
thrownewInvalidJsonPathException('unexpected characters before filter expression.',$position);
113129
}
130+
131+
$current ='?';
114132
$inFilter =true;
115133
$filterParenthesisDepth =0;
134+
135+
continue;
116136
}
117137

118138
if ($inFilter) {
@@ -123,6 +143,15 @@ public static function tokenize(JsonPath $query): array
123143
thrownewInvalidJsonPathException('unmatched closing parenthesis in filter.',$position);
124144
}
125145
}
146+
$current .=$char;
147+
148+
continue;
149+
}
150+
151+
if ($inBracket &&self::isWhitespace($char)) {
152+
$current .=$char;
153+
154+
continue;
126155
}
127156

128157
// recursive descent
@@ -158,6 +187,7 @@ public static function tokenize(JsonPath $query): array
158187
thrownewInvalidJsonPathException('unclosed string literal.',$length -1);
159188
}
160189

190+
$current =trim($current);
161191
if ('' !==$current) {
162192
// final validation of the whole name
163193
if (!preg_match('/^(?:\*|[a-zA-Z_\x{0080}-\x{D7FF}\x{E000}-\x{10FFFF}][a-zA-Z0-9_\x{0080}-\x{D7FF}\x{E000}-\x{10FFFF}]*)$/u',$current)) {
@@ -169,4 +199,18 @@ public static function tokenize(JsonPath $query): array
169199

170200
return$tokens;
171201
}
202+
203+
privatestaticfunctionisWhitespace(string$char):bool
204+
{
205+
return\in_array($char,self::RFC9535_WHITESPACE_CHARS,true);
206+
}
207+
208+
privatestaticfunctionskipWhitespace(array$chars,int$index,int$length):int
209+
{
210+
while ($index <$length &&self::isWhitespace($chars[$index])) {
211+
++$index;
212+
}
213+
214+
return$index;
215+
}
172216
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp