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