mirror of
https://github.com/actions/runner.git
synced 2026-07-03 11:06:08 +08:00
Normalize Windows path casing using GetFinalPathNameByHandle
On Windows, the runner inherits whatever path casing is used to start it (e.g. c:\actions-runner vs C:\actions-runner). NTFS is case-insensitive but tools like git's includeIf.gitdir do exact string matching, causing auth failures when the casing doesn't match the canonical NTFS path. This adds PathUtil.GetCanonicalPath which uses the Win32 GetFinalPathNameByHandle API to resolve paths to their NTFS canonical casing. It is called when resolving the runner root directory, so all derived paths (workspace, temp, etc.) use the correct casing. Fixes actions/checkout#2345
This commit is contained in:
committed by
Salman Muin Kayser Chishti
parent
0cdaa36d07
commit
7585eb30aa
@@ -392,6 +392,7 @@ namespace GitHub.Runner.Common
|
||||
|
||||
case WellKnownDirectory.Root:
|
||||
path = new DirectoryInfo(GetDirectory(WellKnownDirectory.Bin)).Parent.FullName;
|
||||
path = PathUtil.GetCanonicalPath(path);
|
||||
break;
|
||||
|
||||
case WellKnownDirectory.Temp:
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace GitHub.Runner.Sdk
|
||||
{
|
||||
@@ -6,8 +9,82 @@ namespace GitHub.Runner.Sdk
|
||||
{
|
||||
#if OS_WINDOWS
|
||||
public static readonly string PathVariable = "Path";
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern SafeFileHandle CreateFile(
|
||||
string lpFileName,
|
||||
uint dwDesiredAccess,
|
||||
uint dwShareMode,
|
||||
System.IntPtr lpSecurityAttributes,
|
||||
uint dwCreationDisposition,
|
||||
uint dwFlagsAndAttributes,
|
||||
System.IntPtr hTemplateFile);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern uint GetFinalPathNameByHandle(
|
||||
SafeFileHandle hFile,
|
||||
[Out] StringBuilder lpszFilePath,
|
||||
uint cchFilePath,
|
||||
uint dwFlags);
|
||||
|
||||
private const uint FILE_READ_ATTRIBUTES = 0x80;
|
||||
private const uint FILE_SHARE_READ = 0x1;
|
||||
private const uint FILE_SHARE_WRITE = 0x2;
|
||||
private const uint FILE_SHARE_DELETE = 0x4;
|
||||
private const uint OPEN_EXISTING = 3;
|
||||
private const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
|
||||
private const uint VOLUME_NAME_DOS = 0x0;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the NTFS canonical path for a directory, resolving drive letter
|
||||
/// and folder name casing to match what is stored on disk.
|
||||
/// On non-Windows platforms, returns the path unchanged.
|
||||
/// </summary>
|
||||
public static string GetCanonicalPath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path) || !Directory.Exists(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
using var handle = CreateFile(
|
||||
path,
|
||||
FILE_READ_ATTRIBUTES,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||
System.IntPtr.Zero,
|
||||
OPEN_EXISTING,
|
||||
FILE_FLAG_BACKUP_SEMANTICS,
|
||||
System.IntPtr.Zero);
|
||||
|
||||
if (handle.IsInvalid)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
var buffer = new StringBuilder(1024);
|
||||
var result = GetFinalPathNameByHandle(handle, buffer, (uint)buffer.Capacity, VOLUME_NAME_DOS);
|
||||
if (result == 0 || result > buffer.Capacity)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
var canonicalPath = buffer.ToString();
|
||||
|
||||
// Strip the \\?\ prefix that GetFinalPathNameByHandle adds
|
||||
if (canonicalPath.StartsWith(@"\\?\"))
|
||||
{
|
||||
canonicalPath = canonicalPath.Substring(4);
|
||||
}
|
||||
|
||||
return canonicalPath;
|
||||
}
|
||||
#else
|
||||
public static readonly string PathVariable = "PATH";
|
||||
|
||||
public static string GetCanonicalPath(string path)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
#endif
|
||||
|
||||
public static string PrependPath(string path, string currentPath)
|
||||
|
||||
79
src/Test/L0/Util/PathUtilL0.cs
Normal file
79
src/Test/L0/Util/PathUtilL0.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using GitHub.Runner.Sdk;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Xunit;
|
||||
|
||||
namespace GitHub.Runner.Common.Tests.Util
|
||||
{
|
||||
public sealed class PathUtilL0
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Common")]
|
||||
public void GetCanonicalPath_ReturnsPath_WhenDirectoryDoesNotExist()
|
||||
{
|
||||
var fakePath = Path.Combine(Path.GetTempPath(), "nonexistent_" + Path.GetRandomFileName());
|
||||
var result = PathUtil.GetCanonicalPath(fakePath);
|
||||
Assert.Equal(fakePath, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Common")]
|
||||
public void GetCanonicalPath_ReturnsPath_WhenNull()
|
||||
{
|
||||
Assert.Null(PathUtil.GetCanonicalPath(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Common")]
|
||||
public void GetCanonicalPath_ReturnsEmpty_WhenEmpty()
|
||||
{
|
||||
Assert.Equal(string.Empty, PathUtil.GetCanonicalPath(string.Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Common")]
|
||||
public void GetCanonicalPath_ReturnsValidPath_ForExistingDirectory()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "pathutil_test_" + Path.GetRandomFileName());
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var result = PathUtil.GetCanonicalPath(tempDir);
|
||||
Assert.NotNull(result);
|
||||
Assert.True(Directory.Exists(result));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(tempDir))
|
||||
{
|
||||
Directory.Delete(tempDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if OS_WINDOWS
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Common")]
|
||||
public void GetCanonicalPath_NormalizesDriveLetter_OnWindows()
|
||||
{
|
||||
// The temp directory should always have an uppercase drive letter
|
||||
// when resolved through GetFinalPathNameByHandle
|
||||
var tempDir = Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar);
|
||||
|
||||
// Force lowercase drive letter
|
||||
var lowerCased = char.ToLower(tempDir[0]) + tempDir.Substring(1);
|
||||
|
||||
var result = PathUtil.GetCanonicalPath(lowerCased);
|
||||
|
||||
// The canonical path should have an uppercase drive letter
|
||||
Assert.True(char.IsUpper(result[0]),
|
||||
$"Expected uppercase drive letter but got: {result}");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user