From dbab85297f5e751f9089b822c01d0ac156bc6b09 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL)" Date: Thu, 8 Aug 2019 19:59:46 -0700 Subject: [PATCH 1/4] Add support for AppX links --- .../namespaces/FileSystemProvider.cs | 82 ++++++++++++------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/src/System.Management.Automation/namespaces/FileSystemProvider.cs b/src/System.Management.Automation/namespaces/FileSystemProvider.cs index ddfe724fca9..1618b57a2a3 100644 --- a/src/System.Management.Automation/namespaces/FileSystemProvider.cs +++ b/src/System.Management.Automation/namespaces/FileSystemProvider.cs @@ -1845,11 +1845,10 @@ private void Dir( // a) the user has asked to with the -FollowSymLinks switch parameter and // b) the directory pointed to by the symlink has not already been visited, // preventing symlink loops. - // c) it is not a name surrogate making it not a symlink + // c) it is not a reparse point with a target if (tracker == null) { - if (InternalSymbolicLinkLinkCodeMethods.IsReparsePoint(recursiveDirectory) && - InternalSymbolicLinkLinkCodeMethods.IsNameSurrogateReparsePoint(recursiveDirectory.FullName)) + if (InternalSymbolicLinkLinkCodeMethods.IsReparsePointWithTarget(recursiveDirectory)) { continue; } @@ -2001,8 +2000,7 @@ string ToModeString(FileSystemInfo fileSystemInfo) public static string NameString(PSObject instance) { return instance?.BaseObject is FileSystemInfo fileInfo - ? (InternalSymbolicLinkLinkCodeMethods.IsReparsePoint(fileInfo) && - InternalSymbolicLinkLinkCodeMethods.IsNameSurrogateReparsePoint(fileInfo.FullName)) + ? InternalSymbolicLinkLinkCodeMethods.IsReparsePointWithTarget(fileInfo) ? $"{fileInfo.Name} -> {InternalSymbolicLinkLinkCodeMethods.GetTarget(instance)}" : fileInfo.Name : string.Empty; @@ -7704,6 +7702,8 @@ public static class InternalSymbolicLinkLinkCodeMethods private const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003; + private const uint IO_REPARSE_TAG_APPEXECLINK = 0x8000001B; + private const string NonInterpretedPathPrefix = @"\??\"; private const int MAX_PATH = 260; @@ -7790,6 +7790,17 @@ private struct REPARSE_DATA_BUFFER_MOUNTPOINT public byte[] PathBuffer; } + [StructLayout(LayoutKind.Sequential)] + private struct REPARSE_DATA_BUFFER_APPEXECLINK + { + public uint ReparseTag; + public ushort ReparseDataLength; + public ushort Reserved; + public uint StringCount; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x3FF0)] + public byte[] StringList; + } + [StructLayout(LayoutKind.Sequential)] private struct BY_HANDLE_FILE_INFORMATION { @@ -7990,9 +8001,17 @@ private static string WinInternalGetLinkType(string filePath) REPARSE_DATA_BUFFER_SYMBOLICLINK reparseDataBuffer = Marshal.PtrToStructure(outBuffer); if (reparseDataBuffer.ReparseTag == IO_REPARSE_TAG_SYMLINK) + { linkType = "SymbolicLink"; + } else if (reparseDataBuffer.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) + { linkType = "Junction"; + } + else if (reparseDataBuffer.ReparseTag == IO_REPARSE_TAG_APPEXECLINK) + { + linkType = "AppExeCLink"; + } else { linkType = IsHardLink(ref dangerousHandle) ? "HardLink" : null; @@ -8026,22 +8045,22 @@ internal static bool IsReparsePoint(FileSystemInfo fileInfo) return fileInfo.Attributes.HasFlag(System.IO.FileAttributes.ReparsePoint); } - internal static bool IsNameSurrogateReparsePoint(string filePath) + internal static bool IsReparsePointWithTarget(FileSystemInfo fileInfo) { + if (!IsReparsePoint(fileInfo)) + { + return false; + } #if !UNIX var data = new WIN32_FIND_DATA(); - using (SafeFileHandle handle = FindFirstFileEx(filePath, FINDEX_INFO_LEVELS.FindExInfoBasic, ref data, FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, 0)) + using (SafeFileHandle handle = FindFirstFileEx(fileInfo.FullName, FINDEX_INFO_LEVELS.FindExInfoBasic, ref data, FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, 0)) { - // Name surrogates are reparse points that point to other named entities local to the filesystem (like symlinks) - // In the case of OneDrive, they are not surrogates and would be safe to recurse into. - // This code is equivalent to the IsReparseTagNameSurrogate macro: https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntifs/nf-ntifs-isreparsetagnamesurrogate - if (!handle.IsInvalid && (data.dwReserved0 & 0x20000000) == 0) + if (!handle.IsInvalid && (data.dwReserved0 & 0x20000000) == 0 && (data.dwReserved0 & IO_REPARSE_TAG_APPEXECLINK) != IO_REPARSE_TAG_APPEXECLINK) { return false; } } #endif - // true means the reparse point is a symlink return true; } @@ -8205,30 +8224,33 @@ private static string WinInternalGetTarget(SafeFileHandle handle) throw new Win32Exception(lastError); } - // Unmarshal to symbolic link to look for tags. - REPARSE_DATA_BUFFER_SYMBOLICLINK reparseDataBuffer = Marshal.PtrToStructure(outBuffer); - - if (reparseDataBuffer.ReparseTag != IO_REPARSE_TAG_SYMLINK && reparseDataBuffer.ReparseTag != IO_REPARSE_TAG_MOUNT_POINT) - return null; - string targetDir = null; - if (reparseDataBuffer.ReparseTag == IO_REPARSE_TAG_SYMLINK) - { - targetDir = Encoding.Unicode.GetString(reparseDataBuffer.PathBuffer, reparseDataBuffer.SubstituteNameOffset, reparseDataBuffer.SubstituteNameLength); - } + REPARSE_DATA_BUFFER_SYMBOLICLINK reparseDataBuffer = Marshal.PtrToStructure(outBuffer); - if (reparseDataBuffer.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) + switch (reparseDataBuffer.ReparseTag) { - // Since this is a junction we need to unmarshal to the correct structure. - REPARSE_DATA_BUFFER_MOUNTPOINT reparseDataBufferMountPoint = Marshal.PtrToStructure(outBuffer); + case IO_REPARSE_TAG_SYMLINK: + targetDir = Encoding.Unicode.GetString(reparseDataBuffer.PathBuffer, reparseDataBuffer.SubstituteNameOffset, reparseDataBuffer.SubstituteNameLength); + break; - targetDir = Encoding.Unicode.GetString(reparseDataBufferMountPoint.PathBuffer, reparseDataBufferMountPoint.SubstituteNameOffset, reparseDataBufferMountPoint.SubstituteNameLength); - } + case IO_REPARSE_TAG_MOUNT_POINT: + REPARSE_DATA_BUFFER_MOUNTPOINT reparseMountPointDataBuffer = Marshal.PtrToStructure(outBuffer); + targetDir = Encoding.Unicode.GetString(reparseMountPointDataBuffer.PathBuffer, reparseMountPointDataBuffer.SubstituteNameOffset, reparseMountPointDataBuffer.SubstituteNameLength); + break; - if (targetDir != null && targetDir.StartsWith(NonInterpretedPathPrefix, StringComparison.OrdinalIgnoreCase)) - { - targetDir = targetDir.Substring(NonInterpretedPathPrefix.Length); + case IO_REPARSE_TAG_APPEXECLINK: + REPARSE_DATA_BUFFER_APPEXECLINK reparseAppExeDataBuffer = Marshal.PtrToStructure(outBuffer); + // The target file is at index 2 + if (reparseAppExeDataBuffer.StringCount >= 3) + { + string temp = Encoding.Unicode.GetString(reparseAppExeDataBuffer.StringList); + targetDir = temp.Split('\0')[2]; + } + break; + + default: + return null; } return targetDir; From 1a9c5725277e9757da4ff3dad9f4f1916d378c68 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL)" Date: Thu, 8 Aug 2019 20:34:12 -0700 Subject: [PATCH 2/4] Fix accidental removal of code to fixup target path name --- .../namespaces/FileSystemProvider.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/System.Management.Automation/namespaces/FileSystemProvider.cs b/src/System.Management.Automation/namespaces/FileSystemProvider.cs index 1618b57a2a3..f4d281c661f 100644 --- a/src/System.Management.Automation/namespaces/FileSystemProvider.cs +++ b/src/System.Management.Automation/namespaces/FileSystemProvider.cs @@ -8253,6 +8253,11 @@ private static string WinInternalGetTarget(SafeFileHandle handle) return null; } + if (targetDir != null && targetDir.StartsWith(NonInterpretedPathPrefix, StringComparison.OrdinalIgnoreCase)) + { + targetDir = targetDir.Substring(NonInterpretedPathPrefix.Length); + } + return targetDir; } finally From c642b05dfb51ec2f52e4f3f121908026f6234370 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL)" Date: Fri, 9 Aug 2019 12:27:00 -0700 Subject: [PATCH 3/4] address Ilya's feedback --- .../namespaces/FileSystemProvider.cs | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/System.Management.Automation/namespaces/FileSystemProvider.cs b/src/System.Management.Automation/namespaces/FileSystemProvider.cs index f4d281c661f..c383f9493e9 100644 --- a/src/System.Management.Automation/namespaces/FileSystemProvider.cs +++ b/src/System.Management.Automation/namespaces/FileSystemProvider.cs @@ -8000,21 +8000,23 @@ private static string WinInternalGetLinkType(string filePath) REPARSE_DATA_BUFFER_SYMBOLICLINK reparseDataBuffer = Marshal.PtrToStructure(outBuffer); - if (reparseDataBuffer.ReparseTag == IO_REPARSE_TAG_SYMLINK) + switch (reparseDataBuffer.ReparseTag) { - linkType = "SymbolicLink"; - } - else if (reparseDataBuffer.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) - { - linkType = "Junction"; - } - else if (reparseDataBuffer.ReparseTag == IO_REPARSE_TAG_APPEXECLINK) - { - linkType = "AppExeCLink"; - } - else - { - linkType = IsHardLink(ref dangerousHandle) ? "HardLink" : null; + case IO_REPARSE_TAG_SYMLINK: + linkType = "SymbolicLink"; + break; + + case IO_REPARSE_TAG_MOUNT_POINT: + linkType = "Junction"; + break; + + case IO_REPARSE_TAG_APPEXECLINK: + linkType = "AppExeCLink"; + break; + + default: + linkType = IsHardLink(ref dangerousHandle) ? "HardLink" : null; + break; } return linkType; @@ -8055,6 +8057,9 @@ internal static bool IsReparsePointWithTarget(FileSystemInfo fileInfo) var data = new WIN32_FIND_DATA(); using (SafeFileHandle handle = FindFirstFileEx(fileInfo.FullName, FINDEX_INFO_LEVELS.FindExInfoBasic, ref data, FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, 0)) { + // Name surrogates (0x20000000) are reparse points that point to other named entities local to the filesystem (like symlinks) + // In the case of OneDrive, they are not surrogates and would be safe to recurse into. + // This code is equivalent to the IsReparseTagNameSurrogate macro: https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntifs/nf-ntifs-isreparsetagnamesurrogate if (!handle.IsInvalid && (data.dwReserved0 & 0x20000000) == 0 && (data.dwReserved0 & IO_REPARSE_TAG_APPEXECLINK) != IO_REPARSE_TAG_APPEXECLINK) { return false; From a6a66e56fb8ee864a04e3b4e08b56a9b3254619d Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL)" Date: Tue, 13 Aug 2019 09:17:06 -0700 Subject: [PATCH 4/4] add test for appexeclink --- .../Get-ChildItem.Tests.ps1 | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/Get-ChildItem.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/Get-ChildItem.Tests.ps1 index fa91e5a132e..b9e015cbc53 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Management/Get-ChildItem.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Management/Get-ChildItem.Tests.ps1 @@ -32,6 +32,12 @@ Describe "Get-ChildItem" -Tags "CI" { @{Parameters = @{Path = (Join-Path $searchRoot '*'); Recurse = $true; File = $true }; ExpectedCount = 1; Title = "file with wildcard"}, @{Parameters = @{Path = (Join-Path $searchRoot 'F*.txt'); Recurse = $true; File = $true }; ExpectedCount = 1; Title = "file with wildcard filename"} ) + + $SkipAppExeCLinks = $true + if ($IsWindows -and (Get-ChildItem -Path ~\AppData\Local\Microsoft\WindowsApps\*.exe -ErrorAction Ignore) -ne $null) + { + $SkipAppExeCLinks = $false + } } It "Should list the contents of the current folder" { @@ -174,6 +180,12 @@ Describe "Get-ChildItem" -Tags "CI" { $null = New-Item -Path TestDrive:/noextension -ItemType File (Get-ChildItem -File -LiteralPath TestDrive:/ -Filter noext*.*).Name | Should -BeExactly 'noextension' } + + It "Understand APPEXECLINKs" -Skip:($SkipAppExeCLinks) { + $app = Get-ChildItem -Path ~\appdata\local\microsoft\windowsapps\*.exe | Select-Object -First 1 + $app.Target | Should -Not -Be $app.FullName + $app.LinkType | Should -BeExactly 'AppExeCLink' + } } Context 'Env: Provider' {