Skip to content

Commit f818c8f

Browse files
committed
Fixed: do not allow comparison of count() with null; do not treat null as a field name
1 parent c0eb108 commit f818c8f

File tree

4 files changed

+70
-30
lines changed

4 files changed

+70
-30
lines changed

src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -135,40 +135,34 @@ protected ComparisonExpression ParseComparison(string operatorName)
135135
EatText(operatorName);
136136
EatSingleCharacterToken(TokenKind.OpenParen);
137137

138-
// Allow equality comparison of a HasOne relationship with null.
138+
// Allow equality comparison of a to-one relationship with null.
139139
FieldChainRequirements leftChainRequirements = comparisonOperator == ComparisonOperator.Equals
140140
? FieldChainRequirements.EndsInAttribute | FieldChainRequirements.EndsInToOne
141141
: FieldChainRequirements.EndsInAttribute;
142142

143143
QueryExpression leftTerm = ParseCountOrField(leftChainRequirements);
144-
Converter<string, object> rightConstantValueConverter;
144+
145+
EatSingleCharacterToken(TokenKind.Comma);
146+
147+
QueryExpression rightTerm;
145148

146149
if (leftTerm is CountExpression)
147150
{
148-
rightConstantValueConverter = GetConstantValueConverterForCount();
151+
Converter<string, object> rightConstantValueConverter = GetConstantValueConverterForCount();
152+
rightTerm = ParseCountOrConstantOrField(FieldChainRequirements.EndsInAttribute, rightConstantValueConverter);
149153
}
150154
else if (leftTerm is ResourceFieldChainExpression fieldChain && fieldChain.Fields[^1] is AttrAttribute attribute)
151155
{
152-
rightConstantValueConverter = GetConstantValueConverterForAttribute(attribute);
156+
Converter<string, object> rightConstantValueConverter = GetConstantValueConverterForAttribute(attribute);
157+
rightTerm = ParseCountOrConstantOrNullOrField(FieldChainRequirements.EndsInAttribute, rightConstantValueConverter);
153158
}
154159
else
155160
{
156-
// This temporary value never survives; it gets discarded during the second pass below.
157-
rightConstantValueConverter = _ => 0;
161+
rightTerm = ParseNull();
158162
}
159163

160-
EatSingleCharacterToken(TokenKind.Comma);
161-
162-
QueryExpression rightTerm = ParseCountOrConstantOrNullOrField(FieldChainRequirements.EndsInAttribute, rightConstantValueConverter);
163-
164164
EatSingleCharacterToken(TokenKind.CloseParen);
165165

166-
if (leftTerm is ResourceFieldChainExpression leftChain && leftChain.Fields[^1] is RelationshipAttribute && rightTerm is not NullConstantExpression)
167-
{
168-
// Run another pass over left chain to produce an error.
169-
OnResolveFieldChain(leftChain.ToString(), FieldChainRequirements.EndsInAttribute);
170-
}
171-
172166
return new ComparisonExpression(comparisonOperator, leftTerm, rightTerm);
173167
}
174168

@@ -201,8 +195,9 @@ protected AnyExpression ParseAny()
201195
EatText(Keywords.Any);
202196
EatSingleCharacterToken(TokenKind.OpenParen);
203197

204-
ResourceFieldChainExpression targetAttribute = ParseFieldChain(FieldChainRequirements.EndsInAttribute, null);
205-
Converter<string, object> constantValueConverter = GetConstantValueConverterForAttribute((AttrAttribute)targetAttribute.Fields[^1]);
198+
ResourceFieldChainExpression targetAttributeChain = ParseFieldChain(FieldChainRequirements.EndsInAttribute, null);
199+
var targetAttribute = (AttrAttribute)targetAttributeChain.Fields[^1];
200+
Converter<string, object> constantValueConverter = GetConstantValueConverterForAttribute(targetAttribute);
206201

207202
EatSingleCharacterToken(TokenKind.Comma);
208203

@@ -223,7 +218,7 @@ protected AnyExpression ParseAny()
223218

224219
IImmutableSet<LiteralConstantExpression> constantSet = constantsBuilder.ToImmutable();
225220

226-
return new AnyExpression(targetAttribute, constantSet);
221+
return new AnyExpression(targetAttributeChain, constantSet);
227222
}
228223

229224
protected HasExpression ParseHas()
@@ -349,6 +344,25 @@ protected QueryExpression ParseCountOrField(FieldChainRequirements chainRequirem
349344
return ParseFieldChain(chainRequirements, "Count function or field name expected.");
350345
}
351346

