From 328477a1c6cbd7207f6dbac9e92a51fdb8372cd5 Mon Sep 17 00:00:00 2001 From: Qstick Date: Mon, 5 Aug 2019 20:47:46 -0400 Subject: [PATCH] New: Required/Ignored restrictions now support /pattern/ regular expressions Co-Authored-By: taloth --- ...ReleaseRestrictionsSpecificationFixture.cs | 13 ++++ .../ReleaseRestrictionsSpecification.cs | 12 ++-- src/NzbDrone.Core/NzbDrone.Core.csproj | 2 + .../Restrictions/PerlRegexFactory.cs | 72 +++++++++++++++++++ src/NzbDrone.Core/Restrictions/TermMatcher.cs | 63 ++++++++++++++++ 5 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 src/NzbDrone.Core/Restrictions/PerlRegexFactory.cs create mode 100644 src/NzbDrone.Core/Restrictions/TermMatcher.cs diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs index f06408a5c..877c9932b 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs @@ -29,6 +29,8 @@ public void Setup() Title = "Dexter.S08E01.EDITED.WEBRip.x264-KYR" } }; + + Mocker.SetConstant(Mocker.Resolve()); } private void GivenRestictions(string required, string ignored) @@ -123,5 +125,16 @@ public void should_be_false_when_release_contains_one_restricted_word_and_one_re Subject.IsSatisfiedBy(_remoteMovie, 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(_remoteMovie, null).Accepted.Should().Be(expected); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs index 0e74886be..945baf2f3 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs @@ -11,13 +11,15 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { public class ReleaseRestrictionsSpecification : IDecisionEngineSpecification { - private readonly IRestrictionService _restrictionService; private readonly Logger _logger; + private readonly IRestrictionService _restrictionService; + private readonly ITermMatcher _termMatcher; - public ReleaseRestrictionsSpecification(IRestrictionService restrictionService, Logger logger) + public ReleaseRestrictionsSpecification(ITermMatcher termMatcher, IRestrictionService restrictionService, Logger logger) { - _restrictionService = restrictionService; _logger = logger; + _restrictionService = restrictionService; + _termMatcher = termMatcher; } public SpecificationPriority Priority => SpecificationPriority.Default; @@ -63,9 +65,9 @@ public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase se 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 aed188618..9c22b9ebe 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -1201,9 +1201,11 @@ + + diff --git a/src/NzbDrone.Core/Restrictions/PerlRegexFactory.cs b/src/NzbDrone.Core/Restrictions/PerlRegexFactory.cs new file mode 100644 index 000000000..90fd60400 --- /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; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Restrictions/TermMatcher.cs b/src/NzbDrone.Core/Restrictions/TermMatcher.cs new file mode 100644 index 000000000..e3fd33227 --- /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); + } + } + } +} \ No newline at end of file