diff --git a/SabreTools.IO.Extensions.Test/IOExtensionsTests.cs b/SabreTools.IO.Extensions.Test/IOExtensionsTests.cs index 44197d5..d2c4314 100644 --- a/SabreTools.IO.Extensions.Test/IOExtensionsTests.cs +++ b/SabreTools.IO.Extensions.Test/IOExtensionsTests.cs @@ -355,6 +355,106 @@ namespace SabreTools.IO.Extensions.Test #endregion + #region ResolvePath + + [Theory] + [InlineData(null)] + [InlineData("")] + public void ResolvePath_NullOrEmpty_ReturnsNull(string? path) + { + string? actual = path.ResolvePath(); + Assert.Null(actual); + } + + // TODO: Add tests for home path without modifying the runner's home path + + [Fact] + public void ResolvePath_AbsolutePath_Exists_ReturnsAsIs() + { + string path = typeof(IOExtensionsTests).Assembly.Location; + + string? actual = path.ResolvePath(); + Assert.Equal(path, actual); + } + + [Fact] + public void ResolvePath_AbsolutePath_Missing_ReturnsNull() + { + string path = Path.Combine(Path.GetTempPath(), "fake-path.bin"); + + string? actual = path.ResolvePath(); + Assert.Null(actual); + } + + // TODO: Add ResolvePath_RelativePath_Exists_ReturnsAsIs + + [Fact] + public void ResolvePath_RelativePath_Missing_ReturnsNull() + { + string path = Path.Combine("INVALID", "fake-path.bin"); + + string? actual = path.ResolvePath(); + Assert.Null(actual); + } + + [Fact] + public void ResolvePath_BareName_RuntimeDirectory_ReturnsFullPath() + { + // Create a file in the test assembly's runtime directory + string filename = "fake-path.bin"; + string path = Path.Combine(AppContext.BaseDirectory, filename); + + File.WriteAllBytes(path, []); + try + { + string? actual = filename.ResolvePath(); + Assert.Equal(path, actual); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void ResolvePath_BareName_Path_ReturnsFullPath() + { + // Create a temporary directory for testing + string tempDir = Path.Combine(Path.GetTempPath(), "fake-path"); + Directory.CreateDirectory(tempDir); + + // Create a file in the temporary directory + string filename = "fake-path.bin"; + string path = Path.Combine(tempDir, filename); + File.WriteAllBytes(path, []); + + // Prepend the temp directory to PATH and check it exists + // TODO: See if there's a way around changing the actual environment variables + string? originalPath = Environment.GetEnvironmentVariable("PATH"); + try + { + Environment.SetEnvironmentVariable("PATH", tempDir + Path.PathSeparator + (originalPath ?? string.Empty)); + string? actual = filename.ResolvePath(); + Assert.Equal(path, actual); + } + finally + { + Environment.SetEnvironmentVariable("PATH", originalPath); + Directory.Delete(tempDir, recursive: true); + } + } + + [Fact] + public void ResolvePath_BareName_Missing_ReturnsNull() + { + string filename = "fake-path.bin"; + + string? actual = filename.ResolvePath(); + Assert.Null(actual); + } + + #endregion + #region SafeGetDirectories [Fact] diff --git a/SabreTools.IO.Extensions/IOExtensions.cs b/SabreTools.IO.Extensions/IOExtensions.cs index 5a0944c..f918346 100644 --- a/SabreTools.IO.Extensions/IOExtensions.cs +++ b/SabreTools.IO.Extensions/IOExtensions.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Text; using System.Text.RegularExpressions; @@ -417,6 +418,67 @@ namespace SabreTools.IO.Extensions return path ?? string.Empty; } + /// + /// Resolve a file path that may be relative or contained within a PATH directory. + /// + /// Raw value from the user's options + /// The absolute path of the located files, or null on failure + /// + /// A value containing a path separator is treated as an explicit location and + /// returned as-is when it exists. A bare name (no separator) is searched in the + /// runtime directory first, then in each PATH entry. + /// + public static string? ResolvePath(this string? path) + { + // Invalid paths always return null + if (string.IsNullOrEmpty(path)) + return null; + + // If the configured path starts with a home character + if (path!.StartsWith("~/") || path.StartsWith("~\\")) + { + string homeDirectory = PathTool.GetHomeDirectory(); + + path = path.Substring(2); + path = Path.Combine(homeDirectory, path); + + return File.Exists(path) ? path : null; + } + + // Explicit location (absolute or relative path) + if (path.Contains("/") || path.Contains("\\")) + return File.Exists(path) ? path : null; + + // Check the runtime directory if no directory path is provided + string runtimeDir = PathTool.GetRuntimeDirectory(); + if (!string.IsNullOrEmpty(runtimeDir)) + { + string candidate = Path.Combine(runtimeDir, path); + if (File.Exists(candidate)) + return candidate; + } + + // Attempt to get the PATH variable for searching + string? pathEnv = Environment.GetEnvironmentVariable("PATH"); + if (string.IsNullOrEmpty(pathEnv)) + return null; + + // Loop through all entries in PATH + var pathParts = pathEnv!.Split(Path.PathSeparator); + foreach (string dir in pathParts) + { + if (string.IsNullOrEmpty(dir)) + continue; + + string candidate = Path.Combine(dir, path); + if (File.Exists(candidate)) + return candidate; + } + + // All options failed + return null; + } + #endregion #region Safe Directory Enumeration