347+
protected QueryExpression ParseCountOrConstantOrField(FieldChainRequirements chainRequirements, Converter<string, object> constantValueConverter)
348+
{
349+
CountExpression? count = TryParseCount();
350+
351+
if (count != null)
352+
{
353+
return count;
354+
}
355+
356+
LiteralConstantExpression? constant = TryParseConstant(constantValueConverter);
357+
358+
if (constant != null)
359+
{
360+
return constant;
361+
}
362+
363+
return ParseFieldChain(chainRequirements, "Count function, value between quotes or field name expected.");
364+
}
365+
352366
protected QueryExpression ParseCountOrConstantOrNullOrField(FieldChainRequirements chainRequirements, Converter<string, object> constantValueConverter)
353367
{
354368
CountExpression? count = TryParseCount();
@@ -368,6 +382,19 @@ protected QueryExpression ParseCountOrConstantOrNullOrField(FieldChainRequiremen
368382
return ParseFieldChain(chainRequirements, "Count function, value between quotes, null or field name expected.");
369383
}
370384

385+
protected LiteralConstantExpression? TryParseConstant(Converter<string, object> constantValueConverter)
386+
{
387+
if (TokenStack.TryPeek(out Token? nextToken) && nextToken.Kind == TokenKind.QuotedText)
388+
{
389+
TokenStack.Pop();
390+
391+
object constantValue = constantValueConverter(nextToken.Value!);
392+
return new LiteralConstantExpression(constantValue, nextToken.Value!);
393+
}
394+
395+
return null;
396+
}
397+
371398
protected IdentifierExpression? TryParseConstantOrNull(Converter<string, object> constantValueConverter)
372399
{
373400
if (TokenStack.TryPeek(out Token? nextToken))
@@ -392,13 +419,24 @@ protected QueryExpression ParseCountOrConstantOrNullOrField(FieldChainRequiremen
392419

393420
protected LiteralConstantExpression ParseConstant(Converter<string, object> constantValueConverter)
394421
{
395-
if (TokenStack.TryPop(out Token? token) && token.Kind == TokenKind.QuotedText)
422+
LiteralConstantExpression? constant = TryParseConstant(constantValueConverter);
423+
424+
if (constant == null)
425+
{
426+
throw new QueryParseException("Value between quotes expected.");
427+
}
428+
429+
return constant;
430+
}
431+
432+
protected NullConstantExpression ParseNull()
433+
{
434+
if (TokenStack.TryPop(out Token? token) && token is { Kind: TokenKind.Text, Value: Keywords.Null })
396435
{
397-
object constantValue = constantValueConverter(token.Value!);
398-
return new LiteralConstantExpression(constantValue, token.Value!);
436+
return NullConstantExpression.Instance;
399437
}
400438

401-
throw new QueryParseException("Value between quotes expected.");
439+
throw new QueryParseException("null expected.");
402440
}
403441

404442
private Converter<string, object> GetConstantValueConverterForCount()

src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ private void EatFieldChain(StringBuilder pathBuilder, string? alternativeErrorMe
4949
{
5050
while (true)
5151
{
52-
if (TokenStack.TryPop(out Token? token) && token.Kind == TokenKind.Text)
52+
if (TokenStack.TryPop(out Token? token) && token.Kind == TokenKind.Text && token.Value != Keywords.Null)
5353
{
5454
pathBuilder.Append(token.Value);
5555

test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/FilterParseTests.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public void Reader_Is_Enabled(JsonApiQueryStringParameters parametersDisabled, b
6363
"Relationship 'author' in 'posts.author' must be a to-many relationship on resource type 'blogPosts'.")]
6464
[InlineData("filter[posts.comments.author]", "equals(firstName,'some')",
6565
"Relationship 'author' in 'posts.comments.author' must be a to-many relationship on resource type 'comments'.")]
66-
[InlineData("filter[posts]", "equals(author,'some')", "Attribute 'author' does not exist on resource type 'blogPosts'.")]
66+
[InlineData("filter[posts]", "equals(author,'some')", "null expected.")]
6767
[InlineData("filter[posts]", "lessThan(author,null)", "Attribute 'author' does not exist on resource type 'blogPosts'.")]
6868
[InlineData("filter", " ", "Unexpected whitespace.")]
6969
[InlineData("filter", "contains(owner.displayName ,)", "Unexpected whitespace.")]
@@ -73,20 +73,21 @@ public void Reader_Is_Enabled(JsonApiQueryStringParameters parametersDisabled, b
7373
[InlineData("filter", "equals'", "Unexpected ' outside text.")]
7474
[InlineData("filter", "equals(", "Count function or field name expected.")]
7575
[InlineData("filter", "equals('1'", "Count function or field name expected.")]
76-
[InlineData("filter", "equals(count(posts),", "Count function, value between quotes, null or field name expected.")]
76+
[InlineData("filter", "equals(count(posts),", "Count function, value between quotes or field name expected.")]
77+
[InlineData("filter", "equals(count(posts),null)", "Count function, value between quotes or field name expected.")]
7778
[InlineData("filter", "equals(owner..displayName,'')", "Count function or field name expected.")]
7879
[InlineData("filter", "equals(owner.displayName.,'')", "Count function or field name expected.")]
7980
[InlineData("filter", "equals(title,')", "' expected.")]
8081
[InlineData("filter", "equals(title,null", ") expected.")]
81-
[InlineData("filter", "equals(null", "Field 'null' does not exist on resource type 'blogs'.")]
82+
[InlineData("filter", "equals(null", "Count function or field name expected.")]
8283
[InlineData("filter", "equals(title,(", "Count function, value between quotes, null or field name expected.")]
8384
[InlineData("filter", "equals(has(posts),'true')", "Field 'has' does not exist on resource type 'blogs'.")]
8485
[InlineData("filter", "has(posts,", "Filter function expected.")]
8586
[InlineData("filter", "contains)", "( expected.")]
8687
[InlineData("filter", "contains(title,'a','b')", ") expected.")]
8788
[InlineData("filter", "contains(title,null)", "Value between quotes expected.")]
8889
[InlineData("filter[posts]", "contains(author,null)", "Attribute 'author' does not exist on resource type 'blogPosts'.")]
89-
[InlineData("filter", "any(null,'a','b')", "Attribute 'null' does not exist on resource type 'blogs'.")]
90+
[InlineData("filter", "any(null,'a','b')", "Field name expected.")]
9091
[InlineData("filter", "any('a','b','c')", "Field name expected.")]
9192
[InlineData("filter", "any(title,'b','c',)", "Value between quotes expected.")]
9293
[InlineData("filter", "any(title)", ", expected.")]
@@ -137,6 +138,7 @@ public void Reader_Read_Fails(string parameterName, string parameterValue, strin
137138
[InlineData("filter[owner.posts]", "equals(caption,'some')", "owner.posts", "equals(caption,'some')")]
138139
[InlineData("filter[posts.comments]", "equals(createdAt,'2000-01-01')", "posts.comments", "equals(createdAt,'2000-01-01')")]
139140
[InlineData("filter", "equals(count(posts),'1')", null, "equals(count(posts),'1')")]
141+
[InlineData("filter", "equals(count(posts),count(owner.posts))", null, "equals(count(posts),count(owner.posts))")]
140142
[InlineData("filter[posts]", "equals(caption,null)", "posts", "equals(caption,null)")]
141143
[InlineData("filter[posts]", "equals(author,null)", "posts", "equals(author,null)")]
142144
[InlineData("filter[posts]", "equals(author.userName,author.displayName)", "posts", "equals(author.userName,author.displayName)")]

test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/LegacyFilterParseTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ public LegacyFilterParseTests()
3232
[InlineData("filter[", "some", "Expected field name between brackets in filter parameter name.")]
3333
[InlineData("filter[]", "some", "Expected field name between brackets in filter parameter name.")]
3434
[InlineData("filter[some]", "other", "Field 'some' does not exist on resource type 'blogPosts'.")]
35-
[InlineData("filter[author]", "some", "Attribute 'author' does not exist on resource type 'blogPosts'.")]
35+
[InlineData("filter[author]", "some", "null expected.")]
3636
[InlineData("filter[author.posts]", "some",
3737
"Field 'posts' in 'author.posts' must be an attribute or a to-one relationship on resource type 'webAccounts'.")]
3838
[InlineData("filter[unknown.id]", "some", "Relationship 'unknown' in 'unknown.id' does not exist on resource type 'blogPosts'.")]
3939
[InlineData("filter", "expr:equals(some,'other')", "Field 'some' does not exist on resource type 'blogPosts'.")]
40-
[InlineData("filter", "expr:equals(author,'Joe')", "Attribute 'author' does not exist on resource type 'blogPosts'.")]
40+
[InlineData("filter", "expr:equals(author,'Joe')", "null expected.")]
4141
[InlineData("filter", "expr:has(author)", "Relationship 'author' must be a to-many relationship on resource type 'blogPosts'.")]
4242
[InlineData("filter", "expr:equals(count(author),'1')", "Relationship 'author' must be a to-many relationship on resource type 'blogPosts'.")]
4343
public void Reader_Read_Fails(string parameterName, string parameterValue, string errorMessage)

0 commit comments

Comments
 (0)