diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs index 721c4ec37..b9503476e 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FluentAssertions; using Moq; using NUnit.Framework; @@ -29,6 +29,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Title = "Dexter.S08E01.EDITED.WEBRip.x264-KYR" } }; + + Mocker.SetConstant(Mocker.Resolve()); } private void GivenRestictions(string required, string ignored) @@ -123,5 +125,16 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse(); } + + [TestCase("/WEB/", true)] + [TestCase("/WEB\b/", false)] + [TestCase("/WEb/", false)] + [TestCase(@"/\.WEB/", true)] + public void should_match_perl_regex(string pattern, bool expected) + { + GivenRestictions(pattern, null); + + Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().Be(expected); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs index ffa752f94..804eddeed 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs @@ -13,11 +13,13 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { private readonly IRestrictionService _restrictionService; private readonly Logger _logger; + private readonly ITermMatcher _termMatcher; - public ReleaseRestrictionsSpecification(IRestrictionService restrictionService, Logger logger) + public ReleaseRestrictionsSpecification(ITermMatcher termMatcher, IRestrictionService restrictionService, Logger logger) { _restrictionService = restrictionService; _logger = logger; + _termMatcher = termMatcher; } public SpecificationPriority Priority => SpecificationPriority.Default; @@ -63,9 +65,9 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } - private static List ContainsAny(List terms, string title) + private List ContainsAny(List terms, string title) { - return terms.Where(t => title.ToLowerInvariant().Contains(t.ToLowerInvariant())).ToList(); + return terms.Where(t => _termMatcher.IsMatch(t, title)).ToList(); } } } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 4113248e8..400c4c766 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -1071,9 +1071,11 @@ + + diff --git a/src/NzbDrone.Core/Restrictions/PerlRegexFactory.cs b/src/NzbDrone.Core/Restrictions/PerlRegexFactory.cs new file mode 100644 index 000000000..f447f1102 --- /dev/null +++ b/src/NzbDrone.Core/Restrictions/PerlRegexFactory.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Restrictions +{ + public static class PerlRegexFactory + { + private static Regex _perlRegexFormat = new Regex(@"/(?.*)/(?[a-z]*)", RegexOptions.Compiled); + + public static bool TryCreateRegex(string pattern, out Regex regex) + { + var match = _perlRegexFormat.Match(pattern); + + if (!match.Success) + { + regex = null; + return false; + } + + regex = CreateRegex(match.Groups["pattern"].Value, match.Groups["modifiers"].Value); + return true; + } + + public static Regex CreateRegex(string pattern, string modifiers) + { + var options = GetOptions(modifiers); + + // For now we simply expect the pattern to be .net compliant. We should probably check and reject perl-specific constructs. + return new Regex(pattern, options | RegexOptions.Compiled); + } + + private static RegexOptions GetOptions(string modifiers) + { + var options = RegexOptions.None; + + foreach (var modifier in modifiers) + { + switch (modifier) + { + case 'm': + options |= RegexOptions.Multiline; + break; + + case 's': + options |= RegexOptions.Singleline; + break; + + case 'i': + options |= RegexOptions.IgnoreCase; + break; + + case 'x': + options |= RegexOptions.IgnorePatternWhitespace; + break; + + case 'n': + options |= RegexOptions.ExplicitCapture; + break; + + default: + throw new ArgumentException("Unknown or unsupported perl regex modifier: " + modifier); + } + } + + return options; + } + } +} diff --git a/src/NzbDrone.Core/Restrictions/TermMatcher.cs b/src/NzbDrone.Core/Restrictions/TermMatcher.cs new file mode 100644 index 000000000..e6bd84d89 --- /dev/null +++ b/src/NzbDrone.Core/Restrictions/TermMatcher.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using NzbDrone.Common.Cache; + +namespace NzbDrone.Core.Restrictions +{ + public interface ITermMatcher + { + bool IsMatch(string term, string value); + } + + public class TermMatcher : ITermMatcher + { + private ICached> _matcherCache; + + public TermMatcher(ICacheManager cacheManager) + { + _matcherCache = cacheManager.GetCache>(GetType()); + } + + public bool IsMatch(string term, string value) + { + return GetMatcher(term)(value); + } + + public Predicate GetMatcher(string term) + { + return _matcherCache.Get(term, () => CreateMatcherInternal(term), TimeSpan.FromHours(24)); + } + + private Predicate CreateMatcherInternal(string term) + { + Regex regex; + if (PerlRegexFactory.TryCreateRegex(term, out regex)) + { + return regex.IsMatch; + } + else + { + return new CaseInsensitiveTermMatcher(term).IsMatch; + + } + } + + private sealed class CaseInsensitiveTermMatcher + { + private readonly string _term; + + public CaseInsensitiveTermMatcher(string term) + { + _term = term.ToLowerInvariant(); + } + + public bool IsMatch(string value) + { + return value.ToLowerInvariant().Contains(_term); + } + } + } +}