diff --git a/src/libraries/Common/src/Interop/Windows/Interop.Errors.cs b/src/libraries/Common/src/Interop/Windows/Interop.Errors.cs
index d5f6d1637507fa..9cfa9863b62900 100644
--- a/src/libraries/Common/src/Interop/Windows/Interop.Errors.cs
+++ b/src/libraries/Common/src/Interop/Windows/Interop.Errors.cs
@@ -91,6 +91,7 @@ internal static partial class Errors
internal const int ERROR_EVENTLOG_FILE_CHANGED = 0x5DF;
internal const int ERROR_TRUSTED_RELATIONSHIP_FAILURE = 0x6FD;
internal const int ERROR_RESOURCE_LANG_NOT_FOUND = 0x717;
+ internal const int ERROR_CANT_ACCESS_FILE = 0x780;
internal const int ERROR_NOT_A_REPARSE_POINT = 0x1126;
}
}
diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FileOperations.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FileOperations.cs
index cc4896c1c52e48..4e0349d6fe30f1 100644
--- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FileOperations.cs
+++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FileOperations.cs
@@ -10,6 +10,7 @@ internal static partial class IOReparseOptions
internal const uint IO_REPARSE_TAG_FILE_PLACEHOLDER = 0x80000015;
internal const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003;
internal const uint IO_REPARSE_TAG_SYMLINK = 0xA000000C;
+ internal const uint IO_REPARSE_TAG_APPEXECLINK = 0x8000001B;
}
internal static partial class FileOperations
diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.REPARSE_DATA_BUFFER.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.REPARSE_DATA_BUFFER.cs
index 123ac9235b9fdc..5ba3712b69964d 100644
--- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.REPARSE_DATA_BUFFER.cs
+++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.REPARSE_DATA_BUFFER.cs
@@ -38,5 +38,14 @@ internal struct MountPointReparseBuffer
public ushort PrintNameOffset;
public ushort PrintNameLength;
}
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct AppExecLinkReparseBuffer
+ {
+ public uint ReparseTag;
+ public ushort ReparseDataLength;
+ public ushort Reserved;
+ public uint StringCount;
+ }
}
}
diff --git a/src/libraries/Common/src/System/IO/FileSystem.Attributes.Windows.cs b/src/libraries/Common/src/System/IO/FileSystem.Attributes.Windows.cs
index ad087304b4e5ac..81fd4005d6210a 100644
--- a/src/libraries/Common/src/System/IO/FileSystem.Attributes.Windows.cs
+++ b/src/libraries/Common/src/System/IO/FileSystem.Attributes.Windows.cs
@@ -118,6 +118,8 @@ internal static int FillAttributeInfo(string? path, ref Interop.Kernel32.WIN32_F
internal static bool IsPathUnreachableError(int errorCode)
{
+ // This switch should *not* catch:
+ // ERROR_ACCESS_DENIED, ERROR_SHARING_VIOLATION, ERROR_SEM_TIMEOUT
switch (errorCode)
{
case Interop.Errors.ERROR_FILE_NOT_FOUND:
@@ -132,6 +134,7 @@ internal static bool IsPathUnreachableError(int errorCode)
case Interop.Errors.ERROR_NETWORK_ACCESS_DENIED:
case Interop.Errors.ERROR_INVALID_HANDLE: // eg from \\.\CON
case Interop.Errors.ERROR_FILENAME_EXCED_RANGE: // Path is too long
+ case Interop.Errors.ERROR_CANT_ACCESS_FILE: // Broken AppExecLink files may cause this
return true;
default:
return false;
diff --git a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs
index fbd5df9afdde75..fb6619de22e94e 100644
--- a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs
+++ b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs
@@ -11,6 +11,8 @@
using System.Runtime.CompilerServices;
using Microsoft.Win32;
using Xunit;
+using System.IO.Enumeration;
+using System.Linq;
namespace System
{
@@ -289,6 +291,39 @@ public static bool IsSubstAvailable
}
}
+ ///
+ /// Returns true if the current Windows machine has an Application Execution Alias directory
+ /// located in %LOCALAPPDATA%\Microsoft\WindowsApps, with at least one *.exe file inside.
+ ///
+ public static bool HasUsableAppExecLinksDirectory
+ {
+ get
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ string localAppData = Environment.GetEnvironmentVariable("LOCALAPPDATA");
+ if (string.IsNullOrWhiteSpace(localAppData))
+ {
+ return false;
+ }
+ string windowsAppsDir = Path.Join(localAppData, "Microsoft", "WindowsApps");
+ if (Directory.Exists(windowsAppsDir))
+ {
+ return new FileSystemEnumerable(
+ windowsAppsDir,
+ (ref FileSystemEntry entry) => null,
+ new EnumerationOptions { RecurseSubdirectories = true })
+ {
+ ShouldIncludePredicate = (ref FileSystemEntry entry) =>
+ FileSystemName.MatchesWin32Expression("*.exe", entry.FileName) &&
+ entry.Attributes.HasFlag(FileAttributes.ReparsePoint)
+ }.Any();
+ }
+ }
+ return false;
+ }
+ }
+
private static Version GetICUVersion()
{
int version = 0;
diff --git a/src/libraries/System.IO.FileSystem/tests/AppExecLinks.Windows.cs b/src/libraries/System.IO.FileSystem/tests/AppExecLinks.Windows.cs
new file mode 100644
index 00000000000000..13117b2762063e
--- /dev/null
+++ b/src/libraries/System.IO.FileSystem/tests/AppExecLinks.Windows.cs
@@ -0,0 +1,57 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.IO.Enumeration;
+using System.Linq;
+using Xunit;
+
+namespace System.IO.Tests
+{
+ [PlatformSpecific(TestPlatforms.Windows)]
+ public class AppExecLinks : BaseSymbolicLinks
+ {
+ [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.HasUsableAppExecLinksDirectory))]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void ResolveAppExecLinkTargets(bool returnFinalTarget)
+ {
+ string windowsAppsDir = Path.Join(Environment.GetEnvironmentVariable("LOCALAPPDATA"), "Microsoft", "WindowsApps");
+ var appExecLinkPaths = new FileSystemEnumerable(
+ windowsAppsDir,
+ (ref FileSystemEntry entry) => entry.ToFullPath(),
+ new EnumerationOptions { RecurseSubdirectories = true })
+ {
+ ShouldIncludePredicate = (ref FileSystemEntry entry) =>
+ FileSystemName.MatchesWin32Expression("*.exe", entry.FileName) &&
+ entry.Attributes.HasFlag(FileAttributes.ReparsePoint)
+ };
+
+ foreach (string appExecLinkPath in appExecLinkPaths)
+ {
+ FileInfo linkInfo = new(appExecLinkPath);
+ Assert.Equal(0, linkInfo.Length);
+ Assert.True(linkInfo.Attributes.HasFlag(FileAttributes.ReparsePoint));
+
+ string? linkTarget = linkInfo.LinkTarget;
+ Assert.NotNull(linkTarget);
+ Assert.NotEqual(appExecLinkPath, linkTarget);
+
+ FileSystemInfo? targetInfoFromFileInfo = linkInfo.ResolveLinkTarget(returnFinalTarget);
+ VerifyFileInfo(targetInfoFromFileInfo);
+
+ FileSystemInfo? targetInfoFromFile = File.ResolveLinkTarget(appExecLinkPath, returnFinalTarget);
+ VerifyFileInfo(targetInfoFromFile);
+ }
+
+ void VerifyFileInfo(FileSystemInfo? info)
+ {
+ Assert.True(info is FileInfo);
+ if (info.Exists) // The target may not exist, that's ok
+ {
+ Assert.True(((FileInfo)info).Length > 0);
+ Assert.False(info.Attributes.HasFlag(FileAttributes.ReparsePoint));
+ }
+ }
+ }
+ }
+}
diff --git a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj
index bb7cac297d73a3..6c6a6381e03cf3 100644
--- a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj
+++ b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj
@@ -75,6 +75,7 @@
+
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs
index d2bde47c3134b6..919d23d9dea9f0 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs
@@ -539,6 +539,20 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i
Debug.Assert(!PathInternal.IsPartiallyQualified(targetPath));
return targetPath.ToString();
}
+ else if (rbSymlink.ReparseTag == Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_APPEXECLINK)
+ {
+ success = MemoryMarshal.TryRead(bufferSpan, out Interop.Kernel32.AppExecLinkReparseBuffer rbAppExecLink);
+ Debug.Assert(success);
+
+ // The target file is at index 2
+ if (rbAppExecLink.StringCount >= 3)
+ {
+ int stringListOffset = sizeof(Interop.Kernel32.AppExecLinkReparseBuffer);
+ Span stringList = MemoryMarshal.Cast(bufferSpan.Slice(stringListOffset));
+
+ return GetAppExecLinkTarget(stringList);
+ }
+ }
return null;
}
@@ -546,6 +560,46 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i
{
ArrayPool.Shared.Return(buffer);
}
+
+ // Given an appexeclink reparse point string list, finds the
+ // start and end (null chars) of the 3rd string in the list.
+ static string? GetAppExecLinkTarget(ReadOnlySpan stringList)
+ {
+ // Find the 2nd and 3rd null char
+ int start = -1;
+ int end = -1;
+ int count = 0;
+ for (int i = 0; i < stringList.Length; i++)
+ {
+ if (stringList[i] == '\0')
+ {
+ count++;
+ if (count == 2)
+ {
+ if (i + 1 >= stringList.Length)
+ {
+ // Unexpectedly reached the end of the stringList
+ // There won't be a third null char
+ break;
+ }
+
+ start = i + 1; // +1 to exclude null char
+ }
+ else if (count == 3)
+ {
+ end = i;
+ if (start > 0 && end > start)
+ {
+ return stringList
+ .Slice(start + 1, end - start - 1)
+ .ToString();
+ }
+ }
+ }
+ }
+
+ return null;
+ }
}
private static unsafe string? GetFinalLinkTarget(string linkPath, bool isDirectory)
@@ -555,9 +609,10 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i
// The file or directory is not a reparse point.
if ((data.dwFileAttributes & (uint)FileAttributes.ReparsePoint) == 0 ||
- // Only symbolic links and mount points are supported at the moment.
+ // Only symbolic links, mount points (junctions) and app exec links are supported at the moment.
((data.dwReserved0 & Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_SYMLINK) == 0 &&
- (data.dwReserved0 & Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_MOUNT_POINT) == 0))
+ (data.dwReserved0 & Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_MOUNT_POINT) == 0 &&
+ (data.dwReserved0 & Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_APPEXECLINK) == 0))
{
return null;
}