diff --git a/src/System.Management.Automation/engine/Interop/Windows/QueryDosDevice.cs b/src/System.Management.Automation/engine/Interop/Windows/QueryDosDevice.cs new file mode 100644 index 0000000000..670c6254cf --- /dev/null +++ b/src/System.Management.Automation/engine/Interop/Windows/QueryDosDevice.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +#if !UNIX +using System; +using System.Buffers; +using System.ComponentModel; +using System.Management.Automation; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Windows + { + [LibraryImport(PinvokeDllNames.QueryDosDeviceDllName, EntryPoint = "QueryDosDeviceW", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)] + internal static partial int QueryDosDevice(Span lpDeviceName, Span lpTargetPath, uint ucchMax); + + internal static string GetDosDeviceForNetworkPath(char deviceName) + { + // By default buffer size is set to 300 which would generally be sufficient in most of the cases. + const int StartLength = +#if DEBUG + // In debug, validate ArrayPool growth. + 1; +#else + 300; +#endif + + Span buffer = stackalloc char[StartLength + 1]; + Span fullDeviceName = stackalloc char[3] { deviceName, ':', '\0' }; + char[]? rentedArray = null; + + try + { + while (true) + { + uint length = (uint)buffer.Length; + int retValue = QueryDosDevice(fullDeviceName, buffer, length); + if (retValue > 0) + { + if (buffer.StartsWith("\\??\\")) + { + // QueryDosDevice always return array of NULL-terinating strings with additional final NULL + // so the buffer has always two NULL-s on end. + // + // "\\??\\UNC\\localhost\\c$\\tmp\0\0" -> "UNC\\localhost\\c$\\tmp\0\0" + Span res = buffer.Slice(4); + if (res.StartsWith("UNC")) + { + // -> "C\\localhost\\c$\\tmp\0\0" -> "\\\\localhost\\c$\\tmp" + // + // We need to take only first null-terminated string as QueryDosDevice() docs say. + int i = 3; + for (; i < res.Length; i++) + { + if (res[i] == '\0') + { + break; + } + } + + Diagnostics.Assert(i < res.Length, "Broken QueryDosDevice() buffer."); + + res = res.Slice(2, i); + res[0] = '\\'; + + // If we want always to have terminating slash -> "\\\\localhost\\c$\\tmp\\" + // res = res.Slice(2, retValue - 3); + // res[0] = '\\'; + // res[^1] = '\\'; + } + // else if (res[^3] == ':') + // { + // Diagnostics.Assert(false, "Really it is a dead code since GetDosDevice() is called only if PSDrive.DriveType == DriveType.Network"); + + // // The substed path is the root path of a drive. For example: subst Y: C:\ + // // -> "C:\0\0" -> "C:\" + // res = res.Slice(0, retValue - 1); + // res[^1] = '\\'; + // } + else + { + throw new Exception("GetDosDeviceForNetworkPath() can be called only if PSDrive.DriveType == DriveType.Network."); + } + + return res.ToString(); + } + else + { + Diagnostics.Assert(false, "Really it is a dead code since GetDosDevice() is called only if PSDrive.DriveType == DriveType.Network"); + + // The drive name is not a substed path, then we return the root path of the drive + // "C:\0" -> "C:\\" + fullDeviceName[^1] = '\\'; + return fullDeviceName.ToString(); + } + } + + const int ERROR_INSUFFICIENT_BUFFER = 122; + int errorCode = Marshal.GetLastPInvokeError(); + if (errorCode != ERROR_INSUFFICIENT_BUFFER) + { + throw new Win32Exception((int)errorCode); + } + + char[]? toReturn = rentedArray; + buffer = rentedArray = ArrayPool.Shared.Rent(buffer.Length * 2); + if (toReturn is not null) + { + ArrayPool.Shared.Return(toReturn); + } + } + } + finally + { + if (rentedArray is not null) + { + ArrayPool.Shared.Return(rentedArray); + } + } + } + } +} +#endif diff --git a/src/System.Management.Automation/namespaces/FileSystemProvider.cs b/src/System.Management.Automation/namespaces/FileSystemProvider.cs index e528a9bb5a..e13ac80fa1 100644 --- a/src/System.Management.Automation/namespaces/FileSystemProvider.cs +++ b/src/System.Management.Automation/namespaces/FileSystemProvider.cs @@ -879,9 +879,9 @@ namespace Microsoft.PowerShell.Commands { #if UNIX throw new PlatformNotSupportedException(); + } #else return WinGetSubstitutedPathForNetworkDosDevice(driveName); -#endif } private static string WinGetSubstitutedPathForNetworkDosDevice(string driveName) @@ -889,76 +889,12 @@ namespace Microsoft.PowerShell.Commands string associatedPath = null; if (!string.IsNullOrEmpty(driveName) && driveName.Length == 1) { - // By default buffer size is set to 300 which would generally be sufficient in most of the cases. - int bufferSize = 300; - var pathInfo = new StringBuilder(bufferSize); - driveName += ':'; - - // Call the windows API - while (true) - { - pathInfo.EnsureCapacity(bufferSize); - int retValue = NativeMethods.QueryDosDevice(driveName, pathInfo, bufferSize); - if (retValue > 0) - { - // If the drive letter is a substed path, the result will be in the format of - // - "\??\C:\RealPath" for local path - // - "\??\UNC\RealPath" for network path - associatedPath = pathInfo.ToString(); - if (associatedPath.StartsWith("\\??\\", StringComparison.OrdinalIgnoreCase)) - { - associatedPath = associatedPath.Remove(0, 4); - if (associatedPath.StartsWith("UNC", StringComparison.OrdinalIgnoreCase)) - { - associatedPath = associatedPath.Remove(0, 3); - associatedPath = "\\" + associatedPath; - } - else if (associatedPath.EndsWith(':')) - { - // The substed path is the root path of a drive. For example: subst Y: C:\ - associatedPath += Path.DirectorySeparatorChar; - } - } - else - { - // The drive name is not a substed path, then we return the root path of the drive - associatedPath = driveName + "\\"; - } - - break; - } - - // Windows API call failed - int errorCode = Marshal.GetLastWin32Error(); - if (errorCode != 122) - { - // ERROR_INSUFFICIENT_BUFFER = 122 - // For an error other than "insufficient buffer", throw it - throw new Win32Exception((int)errorCode); - } - - // We got the "insufficient buffer" error. In this case we extend - // the buffer size, unless it's unreasonably too large. - if (bufferSize >= 32767) - { - // "The Windows API has many functions that also have Unicode versions to permit - // an extended-length path for a maximum total path length of 32,767 characters" - // See https://msdn.microsoft.com/library/aa365247.aspx#maxpath - string errorMsg = StringUtil.Format(FileSystemProviderStrings.SubstitutePathTooLong, driveName); - throw new InvalidOperationException(errorMsg); - } - - // Extend the buffer size and try again. - bufferSize *= 10; - if (bufferSize > 32767) - { - bufferSize = 32767; - } - } + associatedPath = Interop.Windows.GetDosDeviceForNetworkPath(driveName[0]); } return associatedPath; } +#endif /// /// Get the root path for a network drive or MS-DOS device. @@ -7293,26 +7229,6 @@ namespace Microsoft.PowerShell.Commands internal static partial bool PathIsNetworkPath(string path); #endif - /// - /// The function can obtain the current mapping for a particular MS-DOS device name. - /// - /// If lpDeviceName is non-NULL, the function retrieves information about the particular MS-DOS device specified by lpDeviceName. - /// The first null-terminated string stored into the buffer is the current mapping for the device. - /// The other null-terminated strings represent undeleted prior mappings for the device. - /// - /// - /// The particular MS-DOS device name. - /// - /// - /// The buffer to receive the result of the query. - /// - /// - /// The maximum number of characters that can be stored into the buffer - /// - /// - [DllImport(PinvokeDllNames.QueryDosDeviceDllName, CharSet = CharSet.Unicode, SetLastError = true)] - internal static extern int QueryDosDevice(string lpDeviceName, StringBuilder lpTargetPath, int ucchMax); - /// /// Creates a symbolic link using the native API. /// diff --git a/src/System.Management.Automation/resources/FileSystemProviderStrings.resx b/src/System.Management.Automation/resources/FileSystemProviderStrings.resx index 7f9035b1e9..894fe5f6af 100644 --- a/src/System.Management.Automation/resources/FileSystemProviderStrings.resx +++ b/src/System.Management.Automation/resources/FileSystemProviderStrings.resx @@ -273,9 +273,6 @@ The '{0}' and '{1}' parameters cannot be specified in the same command. - - The substitute path for the DOS device '{0}' is too long. It exceeds the maximum total path length (32,767 characters) that is valid for the Windows API. - A directory is required for the operation. The item '{0}' is not a directory. diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/FileSystemProviderExtended.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/FileSystemProviderExtended.Tests.ps1 index 199210cba6..3c20ec1072 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Management/FileSystemProviderExtended.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Management/FileSystemProviderExtended.Tests.ps1 @@ -659,7 +659,7 @@ Describe "FileSystem Provider Extended Tests for Get-ChildItem cmdlet" -Tags "CI } } -Describe "Validate Get-Item ResolvedTarget property" -Tag RequireAdminOnWindows { +Describe "Validate Get-Item ResolvedTarget property" -Tags "Feature","RequireAdminOnWindows" { BeforeAll { $rootDir = Join-Path "TestDrive:" "TestDir" diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/Get-PSDrive.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/Get-PSDrive.Tests.ps1 index 1e9f8217cc..f2684773a1 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Management/Get-PSDrive.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Management/Get-PSDrive.Tests.ps1 @@ -65,3 +65,17 @@ Describe "Temp: drive" -Tag Feature { $res.Root | Should -BeExactly ([System.IO.Path]::GetTempPath()) } } + +Describe "Get-PSDrive for network path" -Tags "Feature","RequireAdminOnWindows" { + + It 'Check P/Invoke GetDosDevice/QueryDosDevice' -Skip:(-not $IsWindows) { + $UsedDrives = Get-PSDrive | Select-Object -ExpandProperty Name + $PSDriveName = 'D'..'Z' | Where-Object -FilterScript {$_ -notin $UsedDrives} | Get-Random + + subst "$($PSDriveName):" \\localhost\c$\Windows + + $drive = Get-PSDrive $PSDriveName + $drive.DisplayRoot | Should -BeExactly '\\localhost\c$\Windows' + subst "$($PSDriveName):" /D + } +}