// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; namespace EpicGames.Core { /// /// Describes a tree of files represented by some arbitrary type. Allows manipulating files/directories in a functional manner; /// filtering a view of a certain directory, mapping files from one location to another, etc... before actually realizing those changes on disk. /// public abstract class FileSet : IEnumerable { /// /// An empty fileset /// public static FileSet Empty { get; } = new FileSetFromFiles(Enumerable.Empty<(string, FileReference)>()); /// /// Path of this tree /// public string Path { get; } /// /// Constructor /// /// Relative path within the tree public FileSet(string Path) { this.Path = Path; } /// /// Enumerate files in the current tree /// /// Sequence consisting of names and file objects public abstract IEnumerable> EnumerateFiles(); /// /// Enumerate subtrees in the current tree /// /// Sequence consisting of names and subtree objects public abstract IEnumerable> EnumerateDirectories(); /// /// Creates a file tree from a given set of files /// /// /// Tree containing the given files public static FileSet FromFile(DirectoryReference Directory, string File) { return FromFiles(new[] { (File, FileReference.Combine(Directory, File)) }); } /// /// Creates a file tree from a given set of files /// /// /// Tree containing the given files public static FileSet FromFiles(IEnumerable<(string, FileReference)> Files) { return new FileSetFromFiles(Files); } /// /// Creates a file tree from a given set of files /// /// /// Tree containing the given files public static FileSet FromFile(DirectoryReference Directory, FileReference File) { return FromFiles(Directory, new[] { File }); } /// /// Creates a file tree from a given set of files /// /// /// Tree containing the given files public static FileSet FromFiles(DirectoryReference Directory, IEnumerable Files) { return new FileSetFromFiles(Files.Select(x => (x.MakeRelativeTo(Directory), x))); } /// /// Creates a file tree from a folder on disk /// /// /// public static FileSet FromDirectory(DirectoryReference Directory) { return new FileSetFromDirectory(new DirectoryInfo(Directory.FullName)); } /// /// Creates a file tree from a folder on disk /// /// /// public static FileSet FromDirectory(DirectoryInfo DirectoryInfo) { return new FileSetFromDirectory(DirectoryInfo); } /// /// Create a tree containing files filtered by any of the given wildcards /// /// /// public FileSet Filter(string Rules) { return Filter(Rules.Split(';')); } /// /// Create a tree containing files filtered by any of the given wildcards /// /// /// public FileSet Filter(params string[] Rules) { return new FileSetFromFilter(this, new FileFilter(Rules)); } /// /// Create a tree containing files filtered by any of the given file filter objects /// /// /// public FileSet Filter(params FileFilter[] Filters) { return new FileSetFromFilter(this, Filters); } /// /// Create a tree containing the exception of files with another tree /// /// Files to exclude from the filter /// public FileSet Except(string Filter) { return Except(Filter.Split(';')); } /// /// Create a tree containing the exception of files with another tree /// /// Files to exclude from the filter /// public FileSet Except(params string[] Rules) { return new FileSetFromFilter(this, new FileFilter(Rules.Select(x => $"-{x}"))); } /// /// Create a tree containing the union of files with another tree /// /// /// /// public static FileSet Union(FileSet Lhs, FileSet Rhs) { return new FileSetFromUnion(Lhs, Rhs); } /// /// Create a tree containing the exception of files with another tree /// /// /// /// public static FileSet Except(FileSet Lhs, FileSet Rhs) { return new FileSetFromExcept(Lhs, Rhs); } /// public static FileSet operator +(FileSet Lhs, FileSet Rhs) { return Union(Lhs, Rhs); } /// public static FileSet operator -(FileSet Lhs, FileSet Rhs) { return Except(Lhs, Rhs); } /// /// Flatten to a map of files in a target directory /// /// public Dictionary Flatten() { Dictionary PathToSourceFile = new Dictionary(StringComparer.OrdinalIgnoreCase); FlattenInternal(String.Empty, PathToSourceFile); return PathToSourceFile; } private void FlattenInternal(string PathPrefix, Dictionary PathToSourceFile) { foreach ((string Path, FileReference File) in EnumerateFiles()) { PathToSourceFile[PathPrefix + Path] = File; } foreach((string Path, FileSet FileSet) in EnumerateDirectories()) { FileSet.FlattenInternal(PathPrefix + Path + "/", PathToSourceFile); } } /// /// Flatten to a map of files in a target directory /// /// public Dictionary Flatten(DirectoryReference OutputDir) { Dictionary TargetToSourceFile = new Dictionary(); foreach ((string Path, FileReference SourceFile) in Flatten()) { FileReference TargetFile = FileReference.Combine(OutputDir, Path); TargetToSourceFile[TargetFile] = SourceFile; } return TargetToSourceFile; } /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// public IEnumerator GetEnumerator() => Flatten().Values.GetEnumerator(); } /// /// File tree from a known set of files /// class FileSetFromFiles : FileSet { Dictionary Files = new Dictionary(); Dictionary SubTrees = new Dictionary(); /// /// Private constructor /// /// private FileSetFromFiles(string Path) : base(Path) { } /// /// Creates a tree from a given set of files /// /// public FileSetFromFiles(IEnumerable<(string, FileReference)> InputFiles) : this(String.Empty) { foreach ((string Path, FileReference File) in InputFiles) { string[] Fragments = Path.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); FileSetFromFiles Current = this; for (int Idx = 0; Idx < Fragments.Length - 1; Idx++) { FileSetFromFiles? Next; if (!Current.SubTrees.TryGetValue(Fragments[Idx], out Next)) { Next = new FileSetFromFiles(Current.Path + Fragments[Idx] + "/"); Current.SubTrees.Add(Fragments[Idx], Next); } Current = Next; } Current.Files.Add(Fragments[^1], File); } } /// public override IEnumerable> EnumerateFiles() => Files; /// public override IEnumerable> EnumerateDirectories() => SubTrees.Select(x => new KeyValuePair(x.Key, x.Value)); } /// /// File tree enumerated from the contents of an existing directory /// sealed class FileSetFromDirectory : FileSet { DirectoryInfo DirectoryInfo; /// /// Constructor /// public FileSetFromDirectory(DirectoryInfo DirectoryInfo) : this(DirectoryInfo, "/") { } /// /// Constructor /// public FileSetFromDirectory(DirectoryInfo DirectoryInfo, string Path) : base(Path) { this.DirectoryInfo = DirectoryInfo; } /// public override IEnumerable> EnumerateFiles() => DirectoryInfo.EnumerateFiles().Select(x => new KeyValuePair(x.Name, new FileReference(x))); /// public override IEnumerable> EnumerateDirectories() => DirectoryInfo.EnumerateDirectories().Select(x => KeyValuePair.Create(x.Name, new FileSetFromDirectory(x))); } /// /// File tree enumerated from the combination of two separate trees /// class FileSetFromUnion : FileSet { FileSet Lhs; FileSet Rhs; /// /// Constructor /// /// First file tree for the union /// Other file tree for the union public FileSetFromUnion(FileSet Lhs, FileSet Rhs) : base(Lhs.Path) { this.Lhs = Lhs; this.Rhs = Rhs; } /// public override IEnumerable> EnumerateFiles() { Dictionary Files = new Dictionary(Lhs.EnumerateFiles(), StringComparer.OrdinalIgnoreCase); foreach ((string Name, FileReference File) in Rhs.EnumerateFiles()) { FileReference? ExistingFile; if (!Files.TryGetValue(Name, out ExistingFile)) { Files.Add(Name, File); } else if (ExistingFile == null || !ExistingFile.Equals(File)) { throw new InvalidOperationException($"Conflict for contents of {Path}{Name} - could be {ExistingFile} or {File}"); } } return Files; } /// public override IEnumerable> EnumerateDirectories() { Dictionary NameToSubTree = new Dictionary(Lhs.EnumerateDirectories(), StringComparer.OrdinalIgnoreCase); foreach ((string Name, FileSet SubTree) in Rhs.EnumerateDirectories()) { FileSet? ExistingSubTree; if (NameToSubTree.TryGetValue(Name, out ExistingSubTree)) { NameToSubTree[Name] = new FileSetFromUnion(ExistingSubTree, SubTree); } else { NameToSubTree[Name] = SubTree; } } return NameToSubTree; } } /// /// File tree enumerated from the combination of two separate trees /// class FileSetFromExcept : FileSet { FileSet Lhs; FileSet Rhs; /// /// Constructor /// /// First file tree for the union /// Other file tree for the union public FileSetFromExcept(FileSet Lhs, FileSet Rhs) : base(Lhs.Path) { this.Lhs = Lhs; this.Rhs = Rhs; } /// public override IEnumerable> EnumerateFiles() { HashSet RhsFiles = new HashSet(Rhs.EnumerateFiles().Select(x => x.Key), StringComparer.OrdinalIgnoreCase); return Lhs.EnumerateFiles().Where(x => !RhsFiles.Contains(x.Key)); } /// public override IEnumerable> EnumerateDirectories() { Dictionary RhsDirs = new Dictionary(Rhs.EnumerateDirectories(), StringComparer.OrdinalIgnoreCase); foreach ((string Name, FileSet LhsSet) in Lhs.EnumerateDirectories()) { FileSet? RhsSet; if (RhsDirs.TryGetValue(Name, out RhsSet)) { yield return KeyValuePair.Create(Name, new FileSetFromExcept(LhsSet, RhsSet)); } else { yield return KeyValuePair.Create(Name, LhsSet); } } } } /// /// File tree which includes only those files which match any given filter /// /// Class containing information about a file class FileSetFromFilter : FileSet { FileSet Inner; FileFilter[] Filters; /// /// Constructor /// /// The tree to filter /// public FileSetFromFilter(FileSet Inner, params FileFilter[] Filters) : base(Inner.Path) { this.Inner = Inner; this.Filters = Filters; } /// public override IEnumerable> EnumerateFiles() { foreach (KeyValuePair Item in Inner.EnumerateFiles()) { string FilterName = Inner.Path + Item.Key; if (Filters.Any(x => x.Matches(FilterName))) { yield return Item; } } } /// public override IEnumerable> EnumerateDirectories() { foreach (KeyValuePair Item in Inner.EnumerateDirectories()) { string FilterName = Inner.Path + Item.Key; FileFilter[] PossibleFilters = Filters.Where(x => x.PossiblyMatches(FilterName)).ToArray(); if (PossibleFilters.Length > 0) { FileSetFromFilter SubTreeFilter = new FileSetFromFilter(Item.Value, PossibleFilters); yield return new KeyValuePair(Item.Key, SubTreeFilter); } } } } }