Skip to content

Commit a1ab463

Browse files
rmarinhoCopilot
andcommitted
Fix PathUtils.IsSymlink throwing on common lstat failures
IsSymlink previously threw an exception for any lstat failure, including ENOENT (file not found) and EACCES (permission denied). These are expected non-exceptional conditions — especially when walking directory trees to check for symlinks via IsSymlinkOrHasParentSymlink. Now returns false for ENOENT, EACCES, and ENOTDIR, and only throws for genuinely unexpected errno values. Also adds InternalsVisibleTo for the test project and new PathUtilsTests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 30400f1 commit a1ab463

3 files changed

Lines changed: 72 additions & 2 deletions

File tree

Xamarin.MacDev/PathUtils.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,15 @@ public static bool IsSymlink (string file)
5656
}
5757
Stat buf;
5858
var rv = lstat (file, out buf);
59-
if (rv != 0)
60-
throw new Exception (string.Format ("Could not lstat '{0}': {1}", file, Marshal.GetLastWin32Error ()));
59+
if (rv != 0) {
60+
var errno = Marshal.GetLastWin32Error ();
61+
// ENOENT (2) = file not found, EACCES (13) = permission denied,
62+
// ENOTDIR (20) = path component is not a directory.
63+
// These are expected non-exceptional conditions.
64+
if (errno == 2 || errno == 13 || errno == 20)
65+
return false;
66+
throw new Exception (string.Format ("Could not lstat '{0}': {1}", file, errno));
67+
}
6168
const int S_IFLNK = 40960;
6269
return (buf.st_mode & S_IFLNK) == S_IFLNK;
6370
}

Xamarin.MacDev/Xamarin.MacDev.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
<ItemGroup Condition=" '$(TargetFramework)' != 'netstandard2.0' ">
3939
<Compile Remove="NullableAttributes.cs" />
4040
</ItemGroup>
41+
<ItemGroup>
42+
<InternalsVisibleTo Include="tests" />
43+
</ItemGroup>
4144
<ItemGroup>
4245
<PackageReference Include="System.Text.Json" Version="8.0.5" Condition=" '$(TargetFramework)' == 'netstandard2.0' " />
4346
</ItemGroup>

tests/PathUtilsTests.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
#nullable enable
5+
6+
using System;
7+
using System.IO;
8+
using NUnit.Framework;
9+
using Xamarin.MacDev;
10+
11+
namespace tests {
12+
13+
[TestFixture]
14+
public class PathUtilsTests {
15+
16+
[Test]
17+
[Platform ("MacOsX")]
18+
public void IsSymlink_ReturnsFalse_ForNonExistentFile ()
19+
{
20+
var path = Path.Combine (Path.GetTempPath (), Path.GetRandomFileName ());
21+
// Should not throw; returns false for ENOENT
22+
Assert.That (PathUtils.IsSymlink (path), Is.False);
23+
}
24+
25+
[Test]
26+
[Platform ("MacOsX")]
27+
public void IsSymlink_ReturnsFalse_ForRegularFile ()
28+
{
29+
var path = Path.GetTempFileName ();
30+
try {
31+
Assert.That (PathUtils.IsSymlink (path), Is.False);
32+
} finally {
33+
File.Delete (path);
34+
}
35+
}
36+
37+
[Test]
38+
[Platform ("MacOsX")]
39+
public void IsSymlink_ReturnsTrue_ForSymlink ()
40+
{
41+
var target = Path.GetTempFileName ();
42+
var link = target + ".link";
43+
try {
44+
File.CreateSymbolicLink (link, target);
45+
Assert.That (PathUtils.IsSymlink (link), Is.True);
46+
} finally {
47+
File.Delete (link);
48+
File.Delete (target);
49+
}
50+
}
51+
52+
[Test]
53+
[Platform ("MacOsX")]
54+
public void IsSymlinkOrHasParentSymlink_ReturnsFalse_ForNonExistentPath ()
55+
{
56+
var path = Path.Combine (Path.GetTempPath (), Path.GetRandomFileName ());
57+
Assert.That (PathUtils.IsSymlinkOrHasParentSymlink (path), Is.False);
58+
}
59+
}
60+
}

0 commit comments

Comments
 (0)