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