// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; 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([]); /// /// Path of this tree /// public string Path { get; } /// /// Constructor /// /// Relative path within the tree protected FileSet(string path) { 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 /// /// Base directory /// File to add /// Tree containing the given files public static FileSet FromFile(DirectoryReference directory, string file) { return FromFiles([(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 /// /// Base directory for the file /// File to include /// Tree containing the given files public static FileSet FromFile(DirectoryReference directory, FileReference file) { return FromFiles(directory, [file]); } /// /// Creates a file tree from a given set of files /// /// Base directory for the fileset /// Files to include /// 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}"), FileFilterType.Include)); } /// /// 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 = []; 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 { readonly Dictionary _files = []; readonly Dictionary _subTrees = []; /// /// 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 { readonly DirectoryInfo _directoryInfo; /// /// Constructor /// public FileSetFromDirectory(DirectoryInfo directoryInfo) : this(directoryInfo, "/") { } /// /// Constructor /// public FileSetFromDirectory(DirectoryInfo directoryInfo, string path) : base(path) { _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, $"{Path}{x.Name}/"))); } /// /// File tree enumerated from the combination of two separate trees /// class FileSetFromUnion : FileSet { readonly FileSet _lhs; readonly FileSet _rhs; /// /// Constructor /// /// First file tree for the union /// Other file tree for the union public FileSetFromUnion(FileSet lhs, FileSet rhs) : base(lhs.Path) { _lhs = lhs; _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 { readonly FileSet _lhs; readonly FileSet _rhs; /// /// Constructor /// /// First file tree for the union /// Other file tree for the union public FileSetFromExcept(FileSet lhs, FileSet rhs) : base(lhs.Path) { _lhs = lhs; _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 FileSetFromFilter : FileSet { readonly FileSet _inner; readonly FileFilter[] _filters; /// /// Constructor /// /// The tree to filter /// public FileSetFromFilter(FileSet inner, params FileFilter[] filters) : base(inner.Path) { _inner = inner; _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); } } } } }