// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; 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] [TypeConverter(typeof(FileReferenceTypeConverter))] public class FileReference : FileSystemReference, IEquatable, IComparable { /// /// Dummy enum to allow invoking the constructor which takes a sanitized full path /// public enum Sanitize { /// /// Dummy value /// 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 /// Sanitize argument. Ignored. public FileReference(string fullName, Sanitize sanitize) : base(fullName) { _ = sanitize; } /// /// 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(nameof(fileName))] public static FileReference? FromString(string? fileName) => String.IsNullOrEmpty(fileName) ? null : new FileReference(fileName); /// /// Gets the file name without path information /// /// A string containing the file name public string GetFileName() => 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() => 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() => 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) => new FileReference(Path.ChangeExtension(FullName, extension), 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) => new FileReference(CombineStrings(baseDirectory, fragments), 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) => 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) => !(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) => obj == this; /// /// Returns a hash code for this object /// /// public override int GetHashCode() => Comparer.GetHashCode(FullName); /// public int CompareTo(FileReference? other) => Comparer.Compare(FullName, other?.FullName); /// public static bool operator <(FileReference left, FileReference right) => left is null ? right is not null : left.CompareTo(right) < 0; /// public static bool operator <=(FileReference left, FileReference right) => left is null || left.CompareTo(right) <= 0; /// public static bool operator >(FileReference left, FileReference right) => left is not null && left.CompareTo(right) > 0; /// public static bool operator >=(FileReference left, FileReference right) => left is null ? right is null : left.CompareTo(right) >= 0; /// /// 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) => 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); } } } /// /// Get the encoding of a file by checking for BOM bytes /// /// Location of the file /// Number of bytes to skip /// The encoding public static Encoding GetEncoding(FileReference location, out int skipBytes) { try { Span span = stackalloc byte[4]; using (FileStream stream = Open(location, FileMode.Open, FileAccess.Read, FileShare.Read)) { stream.Read(span); } return FileUtils.GetEncoding(span, out skipBytes); } catch (IOException) { } skipBytes = 0; return Encoding.UTF8; } /// /// Get the encoding of a file by checking for BOM bytes /// /// Location of the file /// The encoding public static Encoding GetEncoding(FileReference location) => GetEncoding(location, out int _); /// /// Get the encoding of a file by checking for BOM bytes /// /// Location of the file /// Cancellation token for the operation /// The encoding public static async Task GetEncodingAsync(FileReference location, CancellationToken cancellationToken = default) { try { byte[] buffer = new byte[4]; using (FileStream stream = Open(location, FileMode.Open, FileAccess.Read, FileShare.Read)) { await stream.ReadAsync(buffer, 0, 4, cancellationToken); } return FileUtils.GetEncoding(buffer); } catch (IOException) { } return Encoding.UTF8; } /// /// 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) => new FileReference(FileUtils.FindCorrectCase(location.ToFileInfo())); /// /// Constructs a FileInfo object from this reference /// /// New FileInfo object public FileInfo ToFileInfo() => 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) => File.Exists(location.FullName); /// /// Gets the attributes for a file /// /// Location of the file /// Attributes for the file public static FileAttributes GetAttributes(FileReference location) => 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) => 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) => 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) => 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) => 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) => 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) => File.ReadAllBytes(location.FullName); /// /// Reads the contents of a file /// /// Location of the file /// Cancellation token for the operation /// Byte array containing the contents of the file public static Task ReadAllBytesAsync(FileReference location, CancellationToken cancellationToken = default) => 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) => File.ReadAllText(location.FullName, encoding); /// /// Reads the contents of a file /// /// Location of the file /// Cancellation token for the operation /// Contents of the file as a single string public static Task ReadAllTextAsync(FileReference location, CancellationToken cancellationToken = default) => File.ReadAllTextAsync(location.FullName, cancellationToken); /// /// Reads the contents of a file /// /// Location of the file /// Encoding of the file /// Cancellation token for the operation /// Contents of the file as a single string public static Task ReadAllTextAsync(FileReference location, Encoding encoding, CancellationToken cancellationToken = default) => 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) => 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) => File.ReadAllLines(location.FullName, encoding); /// /// Reads the contents of a file /// /// Location of the file /// Cancellation token for the operation /// String array containing the contents of the file public static Task ReadAllLinesAsync(FileReference location, CancellationToken cancellationToken = default) => File.ReadAllLinesAsync(location.FullName, cancellationToken); /// /// Reads the contents of a file /// /// Location of the file /// The encoding to use when parsing the file /// Cancellation token for the operation /// String array containing the contents of the file public static Task ReadAllLinesAsync(FileReference location, Encoding encoding, CancellationToken cancellationToken = default) => 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 /// Cancellation token for the operation public static Task WriteAllBytesAsync(FileReference location, byte[] contents, CancellationToken cancellationToken = default) => 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 (Exists(location)) { byte[] currentContents = ReadAllBytes(location); if (contents.AsSpan().SequenceEqual(currentContents)) { return false; } } WriteAllBytes(location, contents); return true; } /// /// 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 /// Cancellation token for the operation public static async Task WriteAllBytesIfDifferentAsync(FileReference location, byte[] contents, CancellationToken cancellationToken = default) { if (Exists(location)) { byte[] currentContents = await ReadAllBytesAsync(location, cancellationToken); if (contents.AsSpan().SequenceEqual(currentContents)) { return false; } } await WriteAllBytesAsync(location, contents, cancellationToken); 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 (Exists(location)) { string currentContents = ReadAllText(location); if (String.Equals(contents, currentContents, StringComparison.Ordinal)) { return false; } } WriteAllText(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 /// Cancellation token for the operation public static async Task WriteAllTextIfDifferentAsync(FileReference location, string contents, CancellationToken cancellationToken = default) { if (Exists(location)) { string currentContents = await ReadAllTextAsync(location, cancellationToken); if (String.Equals(contents, currentContents, StringComparison.Ordinal)) { return false; } } await WriteAllTextAsync(location, contents, cancellationToken); 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 /// Cancellation token for the operation public static Task WriteAllLinesAsync(FileReference location, IEnumerable contents, CancellationToken cancellationToken = default) => 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 /// Cancellation token for the operation public static Task WriteAllLinesAsync(FileReference location, IEnumerable contents, Encoding encoding, CancellationToken cancellationToken = default) => File.WriteAllLinesAsync(location.FullName, contents, encoding, cancellationToken); /// /// Writes the contents of a file /// /// Location of the file /// Contents of the file /// Cancellation token for the operation public static Task WriteAllLinesAsync(FileReference location, string[] contents, CancellationToken cancellationToken = default) => 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 /// Cancellation token for the operation public static Task WriteAllLinesAsync(FileReference location, string[] contents, Encoding encoding, CancellationToken cancellationToken = default) => 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 /// Cancellation token for the operation public static Task WriteAllTextAsync(FileReference location, string contents, CancellationToken cancellationToken = default) => File.WriteAllTextAsync(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 /// Cancellation token for the operation public static Task WriteAllTextAsync(FileReference location, string contents, Encoding encoding, CancellationToken cancellationToken = default) => File.WriteAllTextAsync(location.FullName, contents, encoding, cancellationToken); /// /// 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 /// Cancellation token for the operation public static Task AppendAllLinesAsync(FileReference location, IEnumerable contents, CancellationToken cancellationToken = default) => File.AppendAllLinesAsync(location.FullName, contents, cancellationToken); /// /// 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 /// Cancellation token for the operation public static Task AppendAllLinesAsync(FileReference location, IEnumerable contents, Encoding encoding, CancellationToken cancellationToken = default) => File.AppendAllLinesAsync(location.FullName, contents, encoding, cancellationToken); /// /// 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 /// Cancellation token for the operation public static Task AppendAllLinesAsync(FileReference location, string[] contents, CancellationToken cancellationToken = default) => File.AppendAllLinesAsync(location.FullName, contents, cancellationToken); /// /// 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 /// Cancellation token for the operation public static Task AppendAllLinesAsync(FileReference location, string[] contents, Encoding encoding, CancellationToken cancellationToken = default) => File.AppendAllLinesAsync(location.FullName, contents, encoding, cancellationToken); /// /// 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 /// Cancellation token for the operation public static Task AppendAllTextAsync(FileReference location, string contents, CancellationToken cancellationToken = default) => File.AppendAllTextAsync(location.FullName, contents, cancellationToken); /// /// 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 /// Cancellation token for the operation public static Task AppendAllTextAsync(FileReference location, string contents, Encoding encoding, CancellationToken cancellationToken = default) => File.AppendAllTextAsync(location.FullName, contents, encoding, cancellationToken); #endregion } /// /// Type converter to/from strings /// class FileReferenceTypeConverter : TypeConverter { /// public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); } /// public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { if (value is string stringValue) { return new FileReference(stringValue); } return base.ConvertFrom(context, culture, value); } /// public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) { return destinationType == typeof(string) || base.CanConvertTo(context, destinationType); } /// public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) { if (destinationType == typeof(string)) { return value?.ToString(); } return base.ConvertTo(context, culture, value, destinationType); } } /// /// 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) => 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 occurrences of the file. /// The file reference that was read public static FileReference ReadFileReference(this BinaryReader reader, List uniqueFiles) => 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 occurrences 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) => 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(); return fullName == null ? null : new FileReference(fullName, FileReference.Sanitize.None); } } }