Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
handle UNC and device paths
  • Loading branch information
adamsitnik committed Jun 21, 2021
commit 48e8e5f405f9087a372a4c728c8ebca5311d8561
2 changes: 2 additions & 0 deletions src/libraries/Common/tests/Common.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@
<Compile Include="$(CommonPath)System\Net\StreamBuffer.cs" Link="Common\System\Net\StreamBuffer.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetsWindows)'=='true'">
<Compile Include="$(CommonPath)Interop\Windows\Interop.UNICODE_STRING.cs"
Link="Common\Interop\Windows\Interop.UNICODE_STRING.cs" />
<Compile Include="$(CoreLibSharedDir)System\IO\PathInternal.Windows.cs"
Link="System\IO\PathInternal.Windows.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Interop.Libraries.cs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Xunit;

namespace Tests.System.IO
Expand Down Expand Up @@ -257,5 +260,45 @@ public void GetRootLengthDevice(string path, int length)
Assert.Equal(length + PathInternal.ExtendedPathPrefix.Length, PathInternal.GetRootLength(@"\\?\" + path));
Assert.Equal(length + PathInternal.ExtendedPathPrefix.Length, PathInternal.GetRootLength(@"\\.\" + path));
}

public static TheoryData<string, string> DosToNtPathTest_Data => new TheoryData<string, string>
{
{ @"C:\tests\file.cs", @"\??\C:\tests\file.cs" }, // typical path
{ @"\\?\C:\tests\file.cs", @"\??\C:\tests\file.cs" }, // NtPath with \\?\ prefix
{ @"\\.\device\file.cs", @"\??\device\file.cs" }, // device path with \\.\ prefix
{ @"\\server\file.cs", @"\??\UNC\server\file.cs" }, // UNC path with \\ prefix
{ @"\\?\UNC\server\file", @"\??\UNC\server\file" }, // extended UNC prefix
{ @"\??\C:\tests\file.cs", @"\??\C:\tests\file.cs" }, // NtPath with \??\ prefix (no changes required)
{ @"C:\", @"\??\C:\" }, // a short path
{ @"\\s", @"\??\UNC\s" }, // short UNC path
{ @"\\s\", @"\??\UNC\s\" }, // short UNC path with trailing
{ $@"C:\{string.Join("\\", Enumerable.Repeat("a", PathInternal.MaxShortPath + 1))}", $@"\??\C:\{string.Join("\\", Enumerable.Repeat("a", PathInternal.MaxShortPath + 1))}"}, // long path
};

[Theory, MemberData(nameof(DosToNtPathTest_Data))]
public void DosToNtPathTest(string path, string expected)
{
// first of all, we use an internal Windows API to ensure that expected value is valid
if (path.Length < PathInternal.MaxShortPath) // RtlDosPathNameToRelativeNtPathName_U_WithStatus does not support long paths
{
RtlDosPathNameToRelativeNtPathName_U_WithStatus(path, out Interop.UNICODE_STRING ntFileName, out IntPtr _, IntPtr.Zero);
try
{
Assert.Equal(expected, Marshal.PtrToStringUni(ntFileName.Buffer));
}
finally
{
Marshal.ZeroFreeGlobalAllocUnicode(ntFileName.Buffer);
}
}

// after that, we test our implementation
var vsb = new ValueStringBuilder(stackalloc char[PathInternal.MaxShortPath]);
PathInternal.DosToNtPath(path, ref vsb);
Assert.Equal(expected, vsb.ToString());

[DllImport(Interop.Libraries.NtDll, CharSet = CharSet.Unicode)]
static extern int RtlDosPathNameToRelativeNtPathName_U_WithStatus(string DosFileName, out Interop.UNICODE_STRING NtFileName, out IntPtr FilePart, IntPtr RelativeName);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,31 +56,12 @@ internal static unsafe SafeFileHandle Open(string fullPath, FileMode mode, FileA

private static IntPtr NtCreateFile(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize)
{
uint ntStatus;
IntPtr fileHandle;
var vsb = new ValueStringBuilder(stackalloc char[PathInternal.MaxShortPath]);

const string MandatoryNtPrefix = @"\??\";
if (fullPath.StartsWith(MandatoryNtPrefix, StringComparison.Ordinal))
{
(ntStatus, fileHandle) = Interop.NtDll.NtCreateFile(fullPath, mode, access, share, options, preallocationSize);
}
else
{
var vsb = new ValueStringBuilder(stackalloc char[256]);
vsb.Append(MandatoryNtPrefix);
PathInternal.DosToNtPath(fullPath, ref vsb);

if (fullPath.StartsWith(@"\\?\", StringComparison.Ordinal)) // NtCreateFile does not support "\\?\" prefix, only "\??\"
{
vsb.Append(fullPath.AsSpan(4));
}
else
{
vsb.Append(fullPath);
}

(ntStatus, fileHandle) = Interop.NtDll.NtCreateFile(vsb.AsSpan(), mode, access, share, options, preallocationSize);
vsb.Dispose();
}
(uint ntStatus, IntPtr fileHandle) = Interop.NtDll.NtCreateFile(vsb.AsSpan(), mode, access, share, options, preallocationSize);
vsb.Dispose();

switch (ntStatus)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ internal static partial class PathInternal
internal const string DirectorySeparatorCharAsString = "\\";

internal const string ExtendedPathPrefix = @"\\?\";
internal const string NtPrefix = @"\??\";
internal const string UncPathPrefix = @"\\";
internal const string UncExtendedPrefixToInsert = @"?\UNC\";
internal const string UncExtendedPathPrefix = @"\\?\UNC\";
Expand Down Expand Up @@ -409,5 +410,34 @@ internal static bool IsEffectivelyEmpty(ReadOnlySpan<char> path)
}
return true;
}

// this method works only for `fullPath` returned by Path.GetFullPath
// currently we don't have interest in supporting relative paths
internal static void DosToNtPath(ReadOnlySpan<char> fullPath, ref ValueStringBuilder vsb)
Copy link
Member

Choose a reason for hiding this comment

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

I assume for the opposite, translate NT to DOS, a helper like this should be needed, right?
For context #54253 (comment) , a Symlinks API uses DeviceIoControl which returns an NT path that I need to translate to DOS.

This is my silly attempt on doing it:
https://github.com/dotnet/runtime/blob/908530a70613af1172138e48d041d6c5d710d866/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs#L513-L518
Also, I think I mixed the names (DOS and NT) in the comments.

Copy link
Member Author

Choose a reason for hiding this comment

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

@jozkee You could use RtlNtPathNameToDosPathName, but the problem is that it's internal and not documented, so we should not be using that.

The mapping that you have pointed to seems to be missing a few translations:

  • \??\UNC\ to \\ (files located on a remote machine)
  • \??\ to \\.\ for devices like names pipes: \\.\pipe\$pipeName

But the question is: are they valid in this context? Can someone create a link to a named pipe or a file located on a network share?

FWIW the best doc about the paths I've found so far: https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html

Choose a reason for hiding this comment

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

Symbolic links to files on network shares are possible; fsutil behavior set symlinkevaluation can enable or disable them. I don't know about symbolic links to named pipes.

Copy link
Contributor

Choose a reason for hiding this comment

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

The mapping that you have pointed to seems to be missing a few translations:

The first example you pointed should still work, as far as I know: Converting \??\UNC\ to \\?\UNC, should be equivalent. The difference is that you do not want to pass a path prefixed with \??\ to the user, and should always translate it to \\?\.

Regarding the second example: Have you seen cases where Windows gives you a path prefixed with \??\ but you were expecting it to start with \\.\? And how would you know this?

{
vsb.Append(NtPrefix);

if (fullPath.Length >= 3 && fullPath[0] == '\\' && fullPath[1] == '\\')
{
// \\.\ (Device) or \\?\ (NtPath)
if (fullPath.Length >= 4 && fullPath[3] == '\\' && (fullPath[2] == '.' || fullPath[2] == '?'))
{
vsb.Append(fullPath.Slice(NtPrefix.Length));
}
else // \\ (UNC)
{
vsb.Append(@"UNC\");
vsb.Append(fullPath.Slice(2));
}
}
else if (fullPath.Length >= 4 && fullPath[0] == '\\' && fullPath[1] == '?' && fullPath[2] == '?' && fullPath[3] == '\\') // \??\
{
vsb.Append(fullPath.Slice(NtPrefix.Length));
}
else
{
vsb.Append(fullPath);
}
}
}
}