Replace DllImport with LibraryImport in SMA 2 (#18543)

This commit is contained in:
Ilya
2022-11-22 22:22:14 +05:00
committed by GitHub
parent 0d320ab89a
commit 8bc9fdb2a1
5 changed files with 144 additions and 91 deletions
@@ -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<char> lpDeviceName, Span<char> 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<char> buffer = stackalloc char[StartLength + 1];
Span<char> 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<char> 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<char>.Shared.Rent(buffer.Length * 2);
if (toReturn is not null)
{
ArrayPool<char>.Shared.Return(toReturn);
}
}
}
finally
{
if (rentedArray is not null)
{
ArrayPool<char>.Shared.Return(rentedArray);
}
}
}
}
}
#endif
@@ -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
/// <summary>
/// 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
/// <summary>
/// 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.
/// </summary>
/// <param name="lpDeviceName">
/// The particular MS-DOS device name.
/// </param>
/// <param name="lpTargetPath">
/// The buffer to receive the result of the query.
/// </param>
/// <param name="ucchMax">
/// The maximum number of characters that can be stored into the buffer
/// </param>
/// <returns></returns>
[DllImport(PinvokeDllNames.QueryDosDeviceDllName, CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern int QueryDosDevice(string lpDeviceName, StringBuilder lpTargetPath, int ucchMax);
/// <summary>
/// Creates a symbolic link using the native API.
/// </summary>
@@ -273,9 +273,6 @@
<data name="NoFirstLastWaitForRaw" xml:space="preserve">
<value>The '{0}' and '{1}' parameters cannot be specified in the same command.</value>
</data>
<data name="SubstitutePathTooLong" xml:space="preserve">
<value>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.</value>
</data>
<data name="ItemNotDirectory" xml:space="preserve">
<value>A directory is required for the operation. The item '{0}' is not a directory.</value>
</data>
@@ -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"
@@ -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
}
}