diff --git a/Duplicati/Library/Utility/FilterExpression.cs b/Duplicati/Library/Utility/FilterExpression.cs index 80f7bbf69..d3ed4ba93 100644 --- a/Duplicati/Library/Utility/FilterExpression.cs +++ b/Duplicati/Library/Utility/FilterExpression.cs @@ -48,13 +48,23 @@ namespace Duplicati.Library.Utility /// Represents a filter that can comprise multiple filter strings /// public class FilterExpression : IFilter - { + { private struct FilterEntry { public readonly FilterType Type; public readonly string Filter; public readonly System.Text.RegularExpressions.Regex Regexp; + /// + /// The single wildcard character (DOS style) + /// + private const char SINGLE_WILDCARD = '?'; + /// + /// The multiple wildcard character (DOS style) + /// + private const char MULTIPLE_WILDCARD = '*'; + + private static readonly System.Text.RegularExpressions.RegexOptions REGEXP_OPTIONS = Library.Utility.Utility.IsFSCaseSensitive ? System.Text.RegularExpressions.RegexOptions.Compiled : System.Text.RegularExpressions.RegexOptions.Compiled | System.Text.RegularExpressions.RegexOptions.IgnoreCase; public FilterEntry(string filter) @@ -73,12 +83,92 @@ namespace Duplicati.Library.Utility } else { - this.Type = (filter.Contains("*") || filter.Contains("?")) ? FilterType.Wildcard : FilterType.Simple; - this.Filter = filter; + this.Type = (filter.Contains(MULTIPLE_WILDCARD) || filter.Contains(SINGLE_WILDCARD)) ? FilterType.Wildcard : FilterType.Simple; + this.Filter = Utility.IsFSCaseSensitive ? filter.ToUpper() : filter; this.Regexp = new System.Text.RegularExpressions.Regex(Library.Utility.Utility.ConvertGlobbingToRegExp(filter), REGEXP_OPTIONS); } } + /// + /// Tests whether specified string can be matched agains provided pattern string. Pattern may contain single- and multiple-replacing + /// wildcard characters. + /// + /// String which is matched against the pattern. + /// Pattern against which string is matched. + /// true if matches the string ; otherwise false. + /// From: http://www.c-sharpcorner.com/uploadfile/b81385/efficient-string-matching-algorithm-with-use-of-wildcard-characters/ + private static bool IsWildcardMatch(string input, string pattern) + { + int[] inputPosStack = new int[(input.Length + 1) * (pattern.Length + 1)]; // Stack containing input positions that should be tested for further matching + int[] patternPosStack = new int[inputPosStack.Length]; // Stack containing pattern positions that should be tested for further matching + int stackPos = -1; // Points to last occupied entry in stack; -1 indicates that stack is empty + bool[,] pointTested = new bool[input.Length + 1, pattern.Length + 1]; // Each true value indicates that input position vs. pattern position has been tested + int inputPos = 0; // Position in input matched up to the first multiple wildcard in pattern + int patternPos = 0; // Position in pattern matched up to the first multiple wildcard in pattern + // Match beginning of the string until first multiple wildcard in pattern + while (inputPos < input.Length && patternPos < pattern.Length && pattern[patternPos] != MULTIPLE_WILDCARD && (input[inputPos] == pattern[patternPos] || pattern[patternPos] == SINGLE_WILDCARD)) + { + inputPos++; + patternPos++; + } + + + // Push this position to stack if it points to end of pattern or to a general wildcard + if (patternPos == pattern.Length || pattern[patternPos] == MULTIPLE_WILDCARD) + { + pointTested[inputPos, patternPos] = true; + inputPosStack[++stackPos] = inputPos; + patternPosStack[stackPos] = patternPos; + } + bool matched = false; + // Repeat matching until either string is matched against the pattern or no more parts remain on stack to test + while (stackPos >= 0 && !matched) + { + inputPos = inputPosStack[stackPos]; // Pop input and pattern positions from stack + patternPos = patternPosStack[stackPos--]; // Matching will succeed if rest of the input string matches rest of the pattern + + // Modified from original version to match zero or more characters + //if (inputPos == input.Length && patternPos == pattern.Length) + + if (inputPos == input.Length && (patternPos == pattern.Length || (patternPos == pattern.Length - 1 && pattern[patternPos] == MULTIPLE_WILDCARD))) + matched = true; // Reached end of both pattern and input string, hence matching is successful + else + { + // First character in next pattern block is guaranteed to be multiple wildcard + // So skip it and search for all matches in value string until next multiple wildcard character is reached in pattern + for(int curInputStart = inputPos; curInputStart < input.Length; curInputStart++) + { + int curInputPos = curInputStart; + int curPatternPos = patternPos + 1; + if (curPatternPos == pattern.Length) + { // Pattern ends with multiple wildcard, hence rest of the input string is matched with that character + curInputPos = input.Length; + } + else + { + while (curInputPos < input.Length && curPatternPos < pattern.Length && pattern[curPatternPos] != MULTIPLE_WILDCARD && + (input[curInputPos] == pattern[curPatternPos] || pattern[curPatternPos] == SINGLE_WILDCARD)) + { + curInputPos++; + curPatternPos++; + } + } + // If we have reached next multiple wildcard character in pattern without breaking the matching sequence, then we have another candidate for full match + // This candidate should be pushed to stack for further processing + // At the same time, pair (input position, pattern position) will be marked as tested, so that it will not be pushed to stack later again + if (((curPatternPos == pattern.Length && curInputPos == input.Length) || (curPatternPos < pattern.Length && pattern[curPatternPos] == MULTIPLE_WILDCARD)) + && !pointTested[curInputPos, curPatternPos]) + { + pointTested[curInputPos, curPatternPos] = true; + inputPosStack[++stackPos] = curInputPos; + patternPosStack[stackPos] = curPatternPos; + } + } + } + } + return matched; + } + /// /// Gets a value indicating if the filter matches the path /// @@ -90,6 +180,7 @@ namespace Duplicati.Library.Utility case FilterType.Simple: return string.Equals(this.Filter, path, Library.Utility.Utility.ClientFilenameStringComparision); case FilterType.Wildcard: + return IsWildcardMatch(Utility.IsFSCaseSensitive ? path.ToUpper() : path, this.Filter); case FilterType.Regexp: var m = this.Regexp.Match(path); return m.Success && m.Length == path.Length;