diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f5c6319..1781e8ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,11 @@ ### Features * [@Chris-Wolfgang]: Add support for keyed dependency injection via `[FromKeyedServices]` attribute ([#560]) +### Fixes +* [@claude]: Validate dotnet path exists before returning from `TryFindDotNetExePath` ([#600]) + [#560]: https://github.com/natemcmaster/CommandLineUtils/pull/560 +[#600]: https://github.com/natemcmaster/CommandLineUtils/issues/600 ## [v5.0.1](https://github.com/natemcmaster/CommandLineUtils/compare/v5.0.0...v5.0.1) diff --git a/src/CommandLineUtils/Utilities/DotNetExe.cs b/src/CommandLineUtils/Utilities/DotNetExe.cs index e7424021..7f365ff8 100644 --- a/src/CommandLineUtils/Utilities/DotNetExe.cs +++ b/src/CommandLineUtils/Utilities/DotNetExe.cs @@ -65,7 +65,13 @@ public static string FullPathOrDefault() dotnetRoot = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "C:\\Program Files\\dotnet" : "/usr/local/share/dotnet"; } - return Path.Combine(dotnetRoot, fileName); + return FindDotNetInRoot(dotnetRoot, fileName); + } + + internal static string? FindDotNetInRoot(string dotnetRoot, string fileName) + { + var dotnetPath = Path.Combine(dotnetRoot, fileName); + return File.Exists(dotnetPath) ? dotnetPath : null; } } } diff --git a/src/CommandLineUtils/releasenotes.props b/src/CommandLineUtils/releasenotes.props index ae607adb..60678d41 100644 --- a/src/CommandLineUtils/releasenotes.props +++ b/src/CommandLineUtils/releasenotes.props @@ -5,6 +5,9 @@ Changes since 5.0: Features: * @Chris-Wolfgang: Add support for keyed dependency injection via [FromKeyedServices] attribute (#560) + +Fixes: +* @claude: Validate dotnet path exists before returning from TryFindDotNetExePath (#600) Changes since 4.1: diff --git a/test/CommandLineUtils.Tests/DotNetExeTests.cs b/test/CommandLineUtils.Tests/DotNetExeTests.cs index 14c7cdb0..cd00f76e 100644 --- a/test/CommandLineUtils.Tests/DotNetExeTests.cs +++ b/test/CommandLineUtils.Tests/DotNetExeTests.cs @@ -5,6 +5,7 @@ #if NET6_0_OR_GREATER using System.IO; +using System.Runtime.InteropServices; using Xunit; namespace McMaster.Extensions.CommandLineUtils.Tests @@ -20,6 +21,62 @@ public void FindsTheDotNetPath() Assert.True(Path.IsPathRooted(dotnetPath), "The path should be rooted"); Assert.Equal("dotnet", Path.GetFileNameWithoutExtension(dotnetPath), ignoreCase: true); } + + [Fact] + public void FullPathOrDefaultReturnsPathOrDotnet() + { + var result = DotNetExe.FullPathOrDefault(); + Assert.NotNull(result); + Assert.NotEmpty(result); + // Should either be a rooted path that exists, or just "dotnet" + if (Path.IsPathRooted(result)) + { + Assert.True(File.Exists(result), "The file did not exist"); + } + else + { + Assert.Equal("dotnet", result); + } + } + + [Fact] + public void FindDotNetInRoot_ReturnsNull_WhenDirectoryDoesNotExist() + { + var fileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"; + var result = DotNetExe.FindDotNetInRoot("/nonexistent/path/that/does/not/exist", fileName); + Assert.Null(result); + } + + [Fact] + public void FindDotNetInRoot_ReturnsNull_WhenFileDoesNotExist() + { + // Use a directory that exists but won't contain dotnet + var tempDir = Path.GetTempPath(); + var result = DotNetExe.FindDotNetInRoot(tempDir, "dotnet-nonexistent-file"); + Assert.Null(result); + } + + [Fact] + public void FindDotNetInRoot_ReturnsPath_WhenFileExists() + { + // Create a temp file to simulate dotnet existing + var tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempDir); + try + { + var fakeDotnet = "dotnet-test"; + var fakePath = Path.Combine(tempDir, fakeDotnet); + File.WriteAllText(fakePath, ""); + + var result = DotNetExe.FindDotNetInRoot(tempDir, fakeDotnet); + Assert.NotNull(result); + Assert.Equal(fakePath, result); + } + finally + { + Directory.Delete(tempDir, recursive: true); + } + } } } #elif NET472_OR_GREATER