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); + } + } + } +}