diff --git a/Projects/UOContent.Tests/Tests/Engines/AdvancedSearch/AdvancedSearchUtilitiesTests.cs b/Projects/UOContent.Tests/Tests/Engines/AdvancedSearch/AdvancedSearchUtilitiesTests.cs new file mode 100644 index 000000000..986e0d724 --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Engines/AdvancedSearch/AdvancedSearchUtilitiesTests.cs @@ -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", "=")); + } +} diff --git a/Projects/UOContent/Engines/Advanced Search/AdvancedSearchUtilities.cs b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchUtilities.cs index 8d7dae3c4..9532fe20a 100644 --- a/Projects/UOContent/Engines/Advanced Search/AdvancedSearchUtilities.cs +++ b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchUtilities.cs @@ -43,77 +43,80 @@ public static class AdvancedSearchUtilities if (propertyType == typeof(long)) { - var parsedValue = ParseValue(valuePart); - return CompareNumeric((long)propertyValue!, parsedValue, operatorSpan); + return TryParseValue(valuePart, out var parsedValue) && + CompareNumeric((long)propertyValue!, parsedValue, operatorSpan); } if (propertyType == typeof(ulong)) { - var parsedValue = ParseValue(valuePart); - return CompareNumeric((ulong)propertyValue!, parsedValue, operatorSpan); + return TryParseValue(valuePart, out var parsedValue) && + CompareNumeric((ulong)propertyValue!, parsedValue, operatorSpan); } if (propertyType == typeof(int)) { - var parsedValue = ParseValue(valuePart); - return CompareNumeric((int)propertyValue!, parsedValue, operatorSpan); + return TryParseValue(valuePart, out var parsedValue) && + CompareNumeric((int)propertyValue!, parsedValue, operatorSpan); } if (propertyType == typeof(uint)) { - var parsedValue = ParseValue(valuePart); - return CompareNumeric((uint)propertyValue!, parsedValue, operatorSpan); + return TryParseValue(valuePart, out var parsedValue) && + CompareNumeric((uint)propertyValue!, parsedValue, operatorSpan); } if (propertyType == typeof(short)) { - var parsedValue = ParseValue(valuePart); - return CompareNumeric((short)propertyValue!, parsedValue, operatorSpan); + return TryParseValue(valuePart, out var parsedValue) && + CompareNumeric((short)propertyValue!, parsedValue, operatorSpan); } if (propertyType == typeof(ushort)) { - var parsedValue = ParseValue(valuePart); - return CompareNumeric((ushort)propertyValue!, parsedValue, operatorSpan); + return TryParseValue(valuePart, out var parsedValue) && + CompareNumeric((ushort)propertyValue!, parsedValue, operatorSpan); } if (propertyType == typeof(sbyte)) { - var parsedValue = ParseValue(valuePart); - return CompareNumeric((sbyte)propertyValue!, parsedValue, operatorSpan); + return TryParseValue(valuePart, out var parsedValue) && + CompareNumeric((sbyte)propertyValue!, parsedValue, operatorSpan); } if (propertyType == typeof(byte)) { - var parsedValue = ParseValue(valuePart); - return CompareNumeric((byte)propertyValue!, parsedValue, operatorSpan); + return TryParseValue(valuePart, out var parsedValue) && + CompareNumeric((byte)propertyValue!, parsedValue, operatorSpan); } if (propertyType == typeof(float)) { - var parsedValue = ParseValue(valuePart); - return Compare((float)propertyValue!, parsedValue, valuePart, operatorSpan); + return TryParseValue(valuePart, out var parsedValue) && + Compare((float)propertyValue!, parsedValue, valuePart, operatorSpan); } if (propertyType == typeof(double)) { - var parsedValue = ParseValue(valuePart); - return Compare((double)propertyValue!, parsedValue, valuePart, operatorSpan); + return TryParseValue(valuePart, out var parsedValue) && + Compare((double)propertyValue!, parsedValue, valuePart, operatorSpan); } if (propertyType == typeof(string)) { - var parsedValue = ParseValue(valuePart); - return Compare((string)propertyValue!, parsedValue, operatorSpan); + return TryParseValue(valuePart, out var parsedValue) && + Compare((string)propertyValue!, parsedValue, operatorSpan); } if (propertyType == typeof(TimeSpan)) { - var parsedValue = ParseValue(valuePart); - return Compare((TimeSpan)propertyValue!, parsedValue, operatorSpan); + return TryParseValue(valuePart, out var parsedValue) && + Compare((TimeSpan)propertyValue!, parsedValue, operatorSpan); } if (propertyType == typeof(DateTime)) { - var parsedValue = ParseValue(valuePart); - return Compare((DateTime)propertyValue!, parsedValue, operatorSpan); + return TryParseValue(valuePart, out var parsedValue) && + Compare((DateTime)propertyValue!, parsedValue, operatorSpan); } if (propertyType == typeof(bool)) { - var parsedValue = ParseValue(valuePart); - return Compare((bool)propertyValue!, parsedValue, operatorSpan); + return TryParseValue(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(valuePart); - return CompareReference(propertyValue!, parsedValue, operatorSpan); + return TryParseValue(valuePart, out var parsedValue) && + CompareReference(propertyValue!, parsedValue, operatorSpan); } return false; @@ -249,6 +253,12 @@ public static class AdvancedSearchUtilities }; public static T ParseValue(ReadOnlySpan valuePart) + { + TryParseValue(valuePart, out T value); + return value; + } + + internal static bool TryParseValue(ReadOnlySpan 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(valuePart); + return TryParseNumericValue(valuePart, out value); } if (typeof(T) == typeof(ulong)) { - return ParseNumericValue(valuePart); + return TryParseNumericValue(valuePart, out value); } if (typeof(T) == typeof(int)) { - return ParseNumericValue(valuePart); + return TryParseNumericValue(valuePart, out value); } if (typeof(T) == typeof(uint)) { - return ParseNumericValue(valuePart); + return TryParseNumericValue(valuePart, out value); } if (typeof(T) == typeof(short)) { - return ParseNumericValue(valuePart); + return TryParseNumericValue(valuePart, out value); } if (typeof(T) == typeof(ushort)) { - return ParseNumericValue(valuePart); + return TryParseNumericValue(valuePart, out value); } if (typeof(T) == typeof(sbyte)) { - return ParseNumericValue(valuePart); + return TryParseNumericValue(valuePart, out value); } if (typeof(T) == typeof(byte)) { - return ParseNumericValue(valuePart); + return TryParseNumericValue(valuePart, out value); } if (typeof(T) == typeof(float)) { - return ParseNumericValue(valuePart); + return TryParseNumericValue(valuePart, out value); } if (typeof(T) == typeof(double)) { - return ParseNumericValue(valuePart); + return TryParseNumericValue(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(ReadOnlySpan valuePart) where T : INumber => - valuePart.StartsWith("0x") - ? (R)(object)T.Parse(valuePart[2..], NumberStyles.HexNumber, null) - : (R)(object)T.Parse(valuePart, null); + private static bool TryParseNumericValue(ReadOnlySpan valuePart, out R value) where T : INumber + { + 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