// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Security; using EpicGames.Core; namespace UnrealBuildBase { /// /// Stores the state of a directory. May or may not exist. /// public class DirectoryItem : IComparable, IEquatable { /// /// Full path to the directory on disk /// public readonly DirectoryReference Location; /// /// Cached value for whether the directory exists /// Lazy Info; /// /// Cached maps of name to subdirectory and name to file /// Lazy<(Dictionary, Dictionary)> Cache; /// /// Global map of location to item /// static ConcurrentDictionary LocationToItem = []; /// /// Constructor /// /// Path to this directory /// Information about this directory private DirectoryItem(DirectoryReference Location, DirectoryInfo Info) { this.Location = Location; Cache = new(Scan); // For some reason we need to call an extra Refresh on Linux/Mac to not get wrong results from "Exists" this.Info = OperatingSystem.IsWindows() ? new(Info) : new(() => { Info.Refresh(); return Info; }); } /// /// Accessor for map of name to subdirectory item /// Dictionary Directories => Cache.Value.Item1; /// /// Accessor for map of name to file /// Dictionary Files => Cache.Value.Item2; /// /// The name of this directory /// public string Name => Info.Value.Name; /// /// The full name of this directory /// public string FullName => Location.FullName; /// /// Whether the directory exists or not /// public bool Exists => Info.Value.Exists; /// /// The last write time of the file. /// public DateTime LastWriteTimeUtc => Info.Value.LastWriteTimeUtc; /// /// The creation time of the file. /// public DateTime CreationTimeUtc => Info.Value.CreationTimeUtc; /// /// Gets the parent directory item /// public DirectoryItem? GetParentDirectoryItem() { if (Info.Value.Parent == null) { return null; } else { return GetItemByDirectoryInfo(Info.Value.Parent); } } /// /// Gets a new directory item by combining the existing directory item with the given path fragments /// /// Base directory to append path fragments to /// The path fragments to append /// Directory item corresponding to the combined path public static DirectoryItem Combine(DirectoryItem BaseDirectory, params string[] Fragments) => GetItemByDirectoryReference(DirectoryReference.Combine(BaseDirectory.Location, Fragments)); /// /// Finds or creates a directory item from its location /// /// Path to the directory /// The directory item for this location public static DirectoryItem GetItemByPath(string Location) => GetItemByDirectoryReference(new(Location)); /// /// Finds or creates a directory item from its location /// /// Path to the directory /// The directory item for this location public static DirectoryItem GetItemByDirectoryReference(DirectoryReference Location) => LocationToItem.TryGetValue(Location, out DirectoryItem? Result) ? Result : LocationToItem.GetOrAdd(Location, new DirectoryItem(Location, new(Location.FullName))); /// /// Finds or creates a directory item from a DirectoryInfo object /// /// Path to the directory /// The directory item for this location public static DirectoryItem GetItemByDirectoryInfo(DirectoryInfo Info) => GetItemByDirectoryReference(new(Info)); /// /// Reset the contents of the directory and allow them to be fetched again /// public void ResetCachedInfo() { Info = new(() => { DirectoryInfo Info = Location.ToDirectoryInfo(); Info.Refresh(); return Info; }); if (Cache.IsValueCreated) { (Dictionary dirs, Dictionary files) = Cache.Value; foreach (DirectoryItem SubDirectory in dirs.Values) { SubDirectory.ResetCachedInfo(); } foreach (FileItem File in files.Values) { File.ResetCachedInfo(); } Cache = new(Scan); } } /// /// Resets the cached info, if the DirectoryInfo is not found don't create a new entry /// public static void ResetCachedInfo(string Path) { if (LocationToItem.TryGetValue(new DirectoryReference(Path), out DirectoryItem? Result)) { Result.ResetCachedInfo(); } } /// /// Resets all cached directory info. Significantly reduces performance; do not use unless strictly necessary. /// public static void ResetAllCachedInfo_SLOW() { LocationToItem.Values.AsParallel().ForAll(Item => { Item.Info = new Lazy(() => { DirectoryInfo Info = Item.Location.ToDirectoryInfo(); Info.Refresh(); return Info; }); Item.Cache = new(Item.Scan); }); FileItem.ResetAllCachedInfo_SLOW(); } /// /// Caches the subdirectories of this directories /// public bool CacheDirectories() => Cache.Value.Item1 != null; /// /// Enumerates all the subdirectories /// /// Sequence of subdirectory items public IEnumerable EnumerateDirectories() { CacheDirectories(); return Directories.Values; } /// /// Attempts to get a sub-directory by name /// /// Name of the directory /// If successful receives the matching directory item with this name /// True if the file exists, false otherwise public bool TryGetDirectory(string Name, [NotNullWhen(true)] out DirectoryItem? OutDirectory) { if (Name.Length > 0 && Name[0] == '.') { if (Name.Length == 1) { OutDirectory = this; return true; } else if (Name.Length == 2 && Name[1] == '.') { OutDirectory = GetParentDirectoryItem(); return OutDirectory != null; } } CacheDirectories(); return Directories.TryGetValue(Name, out OutDirectory); } /// /// Scans the directory for directories and files, used for lazy initialization. /// /// (Dictionary , Dictionary) Scan() { try { // We want to turn enumerator to a list here since using EnumerateFiles.Count and then enumerate EnumerateFiles cause two iterations of findfirst/findnext // Also, we don't check if exists to minimize kernel call count List infos = Info.Value.EnumerateFileSystemInfos().ToList(); int dirCount = 0; foreach (FileSystemInfo info in infos) { if (info.Attributes.HasFlag(FileAttributes.Directory)) { ++dirCount; } } Dictionary? newDirs = new(dirCount, FileReference.Comparer); Dictionary? newFiles = new(infos.Count - dirCount, FileReference.Comparer); foreach (FileSystemInfo info in infos) { if (info.Attributes.HasFlag(FileAttributes.Directory)) { newDirs.Add(info.Name, DirectoryItem.GetItemByDirectoryInfo((DirectoryInfo)info)); } else { FileItem FileItem = FileItem.GetItemByFileInfo((FileInfo)info); // There are folders in linux sdk that has files with same name but different casing. // Ideally FileReference.Comparer should be case sensitive on linux/mac but I don't dare changing that right now if (newFiles.TryAdd(info.Name, FileItem)) { FileItem.UpdateCachedDirectory(this); } } } return (newDirs, newFiles); } catch (DirectoryNotFoundException) { } catch (SecurityException) { } catch (UnauthorizedAccessException) { } return ([], []); } /// /// Caches the files in this directory /// public bool CacheFiles() => Cache.Value.Item2 != null; /// /// Enumerates all the files /// /// Sequence of FileItems public IEnumerable EnumerateFiles() { CacheFiles(); return Files.Values; } /// /// Check if this directory contains any files /// /// Directory search options /// True if this directory has files public bool ContainsFiles(SearchOption searchOption = SearchOption.TopDirectoryOnly) { return searchOption == SearchOption.TopDirectoryOnly ? EnumerateFiles().Any(x => x.Exists) : (EnumerateFiles().Any(x => x.Exists) || EnumerateDirectories().Any(x => x.ContainsFiles(searchOption))); } /// /// Attempts to get a file from this directory by name. Unlike creating a file item and checking whether it exists, this will /// not create a permanent FileItem object if it does not exist. /// /// Name of the file /// If successful receives the matching file item with this name /// True if the file exists, false otherwise public bool TryGetFile(string Name, [NotNullWhen(true)] out FileItem? OutFile) { CacheFiles(); return Files.TryGetValue(Name, out OutFile); } /// /// Formats this object as a string for debugging /// /// Location of the directory public override string ToString() => Location.ToString(); /// /// Writes out all the enumerated files full names sorted to OutFile /// public static void WriteDebugFileWithAllEnumeratedFiles(string OutFile) { SortedSet AllFiles = []; foreach (DirectoryItem Item in DirectoryItem.LocationToItem.Values) { if (Item.Files != null) { foreach (FileItem File in Item.EnumerateFiles()) { AllFiles.Add(File.FullName); } } } File.WriteAllLines(OutFile, AllFiles); } #region IComparable, IEquatbale /// public int CompareTo(DirectoryItem? other) => Location.CompareTo(other?.Location); /// public bool Equals(DirectoryItem? other) => Location.Equals(other?.Location); /// public override bool Equals(object? obj) { if (ReferenceEquals(this, obj)) { return true; } if (obj is null) { return false; } return Equals(obj as DirectoryItem); } /// public override int GetHashCode() => Location.GetHashCode(); public static bool operator ==(DirectoryItem? left, DirectoryItem? right) { if (left is null) { return right is null; } return left.Equals(right); } public static bool operator !=(DirectoryItem? left, DirectoryItem? right) { return !(left == right); } public static bool operator <(DirectoryItem? left, DirectoryItem? right) { return left is null ? right is not null : left.CompareTo(right) < 0; } public static bool operator <=(DirectoryItem? left, DirectoryItem? right) { return left is null || left.CompareTo(right) <= 0; } public static bool operator >(DirectoryItem? left, DirectoryItem? right) { return left is not null && left.CompareTo(right) > 0; } public static bool operator >=(DirectoryItem? left, DirectoryItem? right) { return left is null ? right is null : left.CompareTo(right) >= 0; } #endregion } /// /// Helper functions for serialization /// public static class DirectoryItemExtensionMethods { /// /// Read a directory item from a binary archive /// /// Reader to serialize data from /// Instance of the serialized directory item public static DirectoryItem? ReadDirectoryItem(this BinaryArchiveReader Reader) { // Use lambda that doesn't require anything to be captured thus eliminating an allocation. return Reader.ReadObjectReference(Reader => DirectoryItem.GetItemByDirectoryReference(Reader.ReadDirectoryReferenceNotNull())); } /// /// Write a directory item to a binary archive /// /// Writer to serialize data to /// Directory item to write public static void WriteDirectoryItem(this BinaryArchiveWriter Writer, DirectoryItem DirectoryItem) { // Use lambda that doesn't require anything to be captured thus eliminating an allocation. Writer.WriteObjectReference(DirectoryItem, (Writer, DirectoryItem) => Writer.WriteDirectoryReference(DirectoryItem.Location)); } /// /// Writes a directory reference to a binary archive /// /// The writer to output data to /// The item to write public static void WriteCompactDirectoryReference(this BinaryArchiveWriter Writer, DirectoryReference Directory) { DirectoryItem Item = DirectoryItem.GetItemByDirectoryReference(Directory); Writer.WriteDirectoryItem(Item); } /// /// Reads a directory reference from a binary archive /// /// Reader to serialize data from /// New directory reference instance public static DirectoryReference ReadCompactDirectoryReference(this BinaryArchiveReader Reader) { return Reader.ReadDirectoryItem()!.Location; } } }