// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Tools.DotNETCommon
{
///
/// 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 readonly string FullName;
///
/// The canonical full name for this object.
///
public readonly string CanonicalName;
///
/// Direct constructor for a path
///
protected FileSystemReference(string InFullName)
{
FullName = InFullName;
CanonicalName = InFullName.ToLowerInvariant();
}
///
/// Direct constructor for a path
///
protected FileSystemReference(string InFullName, string InCanonicalName)
{
FullName = InFullName;
CanonicalName = InCanonicalName;
}
///
/// Create a full path by concatenating multiple strings
///
///
static protected string CombineStrings(DirectoryReference BaseDirectory, params string[] Fragments)
{
// Get the initial string to append to, and strip any root directory suffix from it
StringBuilder NewFullName = new StringBuilder(BaseDirectory.FullName);
if (NewFullName.Length > 0 && NewFullName[NewFullName.Length - 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)
{
if (Extension.Length > 0 && Extension[0] != '.')
{
return HasExtension("." + Extension);
}
else
{
return CanonicalName.EndsWith(Extension, StringComparison.InvariantCultureIgnoreCase);
}
}
///
/// 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)
{
return CanonicalName.StartsWith(Other.CanonicalName) && (CanonicalName.Length == Other.CanonicalName.Length || CanonicalName[Other.CanonicalName.Length] == Path.DirectorySeparatorChar || Other.IsRootDirectory());
}
///
/// 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(FileSystemName Name, int Offset)
{
return 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(FileSystemName Name, int Offset, int Length)
{
// Check the substring to search is at least long enough to contain a match
if(Length < Name.CanonicalName.Length)
{
return false;
}
// Find each occurence 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 = CanonicalName.IndexOf(Name.CanonicalName, MatchIdx, Offset + Length - MatchIdx);
if(MatchIdx == -1)
{
return false;
}
// Check if the substring is a directory
int MatchEndIdx = MatchIdx + Name.CanonicalName.Length;
if(CanonicalName[MatchIdx - 1] == Path.DirectorySeparatorChar && (MatchEndIdx == CanonicalName.Length || CanonicalName[MatchEndIdx] == Path.DirectorySeparatorChar))
{
return true;
}
// Move past the string that didn't match
MatchIdx += Name.CanonicalName.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(FileSystemName Name, DirectoryReference BaseDir)
{
// Check that this is under the base directory
if(!IsUnderDirectory(BaseDir))
{
return false;
}
else
{
return 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(FileSystemName[] Names, DirectoryReference BaseDir)
{
// Check that this is under the base directory
if(!IsUnderDirectory(BaseDir))
{
return false;
}
else
{
return Names.Any(x => ContainsName(x, BaseDir.FullName.Length));
}
}
///
/// 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)
{
// 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 == CanonicalName.Length)
{
// The two paths are identical. Just return the "." character.
if (Idx == Directory.CanonicalName.Length)
{
return ".";
}
// Check if we're finishing on a complete directory name
if (Directory.CanonicalName[Idx] == Path.DirectorySeparatorChar)
{
CommonDirectoryLength = Idx;
}
break;
}
else if (Idx == Directory.CanonicalName.Length)
{
// Check whether the end of the directory name coincides with a boundary for the current name.
if (CanonicalName[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 (CanonicalName[Idx] != Directory.CanonicalName[Idx])
{
break;
}
if (CanonicalName[Idx] == Path.DirectorySeparatorChar)
{
CommonDirectoryLength = Idx;
}
}
}
// If there's no relative path, just return the absolute path
if (CommonDirectoryLength == -1)
{
return FullName;
}
// Append all the '..' separators to get back to the common directory, then the rest of the string to reach the target item
StringBuilder Result = new StringBuilder();
for (int Idx = CommonDirectoryLength + 1; Idx < Directory.CanonicalName.Length; Idx++)
{
// Move up a directory
Result.Append("..");
Result.Append(Path.DirectorySeparatorChar);
// Scan to the next directory separator
while (Idx < Directory.CanonicalName.Length && Directory.CanonicalName[Idx] != Path.DirectorySeparatorChar)
{
Idx++;
}
}
if (CommonDirectoryLength + 1 < FullName.Length)
{
Result.Append(FullName, CommonDirectoryLength + 1, FullName.Length - CommonDirectoryLength - 1);
}
return Result.ToString();
}
///
/// Returns a string representation of this filesystem object
///
/// Full path to the object
public override string ToString()
{
return FullName;
}
}
}