using System; using System.IO; using System.Reflection; using BizHawk.Common.StringExtensions; namespace BizHawk.Common.PathExtensions { public static class PathExtensions { /// iff indicates a child of , with being returned if either path is /// algorithm for Windows taken from https://stackoverflow.com/a/7710620/7467292 public static bool IsSubfolderOf(this string? childPath, string? parentPath) { if (childPath == null || parentPath == null) return false; if (childPath == parentPath || childPath.StartsWith($"{parentPath}{Path.DirectorySeparatorChar}")) return true; if (OSTailoredCode.IsUnixHost) { #if true return OSTailoredCode.SimpleSubshell("realpath", $"-L \"{childPath}\"", $"invalid path {childPath} or missing realpath binary") .StartsWith(OSTailoredCode.SimpleSubshell("realpath", $"-L \"{parentPath}\"", $"invalid path {parentPath} or missing realpath binary")); #else // written for Unix port but may be useful for Windows when moving to .NET Core var parentUriPath = new Uri(parentPath.TrimEnd('.')).AbsolutePath.TrimEnd('/'); try { for (var childUri = new DirectoryInfo(childPath).Parent; childUri != null; childUri = childUri.Parent) { if (new Uri(childUri.FullName).AbsolutePath.TrimEnd('/') == parentUriPath) return true; } } catch { // ignored } return false; #endif } var parentUri = new Uri(parentPath); for (var childUri = new DirectoryInfo(childPath).Parent; childUri != null; childUri = childUri.Parent) { if (new Uri(childUri.FullName) == parentUri) return true; } return false; } /// the absolute path equivalent to which contains %exe% (expanded) as a prefix /// /// returned string omits trailing slash
/// note that the returned string is an absolute path and not a relative path; but TODO it was intended to be relative ///
public static string MakeProgramRelativePath(this string path) => Path.Combine(PathUtils.ExeDirectoryPath, path); /// the relative path which is equivalent to when the CWD is , or if either path is /// returned string omits trailing slash; implementation calls for you public static string? MakeRelativeTo(this string? absolutePath, string? basePath) { if (absolutePath == null || basePath == null) return null; if (!absolutePath.IsSubfolderOf(basePath)) return absolutePath; if (!OSTailoredCode.IsUnixHost) return absolutePath.Replace(basePath, ".").RemoveSuffix(Path.DirectorySeparatorChar); #if true // Unix implementation using realpath var realpathOutput = OSTailoredCode.SimpleSubshell("realpath", $"--relative-to=\"{basePath}\" \"{absolutePath}\"", $"invalid path {absolutePath}, invalid path {basePath}, or missing realpath binary"); return !realpathOutput.StartsWith("../") && realpathOutput != "." && realpathOutput != ".." ? $"./{realpathOutput}" : realpathOutput; #else // for some reason there were two Unix implementations in the codebase before me? --yoshi // alt. #1 if (!IsSubfolder(basePath, absolutePath)) return OSTailoredCode.IsUnixHost && basePath.TrimEnd('.') == $"{absolutePath}/" ? "." : absolutePath; return OSTailoredCode.IsUnixHost ? absolutePath.Replace(basePath.TrimEnd('.'), "./") : absolutePath.Replace(basePath, "."); // alt. #2; algorithm taken from https://stackoverflow.com/a/340454/7467292 var dirSepChar = Path.DirectorySeparatorChar; var fromUri = new Uri(absolutePath.EndsWith(dirSepChar.ToString()) ? absolutePath : absolutePath + dirSepChar); var toUri = new Uri(basePath.EndsWith(dirSepChar.ToString()) ? basePath : basePath + dirSepChar); if (fromUri.Scheme != toUri.Scheme) return basePath; var relativePath = Uri.UnescapeDataString(fromUri.MakeRelativeUri(toUri).ToString()); return (toUri.Scheme.Equals(Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase) ? relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar) : relativePath ).TrimEnd(dirSepChar); #endif } /// iff is blank, or is "." (relative path to CWD), regardless of trailing slash public static bool PathIsSet(this string path) => !string.IsNullOrWhiteSpace(path) && path != "." && path != "./" && path != ".\\"; public static string RemoveInvalidFileSystemChars(this string name) => string.Concat(name.Split(Path.GetInvalidFileNameChars())); } public static class PathUtils { /// absolute path of the dll dir (sibling of EmuHawk.exe) /// returned string omits trailing slash public static readonly string DllDirectoryPath; /// absolute path of the parent dir of DiscoHawk.exe/EmuHawk.exe, commonly referred to as %exe% though none of our code adds it to the environment /// returned string omits trailing slash public static readonly string ExeDirectoryPath; static PathUtils() { var dirPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location); ExeDirectoryPath = OSTailoredCode.IsUnixHost ? (string.IsNullOrEmpty(dirPath) || dirPath == "/" ? string.Empty : dirPath) : (string.IsNullOrEmpty(dirPath) ? throw new Exception("failed to get location of executable, very bad things must have happened") : dirPath.RemoveSuffix('\\')); DllDirectoryPath = Path.Combine(OSTailoredCode.IsUnixHost && ExeDirectoryPath == string.Empty ? "/" : ExeDirectoryPath, "dll"); // yes, this is a lot of extra code to make sure BizHawk can run in `/` on Unix, but I've made up for it by caching these for the program lifecycle --yoshi } } }