Skip to content

Commit

Permalink
Implement new comparison operators
Browse files Browse the repository at this point in the history
  • Loading branch information
adams85 committed Jun 30, 2023
1 parent 2934137 commit 6fe7c38
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 32 deletions.
13 changes: 9 additions & 4 deletions src/ConfigCatClient/Evaluation/EvaluateLogBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,16 @@ public EvaluateLogBuilder AppendComparisonCondition(string? comparisonAttribute,
: AppendComparisonCondition(comparisonAttribute, comparator, (object?)null);
}

public EvaluateLogBuilder AppendComparisonCondition(string? comparisonAttribute, Comparator comparator, double? comparisonValue)
public EvaluateLogBuilder AppendComparisonCondition(string? comparisonAttribute, Comparator comparator, double? comparisonValue, bool isDateTime = false)
{
return comparisonValue is not null
? Append($"User.{comparisonAttribute} {comparator.ToDisplayText()} '{comparisonValue.Value}'")
: AppendComparisonCondition(comparisonAttribute, comparator, (object?)null);
if (comparisonValue is null)
{
return AppendComparisonCondition(comparisonAttribute, comparator, (object?)null);
}

return isDateTime && DateTimeUtils.TryConvertFromUnixTimeSeconds(comparisonValue.Value, out var dateTime)
? Append($"User.{comparisonAttribute} {comparator.ToDisplayText()} '{comparisonValue.Value}' ({dateTime:yyyy-MM-dd'T'HH:mm:ss.fffK})")
: Append($"User.{comparisonAttribute} {comparator.ToDisplayText()} '{comparisonValue.Value}'");
}

