// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Tools.DotNETCommon; namespace Tools.DotNETCommon { public class FilePatternException : Exception { public FilePatternException(string Message) : base(Message) { } public FilePatternException(string Format, params object[] Args) : base(String.Format(Format, Args)) { } public override string ToString() { return Message; } } /// /// Encapsulates a pattern containing the '?', '*', and '...' wildcards. /// public class FilePattern { /// /// Base directory for all matched files /// public readonly DirectoryReference BaseDirectory; /// /// List of tokens in the pattern. Every second token is a wildcard, other tokens are string fragments. Always has an odd number of elements. Path separators are normalized to the host platform format. /// public readonly List Tokens = new List(); /// /// Constructs a file pattern which matches a single file /// /// Location of the file public FilePattern(FileReference File) { BaseDirectory = File.Directory; Tokens.Add(File.GetFileName()); } /// /// Constructs a file pattern from the given string, resolving relative paths to the given directory. /// /// If a relative path is specified by the pattern, the root directory used to turn it into an absolute path /// The pattern to match. If the pattern ends with a directory separator, an implicit '...' is appended. public FilePattern(DirectoryReference RootDirectory, string Pattern) { // Normalize the path separators StringBuilder Text = new StringBuilder(Pattern); if(Path.DirectorySeparatorChar != '\\') { Text.Replace('\\', Path.DirectorySeparatorChar); } if(Path.DirectorySeparatorChar != '/') { Text.Replace('/', Path.DirectorySeparatorChar); } // Find the base directory, stopping when we hit a wildcard. The source directory must end with a path specification. int BaseDirectoryLen = 0; for(int Idx = 0; Idx < Text.Length; Idx++) { if(Text[Idx] == Path.DirectorySeparatorChar) { BaseDirectoryLen = Idx + 1; } else if(Text[Idx] == '?' || Text[Idx] == '*' || (Idx + 2 < Text.Length && Text[Idx] == '.' && Text[Idx + 1] == '.' && Text[Idx + 2] == '.')) { break; } } // Extract the base directory BaseDirectory = DirectoryReference.Combine(RootDirectory, Text.ToString(0, BaseDirectoryLen)); // Convert any directory wildcards ("...") into complete directory wildcards ("\\...\\"). We internally treat use "...\\" as the wildcard // token so we can correctly match zero directories. Patterns such as "foo...bar" should require at least one directory separator, so // should be converted to "foo*\\...\\*bar". for(int Idx = BaseDirectoryLen; Idx < Text.Length; Idx++) { if(Text[Idx] == '.' && Text[Idx + 1] == '.' && Text[Idx + 2] == '.') { // Insert a directory separator before if(Idx > BaseDirectoryLen && Text[Idx - 1] != Path.DirectorySeparatorChar) { Text.Insert(Idx++, '*'); Text.Insert(Idx++, Path.DirectorySeparatorChar); } // Skip past the ellipsis Idx += 3; // Insert a directory separator after if(Idx == Text.Length || Text[Idx] != Path.DirectorySeparatorChar) { Text.Insert(Idx++, Path.DirectorySeparatorChar); Text.Insert(Idx++, '*'); } } } // Parse the tokens int LastIdx = BaseDirectoryLen; for(int Idx = BaseDirectoryLen; Idx < Text.Length; Idx++) { if(Text[Idx] == '?' || Text[Idx] == '*') { Tokens.Add(Text.ToString(LastIdx, Idx - LastIdx)); Tokens.Add(Text.ToString(Idx, 1)); LastIdx = Idx + 1; } else if(Idx - 3 >= BaseDirectoryLen && Text[Idx] == Path.DirectorySeparatorChar && Text[Idx - 1] == '.' && Text[Idx - 2] == '.' && Text[Idx - 3] == '.') { Tokens.Add(Text.ToString(LastIdx, Idx - 3 - LastIdx)); Tokens.Add(Text.ToString(Idx - 3, 4)); LastIdx = Idx + 1; } } Tokens.Add(Text.ToString(LastIdx, Text.Length - LastIdx)); } /// /// A pattern without wildcards may match either a single file or directory based on context. This pattern resolves to the later as necessary, producing a new pattern. /// /// Pattern which matches a directory public FilePattern AsDirectoryPattern() { if(ContainsWildcards()) { return this; } else { StringBuilder Pattern = new StringBuilder(); foreach(string Token in Tokens) { Pattern.Append(Token); } if(Pattern.Length > 0) { Pattern.Append(Path.DirectorySeparatorChar); } Pattern.Append("..."); return new FilePattern(BaseDirectory, Pattern.ToString()); } } /// /// For a pattern that does not contain wildcards, returns the single file location /// /// Location of the referenced file public FileReference GetSingleFile() { if(Tokens.Count == 1) { return FileReference.Combine(BaseDirectory, Tokens[0]); } else { throw new InvalidOperationException("File pattern does not reference a single file"); } } /// /// Checks whether this pattern is explicitly a directory, ie. is terminated with a directory separator /// /// True if the pattern is a directory public bool EndsWithDirectorySeparator() { string LastToken = Tokens[Tokens.Count - 1]; return LastToken.Length > 0 && LastToken[LastToken.Length - 1] == Path.DirectorySeparatorChar; } /// /// Determines whether the pattern contains wildcards /// /// True if the pattern contains wildcards, false otherwise. public bool ContainsWildcards() { return Tokens.Count > 1; } /// /// Tests whether a pattern is compatible with another pattern (that is, that the number and type of wildcards match) /// /// Pattern to compare against /// Whether the patterns are compatible. public bool IsCompatibleWith(FilePattern Other) { // Check there are the same number of tokens in each pattern if(Tokens.Count != Other.Tokens.Count) { return false; } // Check all the wildcard tokens match for(int Idx = 1; Idx < Tokens.Count; Idx += 2) { if(Tokens[Idx] != Other.Tokens[Idx]) { return false; } } return true; } /// /// Converts this pattern to a C# regex format string, which matches paths relative to the base directory formatted with native directory separators /// /// The regex pattern public string GetRegexPattern() { StringBuilder Pattern = new StringBuilder("^"); Pattern.Append(Regex.Escape(Tokens[0])); for(int Idx = 1; Idx < Tokens.Count; Idx += 2) { // Append the wildcard expression if(Tokens[Idx] == "?") { Pattern.Append("([^\\/])"); } else if(Tokens[Idx] == "*") { Pattern.Append("([^\\/]*)"); } else { Pattern.AppendFormat("((?:.+{0})?)", Regex.Escape(Path.DirectorySeparatorChar.ToString())); } // Append the next sequence of characters to match Pattern.Append(Regex.Escape(Tokens[Idx + 1])); } Pattern.Append("$"); return Pattern.ToString(); } /// /// Creates a regex replacement pattern /// /// String representing the regex replacement pattern public string GetRegexReplacementPattern() { StringBuilder Pattern = new StringBuilder(); for(int Idx = 0;;Idx += 2) { // Append the escaped replacement character Pattern.Append(Tokens[Idx].Replace("$", "$$")); // Check if we've reached the end of the string if(Idx == Tokens.Count - 1) { break; } // Insert the capture Pattern.AppendFormat("${0}", (Idx / 2) + 1); } return Pattern.ToString(); } /// /// Creates a file mapping between a set of source patterns and a target pattern. All patterns should have a matching order and number of wildcards. /// /// Files to use for the mapping /// List of source patterns /// Matching output pattern /// Filter to apply to source files /// Dictionary to receive a mapping from target file to source file. An exception is thrown if multiple source files map to one target file, or a source file is also used as a target file. public static Dictionary CreateMapping(HashSet Files, ref FilePattern SourcePattern, ref FilePattern TargetPattern) { // If the source pattern ends in a directory separator, or a set of input files are specified and it doesn't contain wildcards, treat it as a full directory match if(SourcePattern.EndsWithDirectorySeparator()) { SourcePattern = new FilePattern(SourcePattern.BaseDirectory, String.Join("", SourcePattern.Tokens) + "..."); } else if(Files != null) { SourcePattern = SourcePattern.AsDirectoryPattern(); } // If we have multiple potential source files, but no wildcards in the output pattern, assume it's a directory and append the pattern from the source. if(SourcePattern.ContainsWildcards() && !TargetPattern.ContainsWildcards()) { StringBuilder NewPattern = new StringBuilder(); foreach(string Token in TargetPattern.Tokens) { NewPattern.Append(Token); } if(NewPattern.Length > 0 && NewPattern[NewPattern.Length - 1] != Path.DirectorySeparatorChar) { NewPattern.Append(Path.DirectorySeparatorChar); } foreach(string Token in SourcePattern.Tokens) { NewPattern.Append(Token); } TargetPattern = new FilePattern(TargetPattern.BaseDirectory, NewPattern.ToString()); } // If the target pattern ends with a directory separator, treat it as a full directory match if it has wildcards, or a copy of the source pattern if not if(TargetPattern.EndsWithDirectorySeparator()) { TargetPattern = new FilePattern(TargetPattern.BaseDirectory, String.Join("", TargetPattern.Tokens) + "..."); } // Handle the case where source and target pattern are both individual files Dictionary TargetFileToSourceFile = new Dictionary(); if(SourcePattern.ContainsWildcards() || TargetPattern.ContainsWildcards()) { // Check the two patterns are compatible if(!SourcePattern.IsCompatibleWith(TargetPattern)) { throw new FilePatternException("File patterns '{0}' and '{1}' do not have matching wildcards", SourcePattern, TargetPattern); } // Create a filter to match the source files FileFilter Filter = new FileFilter(FileFilterType.Exclude); Filter.Include(String.Join("", SourcePattern.Tokens)); // Apply it to the source directory List SourceFiles; if(Files == null) { SourceFiles = Filter.ApplyToDirectory(SourcePattern.BaseDirectory, true); } else { SourceFiles = CheckInputFiles(Files, SourcePattern.BaseDirectory); } // Map them onto output files FileReference[] TargetFiles = new FileReference[SourceFiles.Count]; // Get the source and target regexes string SourceRegex = SourcePattern.GetRegexPattern(); string TargetRegex = TargetPattern.GetRegexReplacementPattern(); for(int Idx = 0; Idx < SourceFiles.Count; Idx++) { string SourceRelativePath = SourceFiles[Idx].MakeRelativeTo(SourcePattern.BaseDirectory); string TargetRelativePath = Regex.Replace(SourceRelativePath, SourceRegex, TargetRegex); TargetFiles[Idx] = FileReference.Combine(TargetPattern.BaseDirectory, TargetRelativePath); } // Add them to the output map for(int Idx = 0; Idx < TargetFiles.Length; Idx++) { FileReference ExistingSourceFile; if(TargetFileToSourceFile.TryGetValue(TargetFiles[Idx], out ExistingSourceFile) && ExistingSourceFile != SourceFiles[Idx]) { throw new FilePatternException("Output file '{0}' is mapped from '{1}' and '{2}'", TargetFiles[Idx], ExistingSourceFile, SourceFiles[Idx]); } TargetFileToSourceFile[TargetFiles[Idx]] = SourceFiles[Idx]; } } else { // Just copy a single file FileReference SourceFile = SourcePattern.GetSingleFile(); if(FileReference.Exists(SourceFile)) { FileReference TargetFile = TargetPattern.GetSingleFile(); TargetFileToSourceFile[TargetFile] = SourceFile; } else { throw new FilePatternException("Source file '{0}' does not exist", SourceFile); } } // Check that no source file is also destination file foreach(FileReference SourceFile in TargetFileToSourceFile.Values) { if(TargetFileToSourceFile.ContainsKey(SourceFile)) { throw new FilePatternException("'{0}' is listed as a source and target file", SourceFile); } } // Return the map return TargetFileToSourceFile; } /// /// Checks that the given input files all exist and are under the given base directory /// /// Input files to check /// Base directory for files /// List of valid files public static List CheckInputFiles(IEnumerable InputFiles, DirectoryReference BaseDirectory) { List Files = new List(); foreach(FileReference InputFile in InputFiles) { if(!InputFile.IsUnderDirectory(BaseDirectory)) { throw new FilePatternException("Source file '{0}' is not under '{1}'", InputFile, BaseDirectory); } else if(!FileReference.Exists(InputFile)) { throw new FilePatternException("Source file '{0}' does not exist", InputFile); } else { Files.Add(InputFile); } } return Files; } /// /// Formats the pattern as a string /// /// The original representation of this pattern public override string ToString() { return BaseDirectory.ToString() + Path.DirectorySeparatorChar + String.Join("", Tokens); } } }