diff --git a/Projects/UOContent.Tests/Tests/Engines/AdvancedSearch/AdvancedSearchWorkerTests.cs b/Projects/UOContent.Tests/Tests/Engines/AdvancedSearch/AdvancedSearchWorkerTests.cs new file mode 100644 index 000000000..53a884770 --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Engines/AdvancedSearch/AdvancedSearchWorkerTests.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Concurrent; +using Server; +using Server.Engines.AdvancedSearch; +using Server.Items; +using Server.Tests; +using Xunit; + +namespace UOContent.Tests; + +[Collection("Sequential UOContent Tests")] +public class AdvancedSearchWorkerTests +{ + // An item whose property test path will throw when evaluated. + private sealed class ThrowingItem : Item + { + public ThrowingItem() : base(0x1) { } + public ThrowingItem(Serial s) : base(s) { } + public string Boom => throw new InvalidOperationException("boom"); + } + + [Fact] + public void Worker_FilterThrows_DoesNotEscape_ReturnsNoMatch() + { + var worker = new AdvancedSearchThreadWorker(); + var results = new ConcurrentQueue(); + var ignore = new ConcurrentQueue(); + var filter = new AdvancedSearchFilter + { + FilterPropertyTest = true, + PropertyTest = "Boom=1", // reflection GetValue -> throws + }; + + var item = new ThrowingItem(); + + try + { + worker.Wake(new WorldLocation(Point3D.Zero, Map.Felucca), filter, results, ignore); + worker.Push(item); + worker.Sleep(); // drains; must not crash the test process + + Assert.Empty(results); + } + finally + { + item.Delete(); + worker.Exit(); + } + } + + [Fact] + public void Worker_DeletedEntity_IsSkipped() + { + var worker = new AdvancedSearchThreadWorker(); + var results = new ConcurrentQueue(); + var ignore = new ConcurrentQueue(); + var filter = new AdvancedSearchFilter(); // no filters -> everything matches + + var item = new Item(0x1); + item.Delete(); + + try + { + worker.Wake(new WorldLocation(Point3D.Zero, Map.Felucca), filter, results, ignore); + worker.Push(item); + worker.Sleep(); + + Assert.Empty(results); + } + finally + { + worker.Exit(); + } + } +} diff --git a/Projects/UOContent/Engines/Advanced Search/AdvancedSearchThreadWorker.cs b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchThreadWorker.cs index d63c0a442..57ec309f1 100644 --- a/Projects/UOContent/Engines/Advanced Search/AdvancedSearchThreadWorker.cs +++ b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchThreadWorker.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Reflection; using System.Threading; using Server.Items; +using Server.Logging; using Server.Mobiles; using Server.Multis; @@ -10,6 +11,8 @@ namespace Server.Engines.AdvancedSearch; public class AdvancedSearchThreadWorker { + private static readonly ILogger _logger = LogFactory.GetLogger(typeof(AdvancedSearchThreadWorker)); + private readonly Thread _thread; private readonly AutoResetEvent _startEvent; // Main thread tells the thread to start working private readonly AutoResetEvent _stopEvent; // Main thread waits for the worker finish draining @@ -101,6 +104,24 @@ public class AdvancedSearchThreadWorker } private AdvancedSearchResult DoEntitySearch(IEntity entity) + { + if (entity == null || entity.Deleted) + { + return null; + } + + try + { + return DoEntitySearchCore(entity); + } + catch (Exception ex) + { + _logger.Warning(ex, "AdvancedSearch: filter threw for {Entity}; skipping", entity); + return null; + } + } + + private AdvancedSearchResult DoEntitySearchCore(IEntity entity) { if (_filter.HideValidInternalMap) { @@ -317,6 +338,12 @@ public class AdvancedSearchThreadWorker private static bool EvaluateSingleExpression(IEntity entity, ReadOnlySpan expression) { + expression = expression.Trim(); + if (expression.Length == 0) + { + return false; + } + var negate = false; if (expression[0] == '~') {