Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/libraries/Common/src/Interop/Windows/Interop.Errors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Comment on lines +42 to +49
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not related the PR but I wonder why do such structs returned from P/Invokes would not be readonly structs?

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
using System.Runtime.CompilerServices;
using Microsoft.Win32;
using Xunit;
using System.IO.Enumeration;
using System.Linq;

namespace System
{
Expand Down Expand Up @@ -289,6 +291,44 @@ public static bool IsSubstAvailable
}
}

/// <summary>
/// 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.
/// </summary>
public static bool HasUsableAppExecLinksDirectory
{
get
{
try
{
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 Directory.GetFiles(windowsAppsDir, "*.exe", new EnumerationOptions() { RecurseSubdirectories = true, MaxRecursionDepth = 3 }).Length > 0;
return new FileSystemEnumerable<string?>(
windowsAppsDir,
(ref FileSystemEntry entry) => null,
new EnumerationOptions { RecurseSubdirectories = true })
{
ShouldIncludePredicate = (ref FileSystemEntry entry) =>
FileSystemName.MatchesWin32Expression("*.exe", entry.FileName) &&
entry.Attributes.HasFlag(FileAttributes.ReparsePoint)
}.Any();
}
}
}
catch { }
return false;
}
}

private static Version GetICUVersion()
{
int version = 0;
Expand Down
57 changes: 57 additions & 0 deletions src/libraries/System.IO.FileSystem/tests/AppExecLinks.Windows.cs
Original file line number Diff line number Diff line change
@@ -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))]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if I skipped this - will this throw if HasUsableAppExecLinksDirectory return false or the test could be skipped always silently in CIs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should return false and cause the test to be skipped due to the absence of a WindowsApps dir.

The property this attribute is pointing to, should not throw. If it throws, it's a bug and needs to be fixed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern is that in the case the tests can be never really evaluated on CIs.

[InlineData(false)]
[InlineData(true)]
public void ResolveAppExecLinkTargets(bool returnFinalTarget)
{
string windowsAppsDir = Path.Join(Environment.GetEnvironmentVariable("LOCALAPPDATA"), "Microsoft", "WindowsApps");
var appExecLinkPaths = new FileSystemEnumerable<string?>(
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));
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
<Compile Include="FileStream\ctor_options_as.Windows.cs" />
<Compile Include="FileStream\FileStreamConformanceTests.Windows.cs" />
<Compile Include="Junctions.Windows.cs" />
<Compile Include="AppExecLinks.Windows.cs" />
<Compile Include="RandomAccess\Mixed.Windows.cs" />
<Compile Include="RandomAccess\NoBuffering.Windows.cs" />
<Compile Include="RandomAccess\SectorAlignedMemory.Windows.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -539,13 +539,59 @@ 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's a long list of IO_REPARSE_TAG_* .. how is this one special?
https://docs.microsoft.com/en-us/windows/win32/fileio/reparse-point-tags

Copy link
Member

@jozkee jozkee Aug 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is one of the reparse tags proven to point to another file/folder and it is also one of the three tags covered by PowerShell: https://github.com/PowerShell/PowerShell/blob/008f4b057fdc1482d3200de0a61d23570b4d30e4/src/System.Management.Automation/namespaces/FileSystemProvider.cs#L8217-L8230

I am uncertain if all the reparse tags in the link you posted indicate that the reparse point contains a reference to another file/folder, but having these 3 tags (Symlinks, MountPoints and AppExecLinks) seems to me like a good start.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup fair enough, if Powershell has had this support for a while, that's good evidence those are the ones that matter.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is this one special?

It is unnormal reparse point :-) so we had to add it explicitly in PowerShell.

In common, reparse point list is "open" and if new reparse points will be added in future perhaps we will have to add its explicitly too.

I'd remind about OneDrive. .Net API should be tested for OneDrive paths/links too (at list manually).

Copy link
Contributor Author

@carlossanlop carlossanlop Aug 31, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I manually tested enumeration of the OneDrive directory (ensuring there were cloud files) and the behavior was the same in 3.1, 5.0 and 6.0 (including the changes in this PR): I can read the file name and the file attributes, and the files are not getting unexpectedly downloaded - their cloud icon stays next to the files when viewing them in the File Explorer.

@iSazonov by any chance, do you know any specific scenarios in which the OneDrive files get unexpectedly downloaded when manipulating them with .NET? I am not sure if you've mentioned this strange behavior in the past, or if it was someone else. It would be a good chance for me to verify it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@carlossanlop These links are related OneDrive and could be interesting for you:
PowerShell/PowerShell#9895
PowerShell/PowerShell#9509
PowerShell/PowerShell#8745

Also for removing PowerShell/PowerShell#15571

Feel free to ask me if you need additional comments.

{
success = MemoryMarshal.TryRead(bufferSpan, out Interop.Kernel32.AppExecLinkReparseBuffer rbAppExecLink);
Debug.Assert(success);

// The target file is at index 2
if (rbAppExecLink.StringCount >= 3)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This SO answer says that this field is actually named Version https://stackoverflow.com/a/65583702/4231460. It may be a good idea to ask the Windows team what's the exact definition of this struct and validate if we are using it correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From our conversation with the Windows team, this code is doing the correct job.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe show an example in a comment here, to explain the mysterious magic number.
We want to only use documented Windows API - but I see this is documented https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/c3a420cb-8a72-4adf-87e8-eee95379d78f

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or just "see documentation for REPARSE_DATA_BUFFER"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PowerShell team found the const in Windows SDK so it is public and the fact opened a way to use the const.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iSazonov can you share a link to the public Windows SDK location where you found it?

I'm not sure such a link would qualify as "public documentation" though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@carlossanlop If I remember right @SteveL-MSFT found this in Windows SDK files (I guess in *.cpp).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember where exactly I got the original buffer struct from, but given the name, I think most likely from the SO post. Since I can't find any public docs on the struct, it should probably not be relied upon as it could change. I'll start an internal thread with the AppX team to see about getting it documented publicly.

Copy link
Contributor

@iSazonov iSazonov Sep 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{
int stringListOffset = sizeof(Interop.Kernel32.AppExecLinkReparseBuffer);
Span<char> stringList = MemoryMarshal.Cast<byte, char>(bufferSpan.Slice(stringListOffset));

return GetAppExecLinkTarget(stringList);
}
}

return null;
}
finally
{
ArrayPool<byte>.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<char> 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)
{
start = i + 1; // exclude null char
}
else if (count == 3)
{
end = i;
break;
}
}
}
if (start != -1 && end != -1)
{
return stringList.Slice(start, end - start).ToString();
}

return null;
}
}

private static unsafe string? GetFinalLinkTarget(string linkPath, bool isDirectory)
Expand All @@ -555,9 +601,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;
}
Expand All @@ -572,7 +619,7 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i
// If the handle fails because it is unreachable, is because the link was broken.
// We need to fallback to manually traverse the links and return the target of the last resolved link.
int error = Marshal.GetLastWin32Error();
if (IsPathUnreachableError(error))
if (IsPathUnreachableError(error) || error == Interop.Errors.ERROR_CANT_ACCESS_FILE /* Possibly broken AppExecLink */)
{
return GetFinalLinkTargetSlow(linkPath);
}
Expand Down