// 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);
}
}
}
}
}