From 454aad2170771ae71cf2b742f4c19e4aaa4f8520 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 20 Mar 2017 13:01:03 +0100 Subject: [PATCH] U4-9543 - fix shadow filesystem filter --- src/Umbraco.Core/IO/ShadowFileSystem.cs | 85 ++++++++++++----- src/Umbraco.Tests/IO/ShadowFileSystemTests.cs | 95 ------------------- 2 files changed, 59 insertions(+), 121 deletions(-) diff --git a/src/Umbraco.Core/IO/ShadowFileSystem.cs b/src/Umbraco.Core/IO/ShadowFileSystem.cs index cb2f4e346c..8645399a0b 100644 --- a/src/Umbraco.Core/IO/ShadowFileSystem.cs +++ b/src/Umbraco.Core/IO/ShadowFileSystem.cs @@ -218,11 +218,11 @@ namespace Umbraco.Core.IO var normPath = NormPath(path); var shadows = Nodes.Where(kvp => IsChild(normPath, kvp.Key)).ToArray(); var files = filter != null ? _fs.GetFiles(path, filter) : _fs.GetFiles(path); - var regexFilter = FilterToRegex(filter); + var wildcard = filter == null ? null : new WildcardExpression(filter); return files .Except(shadows.Where(kvp => (kvp.Value.IsFile && kvp.Value.IsDelete) || kvp.Value.IsDir) .Select(kvp => kvp.Key)) - .Union(shadows.Where(kvp => kvp.Value.IsFile && kvp.Value.IsExist && FilterByRegex(kvp.Key, regexFilter)).Select(kvp => kvp.Key)) + .Union(shadows.Where(kvp => kvp.Value.IsFile && kvp.Value.IsExist && (wildcard == null || wildcard.IsMatch(kvp.Key))).Select(kvp => kvp.Key)) .Distinct(); } @@ -326,32 +326,65 @@ namespace Umbraco.Core.IO _sfs.AddFile(path, physicalPath, overrideIfExists, copy); Nodes[normPath] = new ShadowNode(false, false); } - - /// - /// Helper function for filtering keys by Regex if a filter is specified. - /// - /// - /// - /// - internal static bool FilterByRegex(string input, string regexFilter) - { - if (regexFilter == null) return true; - return regexFilter != string.Empty && Regex.IsMatch(input, regexFilter); - } - /// - /// Transforms a filter pattern into a Regex pattern - /// - /// - /// - /// - /// Appending '$' only if not containing wildcard is stupid and broken. - /// It is however what seems to be what they're doing in .NET so we need to match the functionality. - /// - internal static string FilterToRegex(string pattern) + // copied from System.Web.Util.Wildcard internal + internal class WildcardExpression { - if (pattern == null) return null; - return "^" + Regex.Escape(pattern).Replace("\\*", ".*").Replace("\\?", ".") + (pattern.Contains("*") ? string.Empty : "$"); + private readonly string _pattern; + private readonly bool _caseInsensitive; + private Regex _regex; + + private static Regex metaRegex = new Regex("[\\+\\{\\\\\\[\\|\\(\\)\\.\\^\\$]"); + private static Regex questRegex = new Regex("\\?"); + private static Regex starRegex = new Regex("\\*"); + private static Regex commaRegex = new Regex(","); + private static Regex slashRegex = new Regex("(?=/)"); + private static Regex backslashRegex = new Regex("(?=[\\\\:])"); + + public WildcardExpression(string pattern, bool caseInsensitive = true) + { + _pattern = pattern; + _caseInsensitive = caseInsensitive; + } + + private void EnsureRegex(string pattern) + { + if (_regex != null) return; + + var options = RegexOptions.None; + + // match right-to-left (for speed) if the pattern starts with a * + + if (pattern.Length > 0 && pattern[0] == '*') + options = RegexOptions.RightToLeft | RegexOptions.Singleline; + else + options = RegexOptions.Singleline; + + // case insensitivity + + if (_caseInsensitive) + options |= RegexOptions.IgnoreCase | RegexOptions.CultureInvariant; + + // Remove regex metacharacters + + pattern = metaRegex.Replace(pattern, "\\$0"); + + // Replace wildcard metacharacters with regex codes + + pattern = questRegex.Replace(pattern, "."); + pattern = starRegex.Replace(pattern, ".*"); + pattern = commaRegex.Replace(pattern, "\\z|\\A"); + + // anchor the pattern at beginning and end, and return the regex + + _regex = new Regex("\\A" + pattern + "\\z", options); + } + + public bool IsMatch(string input) + { + EnsureRegex(_pattern); + return _regex.IsMatch(input); + } } } } diff --git a/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs b/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs index 03cc9364ce..80110698b5 100644 --- a/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs +++ b/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs @@ -805,101 +805,6 @@ namespace Umbraco.Tests.IO Assert.AreEqual(3, sfsFiles.Length); } - [Test] - [Ignore("Does not work on all environments, Directory.GetFiles is broken.")] - public void ShadowGetFilesUsingWildcardAndSingleCharacterFilter() - { - // Arrange - var path = IOHelper.MapPath("FileSysTests"); - Directory.CreateDirectory(path); - Directory.CreateDirectory(path + "/ShadowTests"); - Directory.CreateDirectory(path + "/ShadowSystem"); - - var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore"); - var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore"); - var ss = new ShadowFileSystem(fs, sfs); - - // Act - File.WriteAllText(path + "/ShadowTests/f2.txt", "foo"); - File.WriteAllText(path + "/ShadowTests/f2.doc", "foo"); - File.WriteAllText(path + "/ShadowTests/f2.docx", "foo"); - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) - ss.AddFile("f1.txt", ms); - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) - ss.AddFile("f1.doc", ms); - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) - ss.AddFile("f1.docx", ms); - - // Assert - // ensure we get 6 files from the shadow - var getFiles = ss.GetFiles(string.Empty); - Assert.AreEqual(6, getFiles.Count()); - var getFilesWithWildcardSinglecharFilter = ss.GetFiles(string.Empty, "*.d?c"); - - Assert.AreEqual(4, getFilesWithWildcardSinglecharFilter.Count()); - var getFilesWithWildcardSinglecharFilter2 = ss.GetFiles(string.Empty, "*.d?cx"); - Assert.AreEqual(2, getFilesWithWildcardSinglecharFilter2.Count()); - - var fsFiles = fs.GetFiles(string.Empty).ToArray(); - Assert.AreEqual(3, fsFiles.Length); - var sfsFiles = sfs.GetFiles(string.Empty).ToArray(); - Assert.AreEqual(3, sfsFiles.Length); - } - - [Test] - [Ignore("Does not work on all environments, Directory.GetFiles is broken.")] - public void ShadowFileSystemFilterIsAsBrokenAsRealFileSystemFilter() - { - // Arrange - var path = IOHelper.MapPath("FileSysTests"); - Directory.CreateDirectory(path); - Directory.CreateDirectory(path + "/filter"); - // create files on disk and create a "fake" list of files to verify filters against - File.WriteAllText(path + "/filter/f1.txt", "foo"); - File.WriteAllText(path + "/filter/f1.doc", "foo"); - File.WriteAllText(path + "/filter/f1.docx", "foo"); - var files = new string[] - { - "f1.txt", - "f1.doc", - "f1.docx", - }; - var filter1 = ""; - var filter2 = "*"; - var filter3 = "*.doc"; - var filter4 = "*.d?c"; - var filter5 = "f1.doc"; - var filter6 = "f1.d?c"; - var filter7 = "**.d?c"; - var filter8 = "f*.doc"; - - // Act & Assert - var result1Disk = Directory.GetFiles(path + "/filter/", filter1).Select(Path.GetFileName).OrderBy(x => x).ToArray(); - var result1Fake = files.Where(x => ShadowFileSystem.FilterByRegex(x, ShadowFileSystem.FilterToRegex(filter1))).OrderBy(x => x).ToArray(); - Assert.AreEqual(result1Disk, result1Fake); - var result2Disk = Directory.GetFiles(path + "/filter/", filter2).Select(Path.GetFileName).OrderBy(x => x).ToArray(); - var result2Fake = files.Where(x => ShadowFileSystem.FilterByRegex(x, ShadowFileSystem.FilterToRegex(filter2))).OrderBy(x => x).ToArray(); - Assert.AreEqual(result2Disk, result2Fake); - var result3Disk = Directory.GetFiles(path + "/filter/", filter3).Select(Path.GetFileName).OrderBy(x => x).ToArray(); - var result3Fake = files.Where(x => ShadowFileSystem.FilterByRegex(x, ShadowFileSystem.FilterToRegex(filter3))).OrderBy(x => x).ToArray(); - Assert.AreEqual(result3Disk, result3Fake); - var result4Disk = Directory.GetFiles(path + "/filter/", filter4).Select(Path.GetFileName).OrderBy(x => x).ToArray(); - var result4Fake = files.Where(x => ShadowFileSystem.FilterByRegex(x, ShadowFileSystem.FilterToRegex(filter4))).OrderBy(x => x).ToArray(); - Assert.AreEqual(result4Disk, result4Fake); - var result5Disk = Directory.GetFiles(path + "/filter/", filter5).Select(Path.GetFileName).OrderBy(x => x).ToArray(); - var result5Fake = files.Where(x => ShadowFileSystem.FilterByRegex(x, ShadowFileSystem.FilterToRegex(filter5))).OrderBy(x => x).ToArray(); - Assert.AreEqual(result5Disk, result5Fake); - var result6Disk = Directory.GetFiles(path + "/filter/", filter6).Select(Path.GetFileName).OrderBy(x => x).ToArray(); - var result6Fake = files.Where(x => ShadowFileSystem.FilterByRegex(x, ShadowFileSystem.FilterToRegex(filter6))).OrderBy(x => x).ToArray(); - Assert.AreEqual(result6Disk, result6Fake); - var result7Disk = Directory.GetFiles(path + "/filter/", filter7).Select(Path.GetFileName).OrderBy(x => x).ToArray(); - var result7Fake = files.Where(x => ShadowFileSystem.FilterByRegex(x, ShadowFileSystem.FilterToRegex(filter7))).OrderBy(x => x).ToArray(); - Assert.AreEqual(result7Disk, result7Fake); - var result8Disk = Directory.GetFiles(path + "/filter/", filter8).Select(Path.GetFileName).OrderBy(x => x).ToArray(); - var result8Fake = files.Where(x => ShadowFileSystem.FilterByRegex(x, ShadowFileSystem.FilterToRegex(filter8))).OrderBy(x => x).ToArray(); - Assert.AreEqual(result8Disk, result8Fake); - } - /// /// Returns the full paths of the files on the disk. /// Note that this will be the *actual* path of the file, meaning a file existing on the initialized FS