public EvaluateLogBuilder AppendSegmentCondition(SegmentComparator comparator, Segment? segment)
Expand Down
153 changes: 143 additions & 10 deletions src/ConfigCatClient/Evaluation/RolloutEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -353,12 +353,31 @@ private bool TryEvaluateConditions<TCondition>(TCondition[] conditions, Func<TCo
var comparator = condition.Comparator;
switch (comparator)
{
case Comparator.SensitiveTextEquals:
case Comparator.SensitiveTextNotEqual:
logBuilder.AppendComparisonCondition(userAttributeName, comparator, condition.StringValue, ModelHelper.SensitiveValueMaxLength);
// TODO: error handling - missing configJsonSalt
result = canEvaluate
&& EvaluateSensitiveTextEquals(userAttributeName!, condition.StringValue,
context.Setting.ConfigJsonSalt!, contextSalt, negate: comparator == Comparator.SensitiveTextNotEqual);
break;

case Comparator.SensitiveOneOf:
case Comparator.SensitiveNotOneOf:
logBuilder.AppendComparisonCondition(userAttributeName, comparator, condition.StringListValue, ModelHelper.SensitiveValueMaxLength);
// TODO: error handling - missing configJsonSalt
result = canEvaluate
&& EvaluateSensitiveOneOf(userAttributeValue!, condition.StringListValue, context.Setting.ConfigJsonSalt!, contextSalt, negate: comparator == Comparator.SensitiveNotOneOf);
&& EvaluateSensitiveOneOf(userAttributeValue!, condition.StringListValue,
context.Setting.ConfigJsonSalt!, contextSalt, negate: comparator == Comparator.SensitiveNotOneOf);
break;

case Comparator.SensitiveTextStartsWith:
case Comparator.SensitiveTextEndsWith:
logBuilder.AppendComparisonCondition(userAttributeName, comparator, condition.StringListValue, ModelHelper.SensitiveValueMaxLength);
// TODO: error handling - missing configJsonSalt
result = canEvaluate
&& EvaluateSensitiveTextSliceEquals(userAttributeName!, condition.StringListValue,
context.Setting.ConfigJsonSalt!, contextSalt, startsWith: comparator == Comparator.SensitiveTextStartsWith);
break;

case Comparator.Contains:
Expand Down Expand Up @@ -400,13 +419,22 @@ private bool TryEvaluateConditions<TCondition>(TCondition[] conditions, Func<TCo

case Comparator.DateTimeBefore:
case Comparator.DateTimeAfter:
case Comparator.SensitiveTextEquals:
case Comparator.SensitiveTextNotEqual:
case Comparator.SensitiveTextStartsWith:
case Comparator.SensitiveTextEndsWith:
logBuilder.AppendComparisonCondition(userAttributeName, comparator, condition.DoubleValue);
// TODO: error handling - what to do when the value is invalid (not available/multiple values specified)?
// TODO: error handling - missing configJsonSalt
result = canEvaluate
&& double.TryParse(userAttributeValue!.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out number)
&& EvaluateDateTimeRelation(number, condition.DoubleValue, before: comparator == Comparator.DateTimeBefore);
break;

case Comparator.SensitiveArrayContains:
case Comparator.SensitiveArrayNotContains:
throw new NotImplementedException(); // TODO
logBuilder.AppendComparisonCondition(userAttributeName, comparator, condition.StringValue, ModelHelper.SensitiveValueMaxLength);
// TODO: error handling - missing configJsonSalt
result = canEvaluate
&& EvaluateSensitiveArrayContains(userAttributeName!, condition.StringValue,
context.Setting.ConfigJsonSalt!, contextSalt, negate: comparator == Comparator.SensitiveArrayNotContains);
break;

default:
logBuilder.AppendComparisonCondition(userAttributeName, comparator, condition.GetComparisonValue(throwIfInvalid: false));
Expand All @@ -417,6 +445,19 @@ private bool TryEvaluateConditions<TCondition>(TCondition[] conditions, Func<TCo
return error;
}

private static bool EvaluateSensitiveTextEquals(string text, string? comparisonValue, string configJsonSalt, string contextSalt, bool negate)
{
if (comparisonValue is null)
{
// TODO: error handling - what to do when comparison value is invalid (not available/multiple values specified)?
return false;
}

var hash = HashComparisonValue(text.AsSpan(), configJsonSalt, contextSalt);

return (hash == comparisonValue) ^ negate;
}

private static bool EvaluateSensitiveOneOf(string text, string[]? comparisonValues, string configJsonSalt, string contextSalt, bool negate)
{
if (comparisonValues is null)
Expand All @@ -425,11 +466,11 @@ private static bool EvaluateSensitiveOneOf(string text, string[]? comparisonValu
return false;
}

var hash = HashComparisonValue(text, configJsonSalt, contextSalt);
var hash = HashComparisonValue(text.AsSpan(), configJsonSalt, contextSalt).AsSpan();

for (var i = 0; i < comparisonValues.Length; i++)
{
if (hash == comparisonValues[i].Trim()) // TODO: error handling - what to do when item is null?
if (hash.Equals(comparisonValues[i].AsSpan().Trim(), StringComparison.Ordinal)) // TODO: error handling - what to do when item is null?
{
return !negate;
}
Expand All @@ -438,6 +479,46 @@ private static bool EvaluateSensitiveOneOf(string text, string[]? comparisonValu
return negate;
}

private static bool EvaluateSensitiveTextSliceEquals(string text, string[]? comparisonValues, string configJsonSalt, string contextSalt, bool startsWith)
{
if (comparisonValues is null)
{
// TODO: error handling - what to do when comparison value is invalid (not available/multiple values specified)?
return false;
}

for (var i = 0; i < comparisonValues.Length; i++)
{
var item = comparisonValues[i]; // TODO: error handling - what to do when item is null?

ReadOnlySpan<char> hash2;

var index = comparisonValues[i].IndexOf('_');
if (index < 0
|| !int.TryParse(item.AsSpan(0, index).ToParsable(), NumberStyles.None, CultureInfo.InvariantCulture, out var sliceLength)
|| (hash2 = item.AsSpan(index + 1)).IsEmpty)
{
// TODO: error handling - what to do when item is not in the expected format?
return false;
}

if (text.Length < sliceLength)
{
continue;
}

var slice = startsWith ? text.AsSpan(0, sliceLength) : text.AsSpan(text.Length - sliceLength);

var hash = HashComparisonValue(slice, configJsonSalt, contextSalt);
if (hash.AsSpan().Equals(hash2, StringComparison.Ordinal))
{
return true;
}
}

return false;
}

private static bool EvaluateContains(string text, string[]? comparisonValues, bool negate)
{
if (comparisonValues is null)
Expand Down Expand Up @@ -542,6 +623,58 @@ private static bool EvaluateNumberRelation(double number, Comparator comparator,
};
}

private static bool EvaluateDateTimeRelation(double number, double? comparisonValue, bool before)
{
if (comparisonValue is not { } number2)
{
// TODO: error handling - what to do when comparison value is invalid (not available/multiple values specified)?

// If the user object property is not a valid unix timestamp, or the config.json is not containing a valid unix timestamp,
// we should log a warning message and treat the rule as if it was evaluated to false (so we skip the rule at all and we can go for the next rule).
// We should treat the value as valid if a valid unix timestamp is passed as a string and can be converted to a double or it is passed as a double/number directly.
// TODO: warning message? (if we log a warning message, then we also should in the case of e.g. number comparisons)

return false;
}

return before ? number < number2 : number > number2;
}

private static bool EvaluateSensitiveArrayContains(string text, string? comparisonValue, string configJsonSalt, string contextSalt, bool negate)
{
if (comparisonValue is null)
{
// TODO: error handling - what to do when comparison value is invalid (not available/multiple values specified)?
return false;
}

int index;
for (var startIndex = 0; startIndex < text.Length; startIndex = index + 1)
{
index = text.IndexOf(',', startIndex);
if (index < 0)
{
index = text.Length;
}

var slice = text.AsSpan(startIndex, index - startIndex).Trim();
if (slice.IsEmpty)
{
// TODO: error handling - what to do with empty/whitespace items?
continue;
}

var hash = HashComparisonValue(slice, configJsonSalt, contextSalt);

if (hash == comparisonValue)
{
return !negate;
}
}

return negate;
}

private string? TryEvaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition condition, ref EvaluateContext context, out bool result)
{
result = false;
Expand Down Expand Up @@ -687,8 +820,8 @@ private static bool EvaluateNumberRelation(double number, Comparator comparator,
return null;
}

private static string HashComparisonValue(string value, string configJsonSalt, string contextSalt)
private static string HashComparisonValue(ReadOnlySpan<char> value, string configJsonSalt, string contextSalt)
{
return (value + configJsonSalt + contextSalt).Sha256();
return string.Concat(value.ToConcatenable(), configJsonSalt, contextSalt).Sha256();
}
}
30 changes: 30 additions & 0 deletions src/ConfigCatClient/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,34 @@ public static string Truncate(this string value, int maxLength)
? value.Substring(0, maxLength - ellipsis.Length) + ellipsis
: value.Substring(0, maxLength);
}

public static
#if NET5_0_OR_GREATER
ReadOnlySpan<char>
#else
string
#endif
ToConcatenable(this ReadOnlySpan<char> s)
{
#if NET5_0_OR_GREATER
return s;
#else
return s.ToString();
#endif
}

public static
#if NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
ReadOnlySpan<char>
#else
string
#endif
ToParsable(this ReadOnlySpan<char> s)
{
#if NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
return s;
#else
return s.ToString();
#endif
}
}
54 changes: 36 additions & 18 deletions src/ConfigCatClient/Utils/DateTimeUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,31 @@ namespace ConfigCat.Client.Utils;

internal static class DateTimeUtils
{
public static string ToUnixTimeStamp(this DateTime dateTime)
public static long ToUnixTimeMilliseconds(this DateTime dateTime)
{
#if !NET45
var milliseconds = new DateTimeOffset(dateTime).ToUnixTimeMilliseconds();
return new DateTimeOffset(dateTime).ToUnixTimeMilliseconds();
#else
// Based on: https://github.com/dotnet/runtime/blob/v6.0.13/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs#L629

const long unixEpochMilliseconds = 62_135_596_800_000L;
var milliseconds = dateTime.Ticks / TimeSpan.TicksPerMillisecond - unixEpochMilliseconds;
return dateTime.Ticks / TimeSpan.TicksPerMillisecond - unixEpochMilliseconds;
#endif

return milliseconds.ToString(CultureInfo.InvariantCulture);
}

public static bool TryParseUnixTimeStamp(ReadOnlySpan<char> span, out DateTime dateTime)
public static string ToUnixTimeStamp(this DateTime dateTime)
{
#if NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
var slice = span;
#else
var slice = span.ToString();
#endif
return ToUnixTimeMilliseconds(dateTime).ToString(CultureInfo.InvariantCulture);
}

if (!long.TryParse(slice, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var milliseconds))
public static bool TryConvertFromUnixTimeMilliseconds(long milliseconds, out DateTime dateTime)
{
#if !NET45
try
{
dateTime = default;
return false;
dateTime = DateTimeOffset.FromUnixTimeMilliseconds(milliseconds).UtcDateTime;
return true;
}

#if !NET45
try { dateTime = DateTimeOffset.FromUnixTimeMilliseconds(milliseconds).UtcDateTime; }
catch (ArgumentOutOfRangeException)
{
dateTime = default;
Expand All @@ -55,8 +50,31 @@ public static bool TryParseUnixTimeStamp(ReadOnlySpan<char> span, out DateTime d

var ticks = (milliseconds + unixEpochMilliseconds) * TimeSpan.TicksPerMillisecond;
dateTime = new DateTime(ticks, DateTimeKind.Utc);
return true;
#endif
}

return true;
public static bool TryConvertFromUnixTimeSeconds(double seconds, out DateTime dateTime)
{
long milliseconds;
try { milliseconds = checked((long)(seconds * 1000)); }
catch (OverflowException)
{
dateTime = default;
return false;
}

return TryConvertFromUnixTimeMilliseconds(milliseconds, out dateTime);
}

public static bool TryParseUnixTimeStamp(ReadOnlySpan<char> span, out DateTime dateTime)
{
if (!long.TryParse(span.ToParsable(), NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var milliseconds))
{
dateTime = default;
return false;
}

return TryConvertFromUnixTimeMilliseconds(milliseconds, out dateTime);
}
}

0 comments on commit 6fe7c38

Please sign in to comment.