diff --git a/Xamarin.MacDev/PathUtils.cs b/Xamarin.MacDev/PathUtils.cs
index 34bc51d..c366509 100644
--- a/Xamarin.MacDev/PathUtils.cs
+++ b/Xamarin.MacDev/PathUtils.cs
@@ -48,6 +48,15 @@ static int lstat (string path, out Stat buf)
}
}
+ const int ENOENT = 2;
+ const int EACCES = 13;
+ const int ENOTDIR = 20;
+
+ ///
+ /// Returns whether the given path is a symlink.
+ /// Returns false (rather than throwing) when the path cannot be examined
+ /// due to non-existence, permissions, or a non-directory path component.
+ ///
public static bool IsSymlink (string file)
{
if (Environment.OSVersion.Platform == PlatformID.Win32NT) {
@@ -56,8 +65,12 @@ public static bool IsSymlink (string file)
}
Stat buf;
var rv = lstat (file, out buf);
- if (rv != 0)
- throw new Exception (string.Format ("Could not lstat '{0}': {1}", file, Marshal.GetLastWin32Error ()));
+ if (rv != 0) {
+ var errno = Marshal.GetLastWin32Error ();
+ if (errno == ENOENT || errno == EACCES || errno == ENOTDIR)
+ return false;
+ throw new Exception (string.Format ("Could not lstat '{0}': {1}", file, errno));
+ }
const int S_IFLNK = 40960;
return (buf.st_mode & S_IFLNK) == S_IFLNK;
}
diff --git a/Xamarin.MacDev/Xamarin.MacDev.csproj b/Xamarin.MacDev/Xamarin.MacDev.csproj
index 30f6fb0..fa16155 100644
--- a/Xamarin.MacDev/Xamarin.MacDev.csproj
+++ b/Xamarin.MacDev/Xamarin.MacDev.csproj
@@ -38,6 +38,9 @@
+
+
+
diff --git a/tests/PathUtilsTests.cs b/tests/PathUtilsTests.cs
new file mode 100644
index 0000000..9c2f81b
--- /dev/null
+++ b/tests/PathUtilsTests.cs
@@ -0,0 +1,105 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#nullable enable
+
+using System.IO;
+using NUnit.Framework;
+using Xamarin.MacDev;
+
+namespace tests {
+
+ [TestFixture]
+ public class PathUtilsTests {
+
+ [Test]
+ [Platform ("MacOsX")]
+ public void IsSymlink_ReturnsFalse_ForNonExistentFile ()
+ {
+ var path = Path.Combine (Path.GetTempPath (), Path.GetRandomFileName ());
+ // Should not throw; returns false for ENOENT
+ Assert.That (PathUtils.IsSymlink (path), Is.False);
+ }
+
+ [Test]
+ [Platform ("MacOsX")]
+ public void IsSymlink_ReturnsFalse_ForRegularFile ()
+ {
+ var path = Path.GetTempFileName ();
+ try {
+ Assert.That (PathUtils.IsSymlink (path), Is.False);
+ } finally {
+ File.Delete (path);
+ }
+ }
+
+ [Test]
+ [Platform ("MacOsX")]
+ public void IsSymlink_ReturnsTrue_ForSymlink ()
+ {
+ var target = Path.GetTempFileName ();
+ var link = target + ".link";
+ try {
+#if NET
+ File.CreateSymbolicLink (link, target);
+#else
+ // File.CreateSymbolicLink is not available on net472.
+ // Use a shell command to create the symlink on macOS.
+ var psi = new System.Diagnostics.ProcessStartInfo ("ln", $"-s \"{target}\" \"{link}\"") {
+ UseShellExecute = false,
+ };
+ System.Diagnostics.Process.Start (psi)!.WaitForExit ();
+#endif
+ Assert.That (PathUtils.IsSymlink (link), Is.True);
+ } finally {
+ File.Delete (link);
+ File.Delete (target);
+ }
+ }
+
+ [Test]
+ [Platform ("MacOsX")]
+ public void IsSymlinkOrHasParentSymlink_ReturnsFalse_ForNonExistentPath ()
+ {
+ var path = Path.Combine (Path.GetTempPath (), Path.GetRandomFileName ());
+ Assert.That (PathUtils.IsSymlinkOrHasParentSymlink (path), Is.False);
+ }
+
+ [Test]
+ [Platform ("MacOsX")]
+ public void IsSymlink_ReturnsFalse_WhenPathComponentIsNotDirectory ()
+ {
+ // /etc/hosts is a file, so /etc/hosts/bogus triggers ENOTDIR
+ var path = Path.Combine ("/etc/hosts", "bogus");
+ Assert.That (PathUtils.IsSymlink (path), Is.False);
+ }
+
+ [Test]
+ [Platform ("MacOsX")]
+ public void IsSymlinkOrHasParentSymlink_ReturnsTrue_WhenParentIsSymlink ()
+ {
+ var realDir = Path.Combine (Path.GetTempPath (), Path.GetRandomFileName ());
+ Directory.CreateDirectory (realDir);
+ var childDir = Path.Combine (realDir, "subdir");
+ Directory.CreateDirectory (childDir);
+
+ var linkDir = Path.Combine (Path.GetTempPath (), Path.GetRandomFileName ());
+ try {
+#if NET
+ Directory.CreateSymbolicLink (linkDir, realDir);
+#else
+ var psi = new System.Diagnostics.ProcessStartInfo ("ln", $"-s \"{realDir}\" \"{linkDir}\"") {
+ UseShellExecute = false,
+ };
+ System.Diagnostics.Process.Start (psi)!.WaitForExit ();
+#endif
+ var childViaLink = Path.Combine (linkDir, "subdir");
+ Assert.That (PathUtils.IsSymlinkOrHasParentSymlink (childViaLink), Is.True);
+ } finally {
+ if (Directory.Exists (linkDir))
+ Directory.Delete (linkDir);
+ Directory.Delete (realDir, recursive: true);
+ }
+ }
+ }
+}