// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace EpicGames.Core { /// /// Representation of an absolute file path. Allows fast hashing and comparisons. /// [Serializable] public class FileReference : FileSystemReference, IEquatable, IComparable { /// /// Dummy enum to allow invoking the constructor which takes a sanitized full path /// public enum Sanitize { None } /// /// Default constructor. /// /// Path to this file public FileReference(string inPath) : base(Path.GetFullPath(inPath)) { if(FullName[^1] == '\\' || FullName[^1] == '/') { throw new ArgumentException("File names may not be terminated by a path separator character"); } } /// /// Construct a FileReference from a FileInfo object. /// /// Path to this file public FileReference(FileInfo inInfo) : base(inInfo.FullName) { } /// /// Default constructor. /// /// The full sanitized path public FileReference(string fullName, Sanitize _) : base(fullName) { } /// /// Create a FileReference from a string. If the string is null, returns a null FileReference. /// /// FileName for the string /// Returns a FileReference representing the given string, or null. [return: NotNullIfNotNull("fileName")] public static FileReference? FromString(string? fileName) { if(String.IsNullOrEmpty(fileName)) { return null; } else { return new FileReference(fileName); } } /// /// Gets the file name without path information /// /// A string containing the file name public string GetFileName() { return Path.GetFileName(FullName); } /// /// Gets the file name without path information or an extension /// /// A string containing the file name without an extension public string GetFileNameWithoutExtension() { return Path.GetFileNameWithoutExtension(FullName); } /// /// Gets the file name without path or any extensions /// /// A string containing the file name without an extension public string GetFileNameWithoutAnyExtensions() { int startIdx = FullName.LastIndexOf(Path.DirectorySeparatorChar) + 1; int endIdx = FullName.IndexOf('.', startIdx); if (endIdx < startIdx) { return FullName.Substring(startIdx); } else { return FullName.Substring(startIdx, endIdx - startIdx); } } /// /// Gets the extension for this filename /// /// A string containing the extension of this filename public string GetExtension() { return Path.GetExtension(FullName); } /// /// Change the file's extension to something else /// /// The new extension /// A FileReference with the same path and name, but with the new extension public FileReference ChangeExtension(string? extension) { string newFullName = Path.ChangeExtension(FullName, extension); return new FileReference(newFullName, Sanitize.None); } /// /// Gets the directory containing this file /// /// A new directory object representing the directory containing this object public DirectoryReference Directory { get { int parentLength = FullName.LastIndexOf(Path.DirectorySeparatorChar); if (parentLength == 2 && FullName[1] == ':') { // windows root detected (C:) parentLength++; } if (parentLength == 0 && FullName[0] == Path.DirectorySeparatorChar) { // nix style root (/) detected parentLength = 1; } return new DirectoryReference(FullName.Substring(0, parentLength), DirectoryReference.Sanitize.None); } } /// /// Combine several fragments with a base directory, to form a new filename /// /// The base directory /// Fragments to combine with the base directory /// The new file name public static FileReference Combine(DirectoryReference baseDirectory, params string[] fragments) { string fullName = FileSystemReference.CombineStrings(baseDirectory, fragments); return new FileReference(fullName, Sanitize.None); } /// /// Append a string to the end of a filename /// /// The base file reference /// Suffix to be appended /// The new file reference public static FileReference operator +(FileReference a, string b) { return new FileReference(a.FullName + b, Sanitize.None); } /// /// Compares two filesystem object names for equality. Uses the canonical name representation, not the display name representation. /// /// First object to compare. /// Second object to compare. /// True if the names represent the same object, false otherwise public static bool operator ==(FileReference? a, FileReference? b) { if (a is null) { return b is null; } else if (b is null) { return false; } else { return a.FullName.Equals(b.FullName, Comparison); } } /// /// Compares two filesystem object names for inequality. Uses the canonical name representation, not the display name representation. /// /// First object to compare. /// Second object to compare. /// False if the names represent the same object, true otherwise public static bool operator !=(FileReference? a, FileReference? b) { return !(a == b); } /// /// Compares against another object for equality. /// /// other instance to compare. /// True if the names represent the same object, false otherwise public override bool Equals(object? obj) => obj is FileReference file && file == this; /// /// Compares against another object for equality. /// /// other instance to compare. /// True if the names represent the same object, false otherwise public bool Equals(FileReference? obj) { return obj == this; } /// /// Returns a hash code for this object /// /// public override int GetHashCode() { return Comparer.GetHashCode(FullName); } /// public int CompareTo(FileReference? other) => Comparer.Compare(FullName, other?.FullName); /// /// Helper function to create a remote file reference. Unlike normal FileReference objects, these aren't converted to a full path in the local filesystem, but are /// left as they are passed in. /// /// The absolute path in the remote file system /// New file reference public static FileReference MakeRemote(string absolutePath) { return new FileReference(absolutePath, Sanitize.None); } /// /// Makes a file location writeable; /// /// Location of the file public static void MakeWriteable(FileReference location) { if(Exists(location)) { FileAttributes attributes = GetAttributes(location); if((attributes & FileAttributes.ReadOnly) != 0) { SetAttributes(location, attributes & ~FileAttributes.ReadOnly); } } } /// /// Finds the correct case to match the location of this file on disk. Uses the given case for parts of the path that do not exist. /// /// The path to find the correct case for /// Location of the file with the correct case public static FileReference FindCorrectCase(FileReference location) { return new FileReference(FileUtils.FindCorrectCase(location.ToFileInfo())); } /// /// Constructs a FileInfo object from this reference /// /// New FileInfo object public FileInfo ToFileInfo() { return new FileInfo(FullName); } #region System.IO.File methods /// /// Copies a file from one location to another /// /// Location of the source file /// Location of the target file public static void Copy(FileReference sourceLocation, FileReference targetLocation) { File.Copy(sourceLocation.FullName, targetLocation.FullName); } /// /// Copies a file from one location to another /// /// Location of the source file /// Location of the target file /// Whether to overwrite the file in the target location public static void Copy(FileReference sourceLocation, FileReference targetLocation, bool bOverwrite) { File.Copy(sourceLocation.FullName, targetLocation.FullName, bOverwrite); } /// /// Deletes this file /// public static void Delete(FileReference location) { File.Delete(location.FullName); } /// /// Determines whether the given filename exists /// /// True if it exists, false otherwise public static bool Exists(FileReference location) { return File.Exists(location.FullName); } /// /// Gets the attributes for a file /// /// Location of the file /// Attributes for the file public static FileAttributes GetAttributes(FileReference location) { return File.GetAttributes(location.FullName); } /// /// Gets the time that the file was last written to /// /// Location of the file /// Last write time, in local time public static DateTime GetLastWriteTime(FileReference location) { return File.GetLastWriteTime(location.FullName); } /// /// Gets the time that the file was last written to /// /// Location of the file /// Last write time, in UTC time public static DateTime GetLastWriteTimeUtc(FileReference location) { return File.GetLastWriteTimeUtc(location.FullName); } /// /// Moves a file from one location to another /// /// Location of the source file /// Location of the target file public static void Move(FileReference sourceLocation, FileReference targetLocation) { File.Move(sourceLocation.FullName, targetLocation.FullName); } /// /// Moves a file from one location to another /// /// Location of the source file /// Location of the target file /// Whether to overwrite the file in the target location public static void Move(FileReference sourceLocation, FileReference targetLocation, bool overwrite) { File.Move(sourceLocation.FullName, targetLocation.FullName, overwrite); } /// /// Opens a FileStream on the specified path with read/write access /// /// Location of the file /// Mode to use when opening the file /// New filestream for the given file public static FileStream Open(FileReference location, FileMode mode) { return File.Open(location.FullName, mode); } /// /// Opens a FileStream on the specified path /// /// Location of the file /// Mode to use when opening the file /// Sharing mode for the new file /// New filestream for the given file public static FileStream Open(FileReference location, FileMode mode, FileAccess access) { return File.Open(location.FullName, mode, access); } /// /// Opens a FileStream on the specified path /// /// Location of the file /// Mode to use when opening the file /// Access mode for the new file /// Sharing mode for the open file /// New filestream for the given file public static FileStream Open(FileReference location, FileMode mode, FileAccess access, FileShare share) { return File.Open(location.FullName, mode, access, share); } /// /// Reads the contents of a file /// /// Location of the file /// Byte array containing the contents of the file public static byte[] ReadAllBytes(FileReference location) { return File.ReadAllBytes(location.FullName); } /// /// Reads the contents of a file /// /// Location of the file /// Byte array containing the contents of the file public static Task ReadAllBytesAsync(FileReference location, CancellationToken cancellationToken = default) { return File.ReadAllBytesAsync(location.FullName, cancellationToken); } /// /// Reads the contents of a file /// /// Location of the file /// Contents of the file as a single string public static string ReadAllText(FileReference location) { using (FileStream fs = new FileStream(location.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 4 * 1024, FileOptions.SequentialScan)) { using (StreamReader sr = new StreamReader(fs, Encoding.UTF8, true)) { // Try to read the whole file into a buffer created by hand. This avoids a LOT of memory allocations which in turn reduces the // GC stress on the system. Removing the StreamReader would be nice in the future. long RawFileLength = fs.Length; char[] InitialBuffer = new char[RawFileLength]; int ReadLength = sr.Read(InitialBuffer, 0, (int)RawFileLength); if (sr.EndOfStream) { return new String(InitialBuffer, 0, ReadLength); } else { string Remaining = sr.ReadToEnd(); return String.Concat(new ReadOnlySpan(InitialBuffer, 0, ReadLength), Remaining); } } } } /// /// Reads the contents of a file /// /// Location of the file /// Encoding of the file /// Contents of the file as a single string public static string ReadAllText(FileReference location, Encoding encoding) { return File.ReadAllText(location.FullName, encoding); } /// /// Reads the contents of a file /// /// Location of the file /// Contents of the file as a single string public static Task ReadAllTextAsync(FileReference location, CancellationToken cancellationToken = default) { return File.ReadAllTextAsync(location.FullName, cancellationToken); } /// /// Reads the contents of a file /// /// Location of the file /// Encoding of the file /// Contents of the file as a single string public static Task ReadAllTextAsync(FileReference location, Encoding encoding, CancellationToken cancellationToken = default) { return File.ReadAllTextAsync(location.FullName, encoding, cancellationToken); } /// /// Reads the contents of a file /// /// Location of the file /// String array containing the contents of the file public static string[] ReadAllLines(FileReference location) { return File.ReadAllLines(location.FullName); } /// /// Reads the contents of a file /// /// Location of the file /// The encoding to use when parsing the file /// String array containing the contents of the file public static string[] ReadAllLines(FileReference location, Encoding encoding) { return File.ReadAllLines(location.FullName, encoding); } /// /// Reads the contents of a file /// /// Location of the file /// String array containing the contents of the file public static Task ReadAllLinesAsync(FileReference location, CancellationToken cancellationToken = default) { return File.ReadAllLinesAsync(location.FullName, cancellationToken); } /// /// Reads the contents of a file /// /// Location of the file /// The encoding to use when parsing the file /// String array containing the contents of the file public static Task ReadAllLinesAsync(FileReference location, Encoding encoding, CancellationToken cancellationToken = default) { return File.ReadAllLinesAsync(location.FullName, encoding, cancellationToken); } /// /// Sets the attributes for a file /// /// Location of the file /// New attributes for the file public static void SetAttributes(FileReference location, FileAttributes attributes) { File.SetAttributes(location.FullName, attributes); } /// /// Sets the time that the file was last written to /// /// Location of the file /// Last write time, in local time public static void SetLastWriteTime(FileReference location, DateTime lastWriteTime) { File.SetLastWriteTime(location.FullName, lastWriteTime); } /// /// Sets the time that the file was last written to /// /// Location of the file /// Last write time, in UTC time public static void SetLastWriteTimeUtc(FileReference location, DateTime lastWriteTimeUtc) { File.SetLastWriteTimeUtc(location.FullName, lastWriteTimeUtc); } /// /// Sets the time that the file was last accessed. /// /// Location of the file. /// Last access time, in local time. public static void SetLastAccessTime(FileReference location, DateTime lastWriteTime) { File.SetLastWriteTime(location.FullName, lastWriteTime); } /// /// Sets the time that the file was last accessed. /// /// Location of the file. /// Last access time, in UTC time. public static void SetLastAccessTimeUtc(FileReference location, DateTime lastWriteTimeUtc) { File.SetLastWriteTimeUtc(location.FullName, lastWriteTimeUtc); } /// /// Writes the contents of a file /// /// Location of the file /// Contents of the file public static void WriteAllBytes(FileReference location, byte[] contents) { File.WriteAllBytes(location.FullName, contents); } /// /// Writes the contents of a file /// /// Location of the file /// Contents of the file public static Task WriteAllBytesAsync(FileReference location, byte[] contents, CancellationToken cancellationToken = default) { return File.WriteAllBytesAsync(location.FullName, contents, cancellationToken); } /// /// Writes the data to the given file, if it's different from what's there already. /// Returns true if contents were written. /// /// Location of the file /// Contents of the file public static bool WriteAllBytesIfDifferent(FileReference location, byte[] contents) { if(FileReference.Exists(location)) { byte[] currentContents = FileReference.ReadAllBytes(location); if(contents.AsSpan().SequenceEqual(currentContents)) { return false; } } WriteAllBytes(location, contents); return true; } /// /// Writes the string to the given file, if it's different from what's there already. /// Returns true if contents were written. /// /// Location of the file /// Contents of the file public static bool WriteAllTextIfDifferent(FileReference location, string contents) { if(FileReference.Exists(location)) { string currentContents = FileReference.ReadAllText(location); if (String.Equals(contents, currentContents)) { return false; } } WriteAllText(location, contents); return true; } /// /// Writes the contents of a file /// /// Location of the file /// Contents of the file public static void WriteAllLines(FileReference location, IEnumerable contents) { File.WriteAllLines(location.FullName, contents); } /// /// Writes the contents of a file /// /// Location of the file /// Contents of the file /// The encoding to use when parsing the file public static void WriteAllLines(FileReference location, IEnumerable contents, Encoding encoding) { File.WriteAllLines(location.FullName, contents, encoding); } /// /// Writes the contents of a file /// /// Location of the file /// Contents of the file public static void WriteAllLines(FileReference location, string[] contents) { File.WriteAllLines(location.FullName, contents); } /// /// Writes the contents of a file /// /// Location of the file /// Contents of the file /// The encoding to use when parsing the file public static void WriteAllLines(FileReference location, string[] contents, Encoding encoding) { File.WriteAllLines(location.FullName, contents, encoding); } /// /// Writes the contents of a file /// /// Location of the file /// Contents of the file public static Task WriteAllLinesAsync(FileReference location, IEnumerable contents, CancellationToken cancellationToken = default) { return File.WriteAllLinesAsync(location.FullName, contents, cancellationToken); } /// /// Writes the contents of a file /// /// Location of the file /// Contents of the file /// The encoding to use when parsing the file public static Task WriteAllLinesAsync(FileReference location, IEnumerable contents, Encoding encoding, CancellationToken cancellationToken = default) { return File.WriteAllLinesAsync(location.FullName, contents, encoding, cancellationToken); } /// /// Writes the contents of a file /// /// Location of the file /// Contents of the file public static Task WriteAllLinesAsync(FileReference location, string[] contents, CancellationToken cancellationToken = default) { return File.WriteAllLinesAsync(location.FullName, contents, cancellationToken); } /// /// Writes the contents of a file /// /// Location of the file /// Contents of the file /// The encoding to use when parsing the file public static Task WriteAllLinesAsync(FileReference location, string[] contents, Encoding encoding, CancellationToken cancellationToken = default) { return File.WriteAllLinesAsync(location.FullName, contents, encoding, cancellationToken); } /// /// Writes the contents of a file /// /// Location of the file /// Contents of the file public static void WriteAllText(FileReference location, string contents) { File.WriteAllText(location.FullName, contents); } /// /// Writes the contents of a file /// /// Location of the file /// Contents of the file /// The encoding to use when parsing the file public static void WriteAllText(FileReference location, string contents, Encoding encoding) { File.WriteAllText(location.FullName, contents, encoding); } /// /// Writes the contents of a file /// /// Location of the file /// Contents of the file public static Task WriteAllTextAsync(FileReference location, string contents) { return File.WriteAllTextAsync(location.FullName, contents); } /// /// Writes the contents of a file /// /// Location of the file /// Contents of the file /// The encoding to use when parsing the file public static Task WriteAllTextAsync(FileReference location, string contents, Encoding encoding) { return File.WriteAllTextAsync(location.FullName, contents, encoding); } /// /// Appends the contents to a file /// /// Location of the file /// Contents to append to the file public static void AppendAllLines(FileReference location, IEnumerable contents) { File.AppendAllLines(location.FullName, contents); } /// /// Appends the contents to a file /// /// Location of the file /// Contents to append to the file public static Task AppendAllLinesAsync(FileReference location, IEnumerable contents) { return File.AppendAllLinesAsync(location.FullName, contents); } /// /// Appends the contents to a file /// /// Location of the file /// Contents to append to the file /// The encoding to use when parsing the file public static void AppendAllLines(FileReference location, IEnumerable contents, Encoding encoding) { File.AppendAllLines(location.FullName, contents, encoding); } /// /// Appends the contents to a file /// /// Location of the file /// Contents to append to the file /// The encoding to use when parsing the file public static Task AppendAllLinesAsync(FileReference location, IEnumerable contents, Encoding encoding) { return File.AppendAllLinesAsync(location.FullName, contents, encoding); } /// /// Appends the contents to a file /// /// Location of the file /// Contents to append to the file public static void AppendAllLines(FileReference location, string[] contents) { File.AppendAllLines(location.FullName, contents); } /// /// Appends the contents to a file /// /// Location of the file /// Contents to append to the file public static Task AppendAllLinesAsync(FileReference location, string[] contents) { return File.AppendAllLinesAsync(location.FullName, contents); } /// /// Appends the contents to a file /// /// Location of the file /// Contents to append to the file /// The encoding to use when parsing the file public static void AppendAllLines(FileReference location, string[] contents, Encoding encoding) { File.AppendAllLines(location.FullName, contents, encoding); } /// /// Appends the contents to a file /// /// Location of the file /// Contents to append to the file /// The encoding to use when parsing the file public static Task AppendAllLinesAsync(FileReference location, string[] contents, Encoding encoding) { return File.AppendAllLinesAsync(location.FullName, contents, encoding); } /// /// Appends the contents to a file /// /// Location of the file /// Contents to append to the file public static void AppendAllText(FileReference location, string contents) { File.AppendAllText(location.FullName, contents); } /// /// Appends the contents to a file /// /// Location of the file /// Contents to append to the file public static Task AppendAllTextAsync(FileReference location, string contents) { return File.AppendAllTextAsync(location.FullName, contents); } /// /// Appends the contents to a file /// /// Location of the file /// Contents to append to the file /// The encoding to use when parsing the file public static void AppendAllText(FileReference location, string contents, Encoding encoding) { File.AppendAllText(location.FullName, contents, encoding); } /// /// Appends the contents to a file /// /// Location of the file /// Contents to append to the file /// The encoding to use when parsing the file public static Task AppendAllTextAsync(FileReference location, string contents, Encoding encoding) { return File.AppendAllTextAsync(location.FullName, contents, encoding); } #endregion } /// /// Extension methods for FileReference functionality /// public static class FileReferenceExtensionMethods { /// /// Manually serialize a file reference to a binary stream. /// /// Binary writer to write to /// The file reference to write public static void Write(this BinaryWriter writer, FileReference file) { writer.Write((file == null) ? String.Empty : file.FullName); } /// /// Serializes a file reference, using a lookup table to avoid serializing the same name more than once. /// /// The writer to save this reference to /// A file reference to output; may be null /// A lookup table that caches previous files that have been output, and maps them to unique id's. public static void Write(this BinaryWriter writer, FileReference file, Dictionary fileToUniqueId) { int uniqueId; if (file == null) { writer.Write(-1); } else if (fileToUniqueId.TryGetValue(file, out uniqueId)) { writer.Write(uniqueId); } else { writer.Write(fileToUniqueId.Count); writer.Write(file); fileToUniqueId.Add(file, fileToUniqueId.Count); } } /// /// Manually deserialize a file reference from a binary stream. /// /// Binary reader to read from /// New FileReference object public static FileReference ReadFileReference(this BinaryReader reader) { return BinaryArchiveReader.NotNull(ReadFileReferenceOrNull(reader)); } /// /// Manually deserialize a file reference from a binary stream. /// /// Binary reader to read from /// New FileReference object public static FileReference? ReadFileReferenceOrNull(this BinaryReader reader) { string fullName = reader.ReadString(); return (fullName.Length == 0) ? null : new FileReference(fullName, FileReference.Sanitize.None); } /// /// Deserializes a file reference, using a lookup table to avoid writing the same name more than once. /// /// The source to read from /// List of previously read file references. The index into this array is used in place of subsequent ocurrences of the file. /// The file reference that was read public static FileReference ReadFileReference(this BinaryReader reader, List uniqueFiles) { return BinaryArchiveReader.NotNull(ReadFileReferenceOrNull(reader, uniqueFiles)); } /// /// Deserializes a file reference, using a lookup table to avoid writing the same name more than once. /// /// The source to read from /// List of previously read file references. The index into this array is used in place of subsequent ocurrences of the file. /// The file reference that was read public static FileReference? ReadFileReferenceOrNull(this BinaryReader reader, List uniqueFiles) { int uniqueId = reader.ReadInt32(); if (uniqueId == -1) { return null; } else if (uniqueId < uniqueFiles.Count) { return uniqueFiles[uniqueId]; } else { FileReference result = reader.ReadFileReference(); uniqueFiles.Add(result); return result; } } /// /// Writes a FileReference to a binary archive /// /// The writer to output data to /// The file reference to write public static void WriteFileReference(this BinaryArchiveWriter writer, FileReference? file) { if(file == null) { writer.WriteString(null); } else { writer.WriteString(file.FullName); } } /// /// Reads a FileReference from a binary archive /// /// Reader to serialize data from /// New file reference instance public static FileReference ReadFileReference(this BinaryArchiveReader reader) { return BinaryArchiveReader.NotNull(ReadFileReferenceOrNull(reader)); } /// /// Reads a FileReference from a binary archive /// /// Reader to serialize data from /// New file reference instance public static FileReference? ReadFileReferenceOrNull(this BinaryArchiveReader reader) { string? fullName = reader.ReadString(); if(fullName == null) { return null; } else { return new FileReference(fullName, FileReference.Sanitize.None); } } } }