/// <summary> /// Default constructor /// </summary> public FileFilter(FileFilterType DefaultType = FileFilterType.Exclude) { RootNode = new FileFilterNode(null, ""); DefaultNode = new FileFilterNode(RootNode, "..."); DefaultNode.Type = DefaultType; }
/// <summary> /// Default constructor. /// </summary> public FileFilterNode(FileFilterNode InParent, string InPattern) { Parent = InParent; Pattern = InPattern; RuleNumber = -1; Type = FileFilterType.Exclude; }
/// <summary> /// Returns the highest possible rule number which can match the given list of input tokens, assuming that the list of input tokens is incomplete. /// </summary> /// <param name="CurrentNode">The current node being checked</param> /// <param name="Tokens">The tokens to match</param> /// <param name="TokenIdx">Current token index</param> /// <param name="CurrentBestRuleNumber">The highest rule number seen so far. Used to optimize tree traversals.</param> /// <returns>New highest rule number</returns> int HighestPossibleIncludeMatch(FileFilterNode CurrentNode, string[] Tokens, int TokenIdx, int CurrentBestRuleNumber) { // If we've matched all the input tokens, check if this rule is better than any other we've seen if (TokenIdx == Tokens.Length) { return(Math.Max(CurrentBestRuleNumber, CurrentNode.MaxIncludeRuleNumber)); } // Test all the branches for one that matches int BestRuleNumber = CurrentBestRuleNumber; if (CurrentNode.MaxIncludeRuleNumber > BestRuleNumber) { foreach (FileFilterNode Branch in CurrentNode.Branches) { if (Branch.Pattern == "...") { if (Branch.MaxIncludeRuleNumber > BestRuleNumber) { BestRuleNumber = Branch.MaxIncludeRuleNumber; } } else { if (Branch.IsMatch(Tokens[TokenIdx])) { BestRuleNumber = HighestPossibleIncludeMatch(Branch, Tokens, TokenIdx + 1, BestRuleNumber); } } } } return(BestRuleNumber); }
/// <summary> /// Determines whether it's possible for anything within the given folder name to match the filter. Useful to early out of recursive file searches. /// </summary> /// <param name="FolderName">File to match</param> /// <returns>True if the file passes the filter</returns> public bool PossiblyMatches(string FolderName) { string[] Tokens = FolderName.Trim('/', '\\').Split('/', '\\'); FileFilterNode MatchingNode = FindMatchingNode(RootNode, Tokens.Union(new string[] { "" }).ToArray(), 0, DefaultNode); return(MatchingNode.Type == FileFilterType.Include || HighestPossibleIncludeMatch(RootNode, Tokens, 0, MatchingNode.RuleNumber) > MatchingNode.RuleNumber); }
/// <summary> /// Determines whether the given file matches the filter /// </summary> /// <param name="FileName">File to match</param> /// <returns>True if the file passes the filter</returns> public bool Matches(string FileName) { string[] Tokens = FileName.TrimStart('/', '\\').Split('/', '\\'); FileFilterNode MatchingNode = FindMatchingNode(RootNode, Tokens, 0, DefaultNode); return(MatchingNode.Type == FileFilterType.Include); }
/// <summary> /// Finds the node which matches a given list of tokens. /// </summary> /// <param name="CurrentNode"></param> /// <param name="Tokens"></param> /// <param name="TokenIdx"></param> /// <param name="CurrentBestNode"></param> /// <returns></returns> FileFilterNode FindMatchingNode(FileFilterNode CurrentNode, string[] Tokens, int TokenIdx, FileFilterNode CurrentBestNode) { // If we've matched all the input tokens, check if this rule is better than any other we've seen if (TokenIdx == Tokens.Length) { return((CurrentNode.RuleNumber > CurrentBestNode.RuleNumber) ? CurrentNode : CurrentBestNode); } // If there is no rule under the current node which is better than the current best node, early out if (CurrentNode.MaxIncludeRuleNumber <= CurrentBestNode.RuleNumber && CurrentNode.MaxExcludeRuleNumber <= CurrentBestNode.RuleNumber) { return(CurrentBestNode); } // Test all the branches for one that matches FileFilterNode BestNode = CurrentBestNode; foreach (FileFilterNode Branch in CurrentNode.Branches) { if (Branch.Pattern == "...") { for (int NextTokenIdx = Tokens.Length; NextTokenIdx >= TokenIdx; NextTokenIdx--) { BestNode = FindMatchingNode(Branch, Tokens, NextTokenIdx, BestNode); } } else { if (Branch.IsMatch(Tokens[TokenIdx])) { BestNode = FindMatchingNode(Branch, Tokens, TokenIdx + 1, BestNode); } } } return(BestNode); }
/// <summary> /// Returns the highest possible rule number which can match the given list of input tokens, assuming that the list of input tokens is incomplete. /// </summary> /// <param name="CurrentNode">The current node being checked</param> /// <param name="Tokens">The tokens to match</param> /// <param name="TokenIdx">Current token index</param> /// <param name="CurrentBestRuleNumber">The highest rule number seen so far. Used to optimize tree traversals.</param> /// <returns>New highest rule number</returns> int HighestPossibleIncludeMatch(FileFilterNode CurrentNode, string[] Tokens, int TokenIdx, int CurrentBestRuleNumber) { // If we've matched all the input tokens, check if this rule is better than any other we've seen if (TokenIdx == Tokens.Length) { return Math.Max(CurrentBestRuleNumber, CurrentNode.MaxIncludeRuleNumber); } // Test all the branches for one that matches int BestRuleNumber = CurrentBestRuleNumber; if (CurrentNode.MaxIncludeRuleNumber > BestRuleNumber) { foreach (FileFilterNode Branch in CurrentNode.Branches) { if (Branch.Pattern == "...") { if (Branch.MaxIncludeRuleNumber > BestRuleNumber) { BestRuleNumber = Branch.MaxIncludeRuleNumber; } } else { if (Branch.IsMatch(Tokens[TokenIdx])) { BestRuleNumber = HighestPossibleIncludeMatch(Branch, Tokens, TokenIdx + 1, BestRuleNumber); } } } } return BestRuleNumber; }
/// <summary> /// Finds the node which matches a given list of tokens. /// </summary> /// <param name="CurrentNode"></param> /// <param name="Tokens"></param> /// <param name="TokenIdx"></param> /// <param name="CurrentBestNode"></param> /// <returns></returns> FileFilterNode FindMatchingNode(FileFilterNode CurrentNode, string[] Tokens, int TokenIdx, FileFilterNode CurrentBestNode) { // If we've matched all the input tokens, check if this rule is better than any other we've seen if (TokenIdx == Tokens.Length) { return (CurrentNode.RuleNumber > CurrentBestNode.RuleNumber) ? CurrentNode : CurrentBestNode; } // If there is no rule under the current node which is better than the current best node, early out if (CurrentNode.MaxIncludeRuleNumber <= CurrentBestNode.RuleNumber && CurrentNode.MaxExcludeRuleNumber <= CurrentBestNode.RuleNumber) { return CurrentBestNode; } // Test all the branches for one that matches FileFilterNode BestNode = CurrentBestNode; foreach (FileFilterNode Branch in CurrentNode.Branches) { if (Branch.Pattern == "...") { for (int NextTokenIdx = Tokens.Length; NextTokenIdx >= TokenIdx; NextTokenIdx--) { BestNode = FindMatchingNode(Branch, Tokens, NextTokenIdx, BestNode); } } else { if (Branch.IsMatch(Tokens[TokenIdx])) { BestNode = FindMatchingNode(Branch, Tokens, TokenIdx + 1, BestNode); } } } return BestNode; }
/// <summary> /// Adds an include or exclude rule to the filter /// </summary> /// <param name="Pattern">The pattern which the rule should match</param> /// <param name="bInclude">Whether to include or exclude files matching this rule</param> public void AddRule(string Pattern, FileFilterType Type) { string NormalizedPattern = Pattern.Replace('\\', '/'); // We don't want a slash at the start, but if there was not one specified, it's not anchored to the root of the tree. if (NormalizedPattern.StartsWith("/")) { NormalizedPattern = NormalizedPattern.Substring(1); } else if(!NormalizedPattern.StartsWith("...")) { NormalizedPattern = ".../" + NormalizedPattern; } // All directories indicate a wildcard match if (NormalizedPattern.EndsWith("/")) { NormalizedPattern += "..."; } // Replace any directory wildcards mid-string for (int Idx = NormalizedPattern.IndexOf("..."); Idx != -1; Idx = NormalizedPattern.IndexOf("...", Idx)) { if (Idx > 0 && NormalizedPattern[Idx - 1] != '/') { NormalizedPattern = NormalizedPattern.Insert(Idx, "*/"); Idx++; } Idx += 3; if (Idx < NormalizedPattern.Length && NormalizedPattern[Idx] != '/') { NormalizedPattern = NormalizedPattern.Insert(Idx, "/*"); Idx += 2; } } // Split the pattern into fragments string[] BranchPatterns = NormalizedPattern.Split('/'); // Add it into the tree FileFilterNode LastNode = RootNode; foreach (string BranchPattern in BranchPatterns) { FileFilterNode NextNode = LastNode.Branches.FirstOrDefault(x => x.Pattern == BranchPattern); if (NextNode == null) { NextNode = new FileFilterNode(LastNode, BranchPattern); LastNode.Branches.Add(NextNode); } LastNode = NextNode; } // We've reached the end of the pattern, so mark it as a leaf node Rules.Add(LastNode); LastNode.RuleNumber = Rules.Count - 1; LastNode.Type = Type; // Update the maximums along that path for (FileFilterNode UpdateNode = LastNode; UpdateNode != null; UpdateNode = UpdateNode.Parent) { if (Type == FileFilterType.Include) { UpdateNode.MaxIncludeRuleNumber = LastNode.RuleNumber; } else { UpdateNode.MaxExcludeRuleNumber = LastNode.RuleNumber; } } }
/// <summary> /// Adds an include or exclude rule to the filter /// </summary> /// <param name="Pattern">The pattern which the rule should match</param> /// <param name="bInclude">Whether to include or exclude files matching this rule</param> public void AddRule(string Pattern, FileFilterType Type) { string NormalizedPattern = Pattern.Replace('\\', '/'); // We don't want a slash at the start, but if there was not one specified, it's not anchored to the root of the tree. if (NormalizedPattern.StartsWith("/")) { NormalizedPattern = NormalizedPattern.Substring(1); } else if (!NormalizedPattern.StartsWith("...")) { NormalizedPattern = ".../" + NormalizedPattern; } // All directories indicate a wildcard match if (NormalizedPattern.EndsWith("/")) { NormalizedPattern += "..."; } // Replace any directory wildcards mid-string for (int Idx = NormalizedPattern.IndexOf("..."); Idx != -1; Idx = NormalizedPattern.IndexOf("...", Idx)) { if (Idx > 0 && NormalizedPattern[Idx - 1] != '/') { NormalizedPattern = NormalizedPattern.Insert(Idx, "*/"); Idx++; } Idx += 3; if (Idx < NormalizedPattern.Length && NormalizedPattern[Idx] != '/') { NormalizedPattern = NormalizedPattern.Insert(Idx, "/*"); Idx += 2; } } // Split the pattern into fragments string[] BranchPatterns = NormalizedPattern.Split('/'); // Add it into the tree FileFilterNode LastNode = RootNode; foreach (string BranchPattern in BranchPatterns) { FileFilterNode NextNode = LastNode.Branches.FirstOrDefault(x => x.Pattern == BranchPattern); if (NextNode == null) { NextNode = new FileFilterNode(LastNode, BranchPattern); LastNode.Branches.Add(NextNode); } LastNode = NextNode; } // We've reached the end of the pattern, so mark it as a leaf node Rules.Add(LastNode); LastNode.RuleNumber = Rules.Count - 1; LastNode.Type = Type; // Update the maximums along that path for (FileFilterNode UpdateNode = LastNode; UpdateNode != null; UpdateNode = UpdateNode.Parent) { if (Type == FileFilterType.Include) { UpdateNode.MaxIncludeRuleNumber = LastNode.RuleNumber; } else { UpdateNode.MaxExcludeRuleNumber = LastNode.RuleNumber; } } }