Skip to content

Commit

Permalink
Fixed null-ref for case-(in)sensitive null-search (#165)
Browse files Browse the repository at this point in the history
* Fixed null-ref for case-insensitive null-search
Added null-escaping sequence (to distinguish between prop==null (as null) and prop==\null (as string))

* Added null-search case-insensitive test

* Code style

* Added escape-sequences description to README.md

Co-authored-by: Nikita Prokhorov <nikita.prokhorov@grse.de>
  • Loading branch information
prokhorovn and Nikita Prokhorov authored Jan 10, 2022
1 parent 7b6f3c7 commit 820358e
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 15 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,10 @@ More formally:
* `pageSize` is the number of items returned per page

Notes:
* You can use backslashes to escape commas and pipes within value fields
* You can use backslashes to escape special characters and sequences:
* commas: `Title@=some\,title` makes a match with "some,title"
* pipes: `Title@=some\|title` makes a match with "some|title"
* null values: `Title@=\null` will search for items with title equal to "null" (not a missing value, but "null"-string literally)
* You can have spaces anywhere except *within* `{Name}` or `{Operator}` fields
* If you need to look at the data before applying pagination (eg. get total count), use the optional paramters on `Apply` to defer pagination (an [example](https://github.com/Biarity/Sieve/issues/34))
* Here's a [good example on how to work with enumerables](https://github.com/Biarity/Sieve/issues/2)
Expand Down
17 changes: 12 additions & 5 deletions Sieve/Models/FilterTerm.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

Expand All @@ -7,12 +8,16 @@ namespace Sieve.Models
public class FilterTerm : IFilterTerm, IEquatable<FilterTerm>
{
private const string EscapedPipePattern = @"(?<!($|[^\\]|^)(\\\\)*?\\)\|";
private const string PipeToEscape = @"\|";
private const string BackslashToEscape = @"\\";
private const string OperatorsRegEx = @"(!@=\*|!_=\*|!=\*|!@=|!_=|==\*|@=\*|_=\*|==|!=|>=|<=|>|<|@=|_=)";
private const string EscapeNegPatternForOper = @"(?<!\\)" + OperatorsRegEx;
private const string EscapePosPatternForOper = @"(?<=\\)" + OperatorsRegEx;

private static readonly HashSet<string> _escapedSequences = new HashSet<string>
{
@"\|",
@"\\"
};

public string Filter
{
set
Expand All @@ -30,8 +35,7 @@ public string Filter
}

Values = Regex.Split(filterSplits[2], EscapedPipePattern)
.Select(t => t.Replace(PipeToEscape, "|").Trim())
.Select(t => t.Replace(BackslashToEscape, "\\").Trim())
.Select(UnEscape)
.ToArray();
}

Expand All @@ -40,9 +44,12 @@ public string Filter
OperatorIsCaseInsensitive = Operator.EndsWith("*");
OperatorIsNegated = OperatorParsed != FilterOperator.NotEquals && Operator.StartsWith("!");
}

}

private string UnEscape(string escapedTerm)
=> _escapedSequences.Aggregate(escapedTerm,
(current, sequence) => Regex.Replace(current, $@"(\\)({sequence})", "$2"));

public string[] Names { get; private set; }

public FilterOperator OperatorParsed { get; private set; }
Expand Down
7 changes: 6 additions & 1 deletion Sieve/Services/SieveProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public class SieveProcessor<TSieveModel, TFilterTerm, TSortTerm> : ISieveProcess
where TSortTerm : ISortTerm, new()
{
private const string NullFilterValue = "null";
private const char EscapeChar = '\\';
private readonly ISieveCustomSortMethods _customSortMethods;
private readonly ISieveCustomFilterMethods _customFilterMethods;
private readonly SievePropertyMapper _mapper = new SievePropertyMapper();
Expand Down Expand Up @@ -199,7 +200,7 @@ protected virtual IQueryable<TEntity> ApplyFiltering<TEntity>(TSieveModel model,
? Expression.Constant(null, property.PropertyType)
: ConvertStringValueToConstantExpression(filterTermValue, property, converter);

if (filterTerm.OperatorIsCaseInsensitive)
if (filterTerm.OperatorIsCaseInsensitive && !isFilterTermValueNull)
{
propertyValue = Expression.Call(propertyValue,
typeof(string).GetMethods()
Expand Down Expand Up @@ -311,6 +312,10 @@ private static Expression GenerateFilterNullCheckExpression(Expression propertyV
private static Expression ConvertStringValueToConstantExpression(string value, PropertyInfo property,
TypeConverter converter)
{
// to allow user to distinguish between prop==null (as null) and prop==\null (as "null"-string)
value = value.Equals(EscapeChar + NullFilterValue, StringComparison.InvariantCultureIgnoreCase)
? value.TrimStart(EscapeChar)
: value;
dynamic constantVal = converter.CanConvertFrom(typeof(string))
? converter.ConvertFrom(value)
: Convert.ChangeType(value, property.PropertyType);
Expand Down
36 changes: 28 additions & 8 deletions SieveUnitTests/StringFilterNullTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public StringFilterNullTests()
{
Id = 2,
DateCreated = DateTimeOffset.UtcNow,
Text = "Regular comment without n*ll.",
Text = "Regular comment without n*ll",
Author = "Mouse",
},
new Comment
Expand All @@ -47,24 +47,28 @@ public StringFilterNullTests()
DateCreated = DateTimeOffset.UtcNow,
Text = null,
Author = "null",
},
}
}.AsQueryable();
}

[Fact]
public void Filter_Equals_Null()
[Theory]
[InlineData("Text==null")]
[InlineData("Text==*null")]
public void Filter_Equals_Null(string filter)
{
var model = new SieveModel {Filters = "Text==null"};
var model = new SieveModel {Filters = filter};

var result = _processor.Apply(model, _comments);

Assert.Equal(100, result.Single().Id);
}

[Fact]
public void Filter_NotEquals_Null()
[Theory]
[InlineData("Text!=null")]
[InlineData("Text!=*null")]
public void Filter_NotEquals_Null(string filter)
{
var model = new SieveModel {Filters = "Text!=null"};
var model = new SieveModel {Filters = filter};

var result = _processor.Apply(model, _comments);

Expand Down Expand Up @@ -101,6 +105,22 @@ public void MultiFilter_Contains_NullString(string filter, params int[] expected
Assert.Equal(expectedIds, result.Select(p => p.Id));
}

[Theory]
[InlineData(@"Author==\null", 100)]
[InlineData(@"Author==*\null", 100)]
[InlineData(@"Author==*\NuLl", 100)]
[InlineData(@"Author!=*\null", 0, 1, 2)]
[InlineData(@"Author!=*\NulL", 0, 1, 2)]
[InlineData(@"Author!=\null", 0, 1, 2)]
public void SingleFilter_Equals_NullStringEscaped(string filter, params int[] expectedIds)
{
var model = new SieveModel {Filters = filter};

var result = _processor.Apply(model, _comments);

Assert.Equal(expectedIds, result.Select(p => p.Id));
}

[Theory]
[InlineData("Text_=null")]
[InlineData("Text_=*null")]
Expand Down

0 comments on commit 820358e

Please sign in to comment.