fix(advanced-search): A — property-test parse no longer throws on bad input (TryParse + case-insensitive enums)
This commit is contained in:
parent
858c1d18bc
commit
eb5e15e228
2 changed files with 125 additions and 48 deletions
|
|
@ -0,0 +1,42 @@
|
|||
using System;
|
||||
using Server;
|
||||
using Server.Engines.AdvancedSearch;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests;
|
||||
|
||||
public class AdvancedSearchUtilitiesTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("abc")] // not a number -> was FormatException
|
||||
[InlineData("99999999999")] // overflows int -> was OverflowException
|
||||
[InlineData("0xZZ")] // bad hex -> was FormatException
|
||||
public void CompareValues_BadNumeric_ReturnsFalse_DoesNotThrow(string value)
|
||||
{
|
||||
var ex = Record.Exception(() =>
|
||||
{
|
||||
var result = AdvancedSearchUtilities.CompareValues(typeof(int), 5, value, ">");
|
||||
Assert.False(result);
|
||||
});
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Bogus")] // not a member -> was ArgumentException
|
||||
[InlineData("onehandedxyz")] // not a member, even case-insensitively -> was ArgumentException
|
||||
public void CompareValues_BadEnum_ReturnsFalse_DoesNotThrow(string value)
|
||||
{
|
||||
var ex = Record.Exception(() =>
|
||||
{
|
||||
var result = AdvancedSearchUtilities.CompareValues(typeof(Layer), (byte)Layer.OneHanded, value, "=");
|
||||
Assert.False(result);
|
||||
});
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompareValues_ValidEnum_IgnoresCase()
|
||||
{
|
||||
Assert.True(AdvancedSearchUtilities.CompareValues(typeof(Layer), (byte)Layer.OneHanded, "onehanded", "="));
|
||||
}
|
||||
}
|
||||
|
|
@ -43,77 +43,80 @@ public static class AdvancedSearchUtilities
|
|||
|
||||
if (propertyType == typeof(long))
|
||||
{
|
||||
var parsedValue = ParseValue<long>(valuePart);
|
||||
return CompareNumeric((long)propertyValue!, parsedValue, operatorSpan);
|
||||
return TryParseValue<long>(valuePart, out var parsedValue) &&
|
||||
CompareNumeric((long)propertyValue!, parsedValue, operatorSpan);
|
||||
}
|
||||
if (propertyType == typeof(ulong))
|
||||
{
|
||||
var parsedValue = ParseValue<ulong>(valuePart);
|
||||
return CompareNumeric((ulong)propertyValue!, parsedValue, operatorSpan);
|
||||
return TryParseValue<ulong>(valuePart, out var parsedValue) &&
|
||||
CompareNumeric((ulong)propertyValue!, parsedValue, operatorSpan);
|
||||
}
|
||||
if (propertyType == typeof(int))
|
||||
{
|
||||
var parsedValue = ParseValue<int>(valuePart);
|
||||
return CompareNumeric((int)propertyValue!, parsedValue, operatorSpan);
|
||||
return TryParseValue<int>(valuePart, out var parsedValue) &&
|
||||
CompareNumeric((int)propertyValue!, parsedValue, operatorSpan);
|
||||
}
|
||||
if (propertyType == typeof(uint))
|
||||
{
|
||||
var parsedValue = ParseValue<uint>(valuePart);
|
||||
return CompareNumeric((uint)propertyValue!, parsedValue, operatorSpan);
|
||||
return TryParseValue<uint>(valuePart, out var parsedValue) &&
|
||||
CompareNumeric((uint)propertyValue!, parsedValue, operatorSpan);
|
||||
}
|
||||
if (propertyType == typeof(short))
|
||||
{
|
||||
var parsedValue = ParseValue<short>(valuePart);
|
||||
return CompareNumeric((short)propertyValue!, parsedValue, operatorSpan);
|
||||
return TryParseValue<short>(valuePart, out var parsedValue) &&
|
||||
CompareNumeric((short)propertyValue!, parsedValue, operatorSpan);
|
||||
}
|
||||
if (propertyType == typeof(ushort))
|
||||
{
|
||||
var parsedValue = ParseValue<ushort>(valuePart);
|
||||
return CompareNumeric((ushort)propertyValue!, parsedValue, operatorSpan);
|
||||
return TryParseValue<ushort>(valuePart, out var parsedValue) &&
|
||||
CompareNumeric((ushort)propertyValue!, parsedValue, operatorSpan);
|
||||
}
|
||||
if (propertyType == typeof(sbyte))
|
||||
{
|
||||
var parsedValue = ParseValue<sbyte>(valuePart);
|
||||
return CompareNumeric((sbyte)propertyValue!, parsedValue, operatorSpan);
|
||||
return TryParseValue<sbyte>(valuePart, out var parsedValue) &&
|
||||
CompareNumeric((sbyte)propertyValue!, parsedValue, operatorSpan);
|
||||
}
|
||||
if (propertyType == typeof(byte))
|
||||
{
|
||||
var parsedValue = ParseValue<byte>(valuePart);
|
||||
return CompareNumeric((byte)propertyValue!, parsedValue, operatorSpan);
|
||||
return TryParseValue<byte>(valuePart, out var parsedValue) &&
|
||||
CompareNumeric((byte)propertyValue!, parsedValue, operatorSpan);
|
||||
}
|
||||
if (propertyType == typeof(float))
|
||||
{
|
||||
var parsedValue = ParseValue<float>(valuePart);
|
||||
return Compare((float)propertyValue!, parsedValue, valuePart, operatorSpan);
|
||||
return TryParseValue<float>(valuePart, out var parsedValue) &&
|
||||
Compare((float)propertyValue!, parsedValue, valuePart, operatorSpan);
|
||||
}
|
||||
if (propertyType == typeof(double))
|
||||
{
|
||||
var parsedValue = ParseValue<double>(valuePart);
|
||||
return Compare((double)propertyValue!, parsedValue, valuePart, operatorSpan);
|
||||
return TryParseValue<double>(valuePart, out var parsedValue) &&
|
||||
Compare((double)propertyValue!, parsedValue, valuePart, operatorSpan);
|
||||
}
|
||||
if (propertyType == typeof(string))
|
||||
{
|
||||
var parsedValue = ParseValue<string>(valuePart);
|
||||
return Compare((string)propertyValue!, parsedValue, operatorSpan);
|
||||
return TryParseValue<string>(valuePart, out var parsedValue) &&
|
||||
Compare((string)propertyValue!, parsedValue, operatorSpan);
|
||||
}
|
||||
if (propertyType == typeof(TimeSpan))
|
||||
{
|
||||
var parsedValue = ParseValue<TimeSpan>(valuePart);
|
||||
return Compare((TimeSpan)propertyValue!, parsedValue, operatorSpan);
|
||||
return TryParseValue<TimeSpan>(valuePart, out var parsedValue) &&
|
||||
Compare((TimeSpan)propertyValue!, parsedValue, operatorSpan);
|
||||
}
|
||||
if (propertyType == typeof(DateTime))
|
||||
{
|
||||
var parsedValue = ParseValue<DateTime>(valuePart);
|
||||
return Compare((DateTime)propertyValue!, parsedValue, operatorSpan);
|
||||
return TryParseValue<DateTime>(valuePart, out var parsedValue) &&
|
||||
Compare((DateTime)propertyValue!, parsedValue, operatorSpan);
|
||||
}
|
||||
if (propertyType == typeof(bool))
|
||||
{
|
||||
var parsedValue = ParseValue<bool>(valuePart);
|
||||
return Compare((bool)propertyValue!, parsedValue, operatorSpan);
|
||||
return TryParseValue<bool>(valuePart, out var parsedValue) &&
|
||||
Compare((bool)propertyValue!, parsedValue, operatorSpan);
|
||||
}
|
||||
if (propertyType.IsEnum)
|
||||
{
|
||||
var valueEnum = Enum.Parse(propertyType, valuePart, false);
|
||||
if (!Enum.TryParse(propertyType, valuePart.ToString(), true, out var valueEnum) || valueEnum == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return GetEnumSize(propertyType) switch
|
||||
{
|
||||
|
|
@ -121,12 +124,13 @@ public static class AdvancedSearchUtilities
|
|||
2 => CompareNumeric((short)propertyValue!, (short)valueEnum, operatorSpan),
|
||||
4 => CompareNumeric((int)propertyValue!, (int)valueEnum, operatorSpan),
|
||||
8 => CompareNumeric((long)propertyValue!, (long)valueEnum, operatorSpan),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
if (!propertyType.IsValueType)
|
||||
{
|
||||
var parsedValue = ParseValue<object>(valuePart);
|
||||
return CompareReference(propertyValue!, parsedValue, operatorSpan);
|
||||
return TryParseValue<object>(valuePart, out var parsedValue) &&
|
||||
CompareReference(propertyValue!, parsedValue, operatorSpan);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
@ -249,6 +253,12 @@ public static class AdvancedSearchUtilities
|
|||
};
|
||||
|
||||
public static T ParseValue<T>(ReadOnlySpan<char> valuePart)
|
||||
{
|
||||
TryParseValue(valuePart, out T value);
|
||||
return value;
|
||||
}
|
||||
|
||||
internal static bool TryParseValue<T>(ReadOnlySpan<char> valuePart, out T value)
|
||||
{
|
||||
// Special handling for boolean and hexadecimal values
|
||||
if (typeof(T) == typeof(bool))
|
||||
|
|
@ -256,74 +266,99 @@ public static class AdvancedSearchUtilities
|
|||
var val = valuePart.ToString().ToLower();
|
||||
if (val is "true" or "1" or "enabled" or "on")
|
||||
{
|
||||
return (T)(object)true;
|
||||
value = (T)(object)true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (val is "false" or "0" or "disabled" or "off")
|
||||
{
|
||||
return (T)(object)false;
|
||||
value = (T)(object)false;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(long))
|
||||
{
|
||||
return ParseNumericValue<long, T>(valuePart);
|
||||
return TryParseNumericValue<long, T>(valuePart, out value);
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(ulong))
|
||||
{
|
||||
return ParseNumericValue<ulong, T>(valuePart);
|
||||
return TryParseNumericValue<ulong, T>(valuePart, out value);
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(int))
|
||||
{
|
||||
return ParseNumericValue<int, T>(valuePart);
|
||||
return TryParseNumericValue<int, T>(valuePart, out value);
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(uint))
|
||||
{
|
||||
return ParseNumericValue<uint, T>(valuePart);
|
||||
return TryParseNumericValue<uint, T>(valuePart, out value);
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(short))
|
||||
{
|
||||
return ParseNumericValue<short, T>(valuePart);
|
||||
return TryParseNumericValue<short, T>(valuePart, out value);
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(ushort))
|
||||
{
|
||||
return ParseNumericValue<ushort, T>(valuePart);
|
||||
return TryParseNumericValue<ushort, T>(valuePart, out value);
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(sbyte))
|
||||
{
|
||||
return ParseNumericValue<sbyte, T>(valuePart);
|
||||
return TryParseNumericValue<sbyte, T>(valuePart, out value);
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(byte))
|
||||
{
|
||||
return ParseNumericValue<byte, T>(valuePart);
|
||||
return TryParseNumericValue<byte, T>(valuePart, out value);
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(float))
|
||||
{
|
||||
return ParseNumericValue<float, T>(valuePart);
|
||||
return TryParseNumericValue<float, T>(valuePart, out value);
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(double))
|
||||
{
|
||||
return ParseNumericValue<double, T>(valuePart);
|
||||
return TryParseNumericValue<double, T>(valuePart, out value);
|
||||
}
|
||||
|
||||
// Default parsing for other types
|
||||
return (T)Convert.ChangeType(valuePart.ToString(), typeof(T));
|
||||
try
|
||||
{
|
||||
value = (T)Convert.ChangeType(valuePart.ToString(), typeof(T));
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static R ParseNumericValue<T, R>(ReadOnlySpan<char> valuePart) where T : INumber<T> =>
|
||||
valuePart.StartsWith("0x")
|
||||
? (R)(object)T.Parse(valuePart[2..], NumberStyles.HexNumber, null)
|
||||
: (R)(object)T.Parse(valuePart, null);
|
||||
private static bool TryParseNumericValue<T, R>(ReadOnlySpan<char> valuePart, out R value) where T : INumber<T>
|
||||
{
|
||||
var ok = valuePart.StartsWith("0x")
|
||||
? T.TryParse(valuePart[2..], NumberStyles.HexNumber, null, out var parsed)
|
||||
: T.TryParse(valuePart, null, out parsed);
|
||||
|
||||
if (ok)
|
||||
{
|
||||
value = (R)(object)parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int GetEnumSize(Type enumType) =>
|
||||
Type.GetTypeCode(Enum.GetUnderlyingType(enumType)) switch
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue