// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
namespace EpicGames.Core
{
///
/// Base class for file system objects (files or directories).
///
[Serializable]
public abstract class FileSystemReference
{
///
/// The path to this object. Stored as an absolute path, with O/S preferred separator characters, and no trailing slash for directories.
///
public string FullName { get; }
///
/// The comparer to use for file system references
///
public static StringComparer Comparer { get; } = StringComparer.OrdinalIgnoreCase;
///
/// The comparison to use for file system references
///
public static StringComparison Comparison { get; } = StringComparison.OrdinalIgnoreCase;
///
/// Direct constructor for a path
///
protected FileSystemReference(string fullName) => FullName = fullName;
static readonly ThreadLocal s_combineStringsStringBuilder = new ThreadLocal(() => new StringBuilder(260));
///
/// Create a full path by concatenating multiple strings
///
protected static string CombineStrings(DirectoryReference baseDirectory, params string[] fragments)
{
// Get the initial string to append to, and strip any root directory suffix from it
StringBuilder newFullName = s_combineStringsStringBuilder.Value!.Clear().Append(baseDirectory.FullName);
if (newFullName.Length > 0 && newFullName[^1] == Path.DirectorySeparatorChar)
{
newFullName.Remove(newFullName.Length - 1, 1);
}
// Scan through the fragments to append, appending them to a string and updating the base length as we go
foreach (string fragment in fragments)
{
// Check if this fragment is an absolute path
if ((fragment.Length >= 2 && fragment[1] == ':') || (fragment.Length >= 1 && (fragment[0] == '\\' || fragment[0] == '/')))
{
// It is. Reset the new name to the full version of this path.
newFullName.Clear();
newFullName.Append(Path.GetFullPath(fragment).TrimEnd(Path.DirectorySeparatorChar));
}
else
{
// Append all the parts of this fragment to the end of the existing path.
int startIdx = 0;
while (startIdx < fragment.Length)
{
// Find the end of this fragment. We may have been passed multiple paths in the same string.
int endIdx = startIdx;
while (endIdx < fragment.Length && fragment[endIdx] != '\\' && fragment[endIdx] != '/')
{
endIdx++;
}
// Ignore any empty sections, like leading or trailing slashes, and '.' directory references.
int length = endIdx - startIdx;
if (length == 0)
{
// Multiple directory separators in a row; illegal.
throw new ArgumentException(String.Format("Path fragment '{0}' contains invalid directory separators.", fragment));
}
else if (length == 2 && fragment[startIdx] == '.' && fragment[startIdx + 1] == '.')
{
// Remove the last directory name
for (int separatorIdx = newFullName.Length - 1; separatorIdx >= 0; separatorIdx--)
{
if (newFullName[separatorIdx] == Path.DirectorySeparatorChar)
{
newFullName.Remove(separatorIdx, newFullName.Length - separatorIdx);
break;
}
}
}
else if (length != 1 || fragment[startIdx] != '.')
{
// Append this fragment
newFullName.Append(Path.DirectorySeparatorChar);
newFullName.Append(fragment, startIdx, length);
}
// Move to the next part
startIdx = endIdx + 1;
}
}
}
// Append the directory separator
if (newFullName.Length == 0 || (newFullName.Length == 2 && newFullName[1] == ':'))
{
newFullName.Append(Path.DirectorySeparatorChar);
}
// Set the new path variables
return newFullName.ToString();
}
///
/// Checks whether this name has the given extension.
///
/// The extension to check
/// True if this name has the given extension, false otherwise
public bool HasExtension(string extension) => extension.Length > 0 && extension[0] != '.'
? FullName.Length >= extension.Length + 1 && FullName[FullName.Length - extension.Length - 1] == '.' && FullName.EndsWith(extension, Comparison)
: FullName.EndsWith(extension, Comparison);
///
/// Determines if the given object is at or under the given directory
///
/// Directory to check against
/// True if this path is under the given directory
public bool IsUnderDirectory(DirectoryReference other) => FullName.StartsWith(other.FullName, Comparison) && (FullName.Length == other.FullName.Length || FullName[other.FullName.Length] == Path.DirectorySeparatorChar || other.IsRootDirectory());
///
/// Checks to see if this exists as either a file or directory
/// This is helpful for Mac, because a binary may be a .app which is a directory
///
/// FileSsytem object to check
/// True if a file or a directory exists
public static bool Exists(FileSystemReference location) => File.Exists(location.FullName) || Directory.Exists(location.FullName);
///
/// Searches the path fragments for the given name. Only complete fragments are considered a match.
///
/// Name to check for
/// Offset within the string to start the search
/// True if the given name is found within the path
public bool ContainsName(string name, int offset) => ContainsName(name, offset, FullName.Length - offset);
///
/// Searches the path fragments for the given name. Only complete fragments are considered a match.
///
/// Name to check for
/// Offset within the string to start the search
/// Length of the substring to search
/// True if the given name is found within the path
public bool ContainsName(string name, int offset, int length)
{
// Check the substring to search is at least long enough to contain a match
if (length < name.Length)
{
return false;
}
// Find each occurrence of the name within the remaining string, then test whether it's surrounded by directory separators
int matchIdx = offset;
for (; ; )
{
// Find the next occurrence
matchIdx = FullName.IndexOf(name, matchIdx, offset + length - matchIdx, Comparison);
if (matchIdx == -1)
{
return false;
}
// Check if the substring is a directory
int matchEndIdx = matchIdx + name.Length;
if (FullName[matchIdx - 1] == Path.DirectorySeparatorChar && (matchEndIdx == FullName.Length || FullName[matchEndIdx] == Path.DirectorySeparatorChar))
{
return true;
}
// Move past the string that didn't match
matchIdx += name.Length;
}
}
///
/// Determines if the given object is under the given directory, within a subfolder of the given name. Useful for masking out directories by name.
///
/// Name of a subfolder to also check for
/// Base directory to check against
/// True if the path is under the given directory
public bool ContainsName(string name, DirectoryReference baseDir) => IsUnderDirectory(baseDir) && ContainsName(name, baseDir.FullName.Length);
///
/// Determines if the given object is under the given directory, within a subfolder of the given name. Useful for masking out directories by name.
///
/// Names of subfolders to also check for
/// Base directory to check against
/// True if the path is under the given directory
public bool ContainsAnyNames(IEnumerable names, DirectoryReference baseDir) => IsUnderDirectory(baseDir) && names.Any(x => ContainsName(x, baseDir.FullName.Length));
static readonly ThreadLocal s_makeRelativeToStringBuilder = new ThreadLocal(() => new StringBuilder(260));
///
/// Creates a relative path from the given base directory
///
/// The directory to create a relative path from
/// A relative path from the given directory
public string MakeRelativeTo(DirectoryReference directory)
{
StringBuilder result = s_makeRelativeToStringBuilder.Value!.Clear();
WriteRelativeTo(directory, result);
return result.ToString();
}
///
/// Appens a relative path to a string builder
///
///
///
public void WriteRelativeTo(DirectoryReference directory, StringBuilder result)
{
// Find how much of the path is common between the two paths. This length does not include a trailing directory separator character.
int commonDirectoryLength = -1;
for (int idx = 0; ; idx++)
{
if (idx == FullName.Length)
{
// The two paths are identical. Just return the "." character.
if (idx == directory.FullName.Length)
{
result.Append('.');
return;
}
// Check if we're finishing on a complete directory name
if (directory.FullName[idx] == Path.DirectorySeparatorChar)
{
commonDirectoryLength = idx;
}
break;
}
else if (idx == directory.FullName.Length)
{
// Check whether the end of the directory name coincides with a boundary for the current name.
if (FullName[idx] == Path.DirectorySeparatorChar)
{
commonDirectoryLength = idx;
}
break;
}
else
{
// Check the two paths match, and bail if they don't. Increase the common directory length if we've reached a separator.
if (String.Compare(FullName, idx, directory.FullName, idx, 1, Comparison) != 0)
{
break;
}
if (FullName[idx] == Path.DirectorySeparatorChar)
{
commonDirectoryLength = idx;
}
}
}
// If there's no relative path, just return the absolute path
if (commonDirectoryLength == -1)
{
result.Append(FullName);
return;
}
// Append all the '..' separators to get back to the common directory, then the rest of the string to reach the target item
for (int idx = commonDirectoryLength + 1; idx < directory.FullName.Length; idx++)
{
// Move up a directory
if (result.Length != 0)
{
result.Append(Path.DirectorySeparatorChar);
}
result.Append("..");
// Scan to the next directory separator
while (idx < directory.FullName.Length && directory.FullName[idx] != Path.DirectorySeparatorChar)
{
idx++;
}
}
if (commonDirectoryLength + 1 < FullName.Length)
{
if (result.Length != 0)
{
result.Append(Path.DirectorySeparatorChar);
}
result.Append(FullName, commonDirectoryLength + 1, FullName.Length - commonDirectoryLength - 1);
}
}
///
/// Normalize the path to using forward slashes
///
///
public string ToNormalizedPath() => FullName.Replace("\\", "/", StringComparison.Ordinal);
///
/// Returns a string representation of this filesystem object
///
/// Full path to the object
public override string ToString() => FullName;
}
}