Skip to content

Commit ca19938

Browse files
committed
fs: resolve symlinks and fix bugs in *FileSystem impls
There is a systematic bug in all the IFileSystem implementations in that we were incorrectly calling `Path.GetFileName` rather than `Path.GetFullPath`, meaning we're only comparing the file _names_ of two paths for equality! Fix this. For Mac and Linux we also handle symlinks (or firmlinks) by now using the .NET APIs to resolve links. On .NET Framework we don't do anything.
1 parent 65cead2 commit ca19938

File tree

9 files changed

+497
-9
lines changed

9 files changed

+497
-9
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System.IO;
2+
using GitCredentialManager.Interop.Linux;
3+
using Xunit;
4+
using static GitCredentialManager.Tests.TestUtils;
5+
6+
namespace GitCredentialManager.Tests.Interop.Linux
7+
{
8+
public class LinuxFileSystemTests
9+
{
10+
[PlatformFact(Platforms.Linux)]
11+
public static void LinuxFileSystem_IsSamePath_SamePath_ReturnsTrue()
12+
{
13+
var fs = new LinuxFileSystem();
14+
15+
string baseDir = GetTempDirectory();
16+
string fileA = CreateFile(baseDir, "a.file");
17+
18+
Assert.True(fs.IsSamePath(fileA, fileA));
19+
}
20+
21+
[PlatformFact(Platforms.Linux)]
22+
public static void LinuxFileSystem_IsSamePath_DifferentFile_ReturnsFalse()
23+
{
24+
var fs = new LinuxFileSystem();
25+
26+
string baseDir = GetTempDirectory();
27+
string fileA = CreateFile(baseDir, "a.file");
28+
string fileB = CreateFile(baseDir, "b.file");
29+
30+
Assert.False(fs.IsSamePath(fileA, fileB));
31+
Assert.False(fs.IsSamePath(fileB, fileA));
32+
}
33+
34+
[PlatformFact(Platforms.Linux)]
35+
public static void LinuxFileSystem_IsSamePath_SameFileDifferentCase_ReturnsFalse()
36+
{
37+
var fs = new LinuxFileSystem();
38+
39+
string baseDir = GetTempDirectory();
40+
string fileA1 = CreateFile(baseDir, "a.file");
41+
string fileA2 = Path.Combine(baseDir, "A.file");
42+
43+
Assert.False(fs.IsSamePath(fileA1, fileA2));
44+
Assert.False(fs.IsSamePath(fileA2, fileA1));
45+
}
46+
47+
[PlatformFact(Platforms.Linux)]
48+
public static void LinuxFileSystem_IsSamePath_SameFileDifferentPathNormalization_ReturnsTrue()
49+
{
50+
var fs = new LinuxFileSystem();
51+
52+
string baseDir = GetTempDirectory();
53+
string subDir = CreateDirectory(baseDir, "subDir1", "subDir2");
54+
string fileA1 = CreateFile(baseDir, "a.file");
55+
string fileA2 = Path.Combine(subDir, "..", "..", "a.file");
56+
57+
Assert.True(fs.IsSamePath(fileA1, fileA2));
58+
Assert.True(fs.IsSamePath(fileA2, fileA1));
59+
}
60+
61+
[PlatformFact(Platforms.Linux)]
62+
public static void LinuxFileSystem_IsSamePath_SameFileViaSymlink_ReturnsTrue()
63+
{
64+
var fs = new LinuxFileSystem();
65+
66+
string baseDir = GetTempDirectory();
67+
string fileA1 = CreateFile(baseDir, "a.file");
68+
string fileA2 = CreateFileSymlink(baseDir, "a.link", fileA1);
69+
70+
Assert.True(fs.IsSamePath(fileA1, fileA2));
71+
Assert.True(fs.IsSamePath(fileA2, fileA1));
72+
}
73+
74+
[PlatformFact(Platforms.Linux)]
75+
public static void LinuxFileSystem_IsSamePath_SameFileRelativePath_ReturnsTrue()
76+
{
77+
var fs = new LinuxFileSystem();
78+
79+
string baseDir = GetTempDirectory();
80+
string fileA1 = CreateFile(baseDir, "a.file");
81+
string fileA2 = "./a.file";
82+
83+
using (ChangeDirectory(baseDir))
84+
{
85+
Assert.True(fs.IsSamePath(fileA1, fileA2));
86+
Assert.True(fs.IsSamePath(fileA2, fileA1));
87+
}
88+
}
89+
}
90+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System.IO;
2+
using GitCredentialManager.Interop.MacOS;
3+
using Xunit;
4+
using static GitCredentialManager.Tests.TestUtils;
5+
6+
namespace GitCredentialManager.Tests.Interop.MacOS
7+
{
8+
public class MacOSFileSystemTests
9+
{
10+
[PlatformFact(Platforms.MacOS)]
11+
public static void MacOSFileSystem_IsSamePath_SamePath_ReturnsTrue()
12+
{
13+
var fs = new MacOSFileSystem();
14+
15+
string baseDir = GetTempDirectory();
16+
string fileA = CreateFile(baseDir, "a.file");
17+
18+
Assert.True(fs.IsSamePath(fileA, fileA));
19+
}
20+
21+
[PlatformFact(Platforms.MacOS)]
22+
public static void MacOSFileSystem_IsSamePath_DifferentFile_ReturnsFalse()
23+
{
24+
var fs = new MacOSFileSystem();
25+
26+
string baseDir = GetTempDirectory();
27+
string fileA = CreateFile(baseDir, "a.file");
28+
string fileB = CreateFile(baseDir, "b.file");
29+
30+
Assert.False(fs.IsSamePath(fileA, fileB));
31+
Assert.False(fs.IsSamePath(fileB, fileA));
32+
}
33+
34+
[PlatformFact(Platforms.MacOS)]
35+
public static void MacOSFileSystem_IsSamePath_SameFileDifferentCase_ReturnsTrue()
36+
{
37+
var fs = new MacOSFileSystem();
38+
39+
string baseDir = GetTempDirectory();
40+
string fileA1 = CreateFile(baseDir, "a.file");
41+
string fileA2 = Path.Combine(baseDir, "A.file");
42+
43+
Assert.True(fs.IsSamePath(fileA1, fileA2));
44+
Assert.True(fs.IsSamePath(fileA2, fileA1));
45+
}
46+
47+
[PlatformFact(Platforms.MacOS)]
48+
public static void MacOSFileSystem_IsSamePath_SameFileDifferentPathNormalization_ReturnsTrue()
49+
{
50+
var fs = new MacOSFileSystem();
51+
52+
string baseDir = GetTempDirectory();
53+
string subDir = CreateDirectory(baseDir, "subDir1", "subDir2");
54+
string fileA1 = CreateFile(baseDir, "a.file");
55+
string fileA2 = Path.Combine(subDir, "..", "..", "a.file");
56+
57+
Assert.True(fs.IsSamePath(fileA1, fileA2));
58+
Assert.True(fs.IsSamePath(fileA2, fileA1));
59+
}
60+
61+
[PlatformFact(Platforms.MacOS)]
62+
public static void MacOSFileSystem_IsSamePath_SameFileViaSymlink_ReturnsTrue()
63+
{
64+
var fs = new MacOSFileSystem();
65+
66+
string baseDir = GetTempDirectory();
67+
string fileA1 = CreateFile(baseDir, "a.file");
68+
string fileA2 = CreateFileSymlink(baseDir, "a.link", fileA1);
69+
70+
Assert.True(fs.IsSamePath(fileA1, fileA2));
71+
Assert.True(fs.IsSamePath(fileA2, fileA1));
72+
}
73+
74+
[PlatformFact(Platforms.MacOS)]
75+
public static void MacOSFileSystem_IsSamePath_SameFileRelativePath_ReturnsTrue()
76+
{
77+
var fs = new MacOSFileSystem();
78+
79+
string baseDir = GetTempDirectory();
80+
string fileA1 = CreateFile(baseDir, "a.file");
81+
string fileA2 = "./a.file";
82+
83+
using (ChangeDirectory(baseDir))
84+
{
85+
Assert.True(fs.IsSamePath(fileA1, fileA2));
86+
Assert.True(fs.IsSamePath(fileA2, fileA1));
87+
}
88+
}
89+
}
90+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System.IO;
2+
using GitCredentialManager.Interop.Posix;
3+
using Xunit;
4+
using static GitCredentialManager.Tests.TestUtils;
5+
6+
namespace GitCredentialManager.Tests.Interop.Posix
7+
{
8+
public class PosixFileSystemTests
9+
{
10+
[PlatformFact(Platforms.Posix)]
11+
public void PosixFileSystem_ResolveSymlinks_FileLinks()
12+
{
13+
string baseDir = GetTempDirectory();
14+
string realPath = CreateFile(baseDir, "realFile.txt");
15+
string linkPath = CreateFileSymlink(baseDir, "linkFile.txt", realPath);
16+
17+
string actual = PosixFileSystem.ResolveSymbolicLinks(linkPath);
18+
19+
Assert.Equal(realPath, actual);
20+
}
21+
22+
[PlatformFact(Platforms.Posix)]
23+
public void PosixFileSystem_ResolveSymlinks_DirectoryLinks()
24+
{
25+
//
26+
// Create a real file inside of a directory that is a symlink
27+
// to another directory.
28+
//
29+
// /tmp/{uuid}/linkDir/ -> /tmp/{uuid}/realDir/
30+
//
31+
string baseDir = GetTempDirectory();
32+
string realDir = CreateDirectory(baseDir, "realDir");
33+
string linkDir = CreateDirectorySymlink(baseDir, "linkDir", realDir);
34+
string filePath = CreateFile(linkDir, "file.txt");
35+
36+
string actual = PosixFileSystem.ResolveSymbolicLinks(filePath);
37+
38+
string expected = Path.Combine(realDir, "file.txt");
39+
40+
Assert.Equal(expected, actual);
41+
}
42+
}
43+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
using System.IO;
2+
using GitCredentialManager.Interop.Windows;
3+
using Xunit;
4+
using static GitCredentialManager.Tests.TestUtils;
5+
6+
namespace GitCredentialManager.Tests.Interop.Windows
7+
{
8+
public class WindowsFileSystemTests
9+
{
10+
[PlatformFact(Platforms.Windows)]
11+
public static void WindowsFileSystem_IsSamePath_SamePath_ReturnsTrue()
12+
{
13+
var fs = new WindowsFileSystem();
14+
15+
string baseDir = GetTempDirectory();
16+
string fileA = CreateFile(baseDir, "a.file");
17+
18+
Assert.True(fs.IsSamePath(fileA, fileA));
19+
}
20+
21+
[PlatformFact(Platforms.Windows)]
22+
public static void WindowsFileSystem_IsSamePath_DifferentFile_ReturnsFalse()
23+
{
24+
var fs = new WindowsFileSystem();
25+
26+
string baseDir = GetTempDirectory();
27+
string fileA = CreateFile(baseDir, "a.file");
28+
string fileB = CreateFile(baseDir, "b.file");
29+
30+
Assert.False(fs.IsSamePath(fileA, fileB));
31+
Assert.False(fs.IsSamePath(fileB, fileA));
32+
}
33+
34+
[PlatformFact(Platforms.Windows)]
35+
public static void WindowsFileSystem_IsSamePath_SameFileDifferentCase_ReturnsTrue()
36+
{
37+
var fs = new WindowsFileSystem();
38+
39+
string baseDir = GetTempDirectory();
40+
string fileA1 = CreateFile(baseDir, "a.file");
41+
string fileA2 = Path.Combine(baseDir, "A.file");
42+
43+
Assert.True(fs.IsSamePath(fileA1, fileA2));
44+
Assert.True(fs.IsSamePath(fileA2, fileA1));
45+
}
46+
47+
[PlatformFact(Platforms.Windows)]
48+
public static void WindowsFileSystem_IsSamePath_SameFileDifferentPathNormalization_ReturnsTrue()
49+
{
50+
var fs = new WindowsFileSystem();
51+
52+
string baseDir = GetTempDirectory();
53+
string subDir = CreateDirectory(baseDir, "subDir1", "subDir2");
54+
string fileA1 = CreateFile(baseDir, "a.file");
55+
string fileA2 = Path.Combine(subDir, "..", "..", "a.file");
56+
57+
Assert.True(fs.IsSamePath(fileA1, fileA2));
58+
Assert.True(fs.IsSamePath(fileA2, fileA1));
59+
}
60+
61+
[PlatformFact(Platforms.Windows)]
62+
public static void WindowsFileSystem_IsSamePath_SameFileRelativePath_ReturnsTrue()
63+
{
64+
var fs = new WindowsFileSystem();
65+
66+
string baseDir = GetTempDirectory();
67+
string fileA1 = CreateFile(baseDir, "a.file");
68+
string fileA2 = @".\a.file";
69+
70+
using (ChangeDirectory(baseDir))
71+
{
72+
Assert.True(fs.IsSamePath(fileA1, fileA2));
73+
Assert.True(fs.IsSamePath(fileA2, fileA1));
74+
}
75+
}
76+
}
77+
}

src/shared/Core/Interop/Linux/LinuxFileSystem.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,18 @@ public class LinuxFileSystem : PosixFileSystem
88
{
99
public override bool IsSamePath(string a, string b)
1010
{
11-
a = Path.GetFileName(a);
12-
b = Path.GetFileName(b);
11+
if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b))
12+
{
13+
return false;
14+
}
15+
16+
// Normalize paths
17+
a = Path.GetFullPath(a);
18+
b = Path.GetFullPath(b);
19+
20+
// Resolve symbolic links
21+
a = ResolveSymbolicLinks(a);
22+
b = ResolveSymbolicLinks(b);
1323

1424
return StringComparer.Ordinal.Equals(a, b);
1525
}

src/shared/Core/Interop/MacOS/MacOSFileSystem.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,21 @@ public class MacOSFileSystem : PosixFileSystem
88
{
99
public override bool IsSamePath(string a, string b)
1010
{
11-
a = Path.GetFileName(a);
12-
b = Path.GetFileName(b);
11+
if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b))
12+
{
13+
return false;
14+
}
1315

14-
// TODO: resolve symlinks
15-
// TODO: check if APFS/HFS+ is in case-sensitive mode
16+
// Normalize paths
17+
a = Path.GetFullPath(a);
18+
b = Path.GetFullPath(b);
19+
20+
// Resolve symbolic links
21+
a = ResolveSymbolicLinks(a);
22+
b = ResolveSymbolicLinks(b);
23+
24+
// TODO: determine if file system is case-sensitive
25+
// By default HFS+/APFS is NOT case-sensitive...
1626
return StringComparer.OrdinalIgnoreCase.Equals(a, b);
1727
}
1828
}

0 commit comments

Comments
 (0)