diff --git a/Duplicati/Library/Utility/FilterExpression.cs b/Duplicati/Library/Utility/FilterExpression.cs
index 1570df6a2..ea23eba06 100644
--- a/Duplicati/Library/Utility/FilterExpression.cs
+++ b/Duplicati/Library/Utility/FilterExpression.cs
@@ -76,7 +76,7 @@ namespace Duplicati.Library.Utility
/// The regular expression version of the filter
///
public readonly Regex Regexp;
-
+
///
/// The single wildcard character (DOS style)
///
@@ -116,7 +116,14 @@ namespace Duplicati.Library.Utility
{
this.Type = FilterType.Regexp;
this.Filter = filter.Substring(1, filter.Length - 2);
- this.Regexp = new Regex(this.Filter, REGEXP_OPTIONS);
+ if (Filter.StartsWith("^") && Filter.EndsWith("$"))
+ {
+ this.Regexp = new Regex(this.Filter, REGEXP_OPTIONS);
+ }
+ else
+ {
+ this.Regexp = new Regex("^(" + this.Filter + ")$", REGEXP_OPTIONS);
+ }
}
else if (filter.StartsWith("{", StringComparison.Ordinal) && filter.EndsWith("}", StringComparison.Ordinal))
{
@@ -185,14 +192,14 @@ namespace Duplicati.Library.Utility
regexString = "(" + string.Join(")|(", regexStrings) + ")";
}
- result = new Regex(regexString, REGEXP_OPTIONS);
+ result = new Regex("^(" + regexString + ")$", REGEXP_OPTIONS);
FilterEntry.filterGroupRegexCache[filterGroup] = result;
return result;
}
}
-
+
///
/// Tests whether specified string can be matched against provided pattern string. Pattern may contain single- and multiple-replacing
/// wildcard characters.
@@ -215,7 +222,7 @@ namespace Duplicati.Library.Utility
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)
{
@@ -236,10 +243,10 @@ namespace Duplicati.Library.Utility
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++)
+ for (int curInputStart = inputPos; curInputStart < input.Length; curInputStart++)
{
int curInputPos = curInputStart;
int curPatternPos = patternPos + 1;
@@ -259,7 +266,7 @@ namespace Duplicati.Library.Utility
// 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))
+ if (((curPatternPos == pattern.Length && curInputPos == input.Length) || (curPatternPos < pattern.Length && pattern[curPatternPos] == MULTIPLE_WILDCARD))
&& !pointTested[curInputPos, curPatternPos])
{
pointTested[curInputPos, curPatternPos] = true;
@@ -271,7 +278,7 @@ namespace Duplicati.Library.Utility
}
return matched;
}
-
+
///
/// Gets a value indicating if the filter matches the path
///
@@ -289,7 +296,7 @@ namespace Duplicati.Library.Utility
var m = this.Regexp.Match(path);
return m.Success && m.Length == path.Length;
default:
- return false;
+ return false;
}
}
@@ -308,12 +315,12 @@ namespace Duplicati.Library.Utility
}
}
}
-
+
///
/// The internal list of expressions
///
private readonly List m_filters;
-
+
///
/// Gets the type of the filter
///
@@ -341,7 +348,7 @@ namespace Duplicati.Library.Utility
else
throw new InvalidOperationException($"Cannot extract simple list when the type is: {this.Type}");
}
-
+
///
/// Gets a value indicating if the filter matches the path
///
@@ -355,14 +362,14 @@ namespace Duplicati.Library.Utility
match = null;
return false;
}
-
+
if (m_filters.Any(x => x.Matches(path)))
{
match = this;
result = this.Result;
return true;
}
-
+
match = null;
return false;
}
@@ -371,7 +378,7 @@ namespace Duplicati.Library.Utility
public string GetFilterHash()
{
var hash = MD5HashHelper.GetHash(m_filters?.Select(x => x.Filter));
- return Utility.ByteArrayAsHexString(hash);
+ return Utility.ByteArrayAsHexString(hash);
}
///
@@ -399,26 +406,26 @@ namespace Duplicati.Library.Utility
public FilterExpression(IEnumerable filter, bool result = true)
{
this.Result = result;
-
+
if (filter == null)
{
this.Type = FilterType.Empty;
return;
}
-
+
m_filters = Compact(
(from n in filter
- let nx = new FilterEntry(n)
- where nx.Type != FilterType.Empty
- select nx)
+ let nx = new FilterEntry(n)
+ where nx.Type != FilterType.Empty
+ select nx)
);
-
+
if (m_filters.Count == 0)
this.Type = FilterType.Empty;
else
this.Type = m_filters.Max((a) => a.Type);
}
-
+
private static IEnumerable Expand(string filter)
{
if (string.IsNullOrWhiteSpace(filter))
@@ -438,7 +445,7 @@ namespace Duplicati.Library.Utility
return filter.Split(new char[] { System.IO.Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries);
}
-
+
private static List Compact(IEnumerable items)
{
var r = new List();
@@ -482,7 +489,7 @@ namespace Duplicati.Library.Utility
if (combined.Length > 0)
r.Add(new FilterEntry("[" + combined.Append("]")));
- return r;
+ return r;
}
///
@@ -520,7 +527,7 @@ namespace Duplicati.Library.Utility
// Check for cached results
if (filter != null)
- lock(_matchLock)
+ lock (_matchLock)
if (_matchFallbackLookup.TryGetValue(filter, out cacheLookup))
{
includes = cacheLookup.Item1;
@@ -553,7 +560,7 @@ namespace Duplicati.Library.Utility
}
// Populate the cache
- lock(_matchLock)
+ lock (_matchLock)
{
if (_matchFallbackLookup.Count > 10)
_matchFallbackLookup.Remove(_matchFallbackLookup.Keys.Skip(new Random().Next(0, _matchFallbackLookup.Count)).First());
@@ -575,7 +582,7 @@ namespace Duplicati.Library.Utility
match = null;
return true;
}
-
+
bool result;
if (filter.Matches(path, out result, out match))
return result;
@@ -598,7 +605,7 @@ namespace Duplicati.Library.Utility
}
}
-
+
///
/// Combine the specified filter expressions.
///
@@ -642,12 +649,12 @@ namespace Duplicati.Library.Utility
{
if (this.Empty)
return "";
-
- return
+
+ return
"(" +
string.Join(") || (",
(from n in m_filters
- select n.ToString())
+ select n.ToString())
) +
")";
}
@@ -663,7 +670,7 @@ namespace Duplicati.Library.Utility
return
(from n in m_filters
- select $"{(this.Result ? "+" : "-")}{n.ToString()}"
+ select $"{(this.Result ? "+" : "-")}{n.ToString()}"
).ToArray();
}
@@ -675,7 +682,7 @@ namespace Duplicati.Library.Utility
{
if (filter == null || filter.Empty)
return new string[0];
-
+
IEnumerable res = new string[0];
var work = new Stack();
work.Push(filter);
@@ -709,7 +716,7 @@ namespace Duplicati.Library.Utility
return null;
IFilter res = null;
- foreach(var n in filters)
+ foreach (var n in filters)
{
bool include;
if (n.StartsWith("+", StringComparison.Ordinal))
diff --git a/Duplicati/Library/Utility/Utility.cs b/Duplicati/Library/Utility/Utility.cs
index ba620206f..c83243532 100644
--- a/Duplicati/Library/Utility/Utility.cs
+++ b/Duplicati/Library/Utility/Utility.cs
@@ -209,7 +209,7 @@ namespace Duplicati.Library.Utility
//Replace the globbing expressions with the corresponding regular expressions
globexp = globexp.Replace('?', '.').Replace("*", ".*");
- return globexp;
+ return "^" + globexp + "$";
}
///
diff --git a/Duplicati/UnitTest/FilterTest.cs b/Duplicati/UnitTest/FilterTest.cs
index a510fe37f..b63e77fe8 100644
--- a/Duplicati/UnitTest/FilterTest.cs
+++ b/Duplicati/UnitTest/FilterTest.cs
@@ -67,7 +67,7 @@ namespace Duplicati.UnitTest
// Create a fileset with all data present
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
{
- IBackupResults backupResults = c.Backup(new string[] {DATAFOLDER});
+ IBackupResults backupResults = c.Backup(new string[] { DATAFOLDER });
Assert.AreEqual(0, backupResults.Errors.Count());
Assert.AreEqual(0, backupResults.Warnings.Count());
}
@@ -92,7 +92,7 @@ namespace Duplicati.UnitTest
testopts["ignore-filenames"] = "exclude.me";
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
{
- IBackupResults backupResults = c.Backup(new string[] {DATAFOLDER});
+ IBackupResults backupResults = c.Backup(new string[] { DATAFOLDER });
Assert.AreEqual(0, backupResults.Errors.Count());
Assert.AreEqual(0, backupResults.Warnings.Count());
}
@@ -117,7 +117,7 @@ namespace Duplicati.UnitTest
testopts["exclude-empty-folders"] = "true";
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
{
- IBackupResults backupResults = c.Backup(new string[] {DATAFOLDER});
+ IBackupResults backupResults = c.Backup(new string[] { DATAFOLDER });
Assert.AreEqual(0, backupResults.Errors.Count());
Assert.AreEqual(0, backupResults.Warnings.Count());
}
@@ -142,7 +142,7 @@ namespace Duplicati.UnitTest
var excludefilter = new Library.Utility.FilterExpression($"*{System.IO.Path.DirectorySeparatorChar}myfile.txt", false);
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
{
- IBackupResults backupResults = c.Backup(new string[] {DATAFOLDER}, excludefilter);
+ IBackupResults backupResults = c.Backup(new string[] { DATAFOLDER }, excludefilter);
Assert.AreEqual(0, backupResults.Errors.Count());
Assert.AreEqual(0, backupResults.Warnings.Count());
}
@@ -167,7 +167,7 @@ namespace Duplicati.UnitTest
File.Delete(Path.Combine(source, "toplevel", "normal", "standard.txt"));
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
{
- IBackupResults backupResults = c.Backup(new string[] {DATAFOLDER}, excludefilter);
+ IBackupResults backupResults = c.Backup(new string[] { DATAFOLDER }, excludefilter);
Assert.AreEqual(0, backupResults.Errors.Count());
Assert.AreEqual(0, backupResults.Warnings.Count());
}
@@ -219,5 +219,39 @@ namespace Duplicati.UnitTest
Assert.IsFalse(filter.Matches(entry.Value, out _, out _));
}
}
+
+ [Test]
+ [Category("Filter")]
+ public static void CombineRegexp()
+ {
+ FilterExpression f1 = new FilterExpression(@"[/(a|b)/]");
+ FilterExpression f2 = new FilterExpression(@"[/a/c/]");
+ FilterExpression f3 = new FilterExpression(@"/b/c/");
+ FilterExpression f4 = new FilterExpression(@"[/b/c/d/]");
+ FilterExpression combined = FilterExpression.Combine(f1, FilterExpression.Combine(f2, FilterExpression.Combine(f3, f4)));
+
+ List shouldMatch = new List()
+ {
+ "/a/",
+ "/b/",
+ "/a/c/",
+ "/b/c/",
+ "/b/c/d/"
+ };
+ List shouldNotMatch = new List()
+ {
+ "/b/d/",
+ "/b/d/e",
+ "/a/d/",
+ };
+ foreach (string s in shouldMatch)
+ {
+ Assert.IsTrue(combined.Matches(s, out _, out _));
+ }
+ foreach (string s in shouldNotMatch)
+ {
+ Assert.IsFalse(combined.Matches(s, out _, out _));
+ }
+ }
}
}