Drive created by SUBST (Windows 10) Not Reporting Full Path to QueryDosDevice()

1k views Asked by At

EDIT - LOOKS LIKE I FOUND THE PROBLEM

Sorry. Apparently it was all my fault. See my self-written answer below for details.


I'm working on a test for a SUBST-ed drive that's included as a part of a larger method that tests a string to ensure that it's (at least, possibly) a valid path. After a bit of validation, the "encapsulating" method converts the string to a UNC or absolute path, depending on the path that's passed in, to return elsewhere.

A couple of examples:

  • For drive U:\ mapped to \\SERVERNAME\Share, using the \HKCU\Network\ key in the Windows Registry to find network drive mappings on the local computer, U:\PublicFolder\SomeFile.txt becomes \\SERVERNAME\Share\PublicFolder\SomeFile.txt

  • Alternately, C:\SomeFolder\SomeFile.txt is left unchanged because (as determined within the method) it's an absolute path to a local, physical drive.

So far, most of this appears to be working well and as expected, but I'm encountering an issue with regards to drives created by the SUBST command in Windows 10 (at least - I haven't run any tests under another OS at this time because I don't have another available to me right now).

To be honest, I don't have much experience with the SUBST command and don't use it very often, so, at first, I was having trouble even getting the drive to show up correctly in Windows. After reading through a discussion on the Microsoft Community page (Windows 10 issue "Subst" command doesn't work), I was finally able to get the drive set up "properly" (don't use an elevated command prompt, BTW), but the code I'm using to test for a SUBST-ed drive - converted to VB.NET from this answer - was still not resolving the full path correctly.

Here's the converted code I'm using (I intend to do some "tweaking" later once I have everything working, but this is the current state):

<DllImport("kernel32.dll", SetLastError:=True)>
Private Shared Function QueryDosDevice(ByVal lpDeviceName As String, ByVal lpTargetPath As System.Text.StringBuilder, ByVal ucchMax As Integer) As UInteger
End Function

Private Shared Function IsSubstPath(ByVal pathToTest As String, <Out> ByRef realPath As String) As Boolean
    Dim PathInformation As System.Text.StringBuilder = New System.Text.StringBuilder(260)
    Dim DriveLetter As String = Nothing
    Dim WinApiResult As UInteger = 0

    realPath = Nothing

    Try
        ' Get the drive letter of the path
        DriveLetter = IO.Path.GetPathRoot(pathToTest).Replace("\\", "")
    Catch ex As ArgumentException
        Return False
    End Try

    WinApiResult = QueryDosDevice(DriveLetter, PathInformation, 260)

    If WinApiResult = 0 Then
        Dim LastWinError As Integer = Marshal.GetLastWin32Error()

        Return False
    End If

    ' If the drive is SUBST'ed, the result will be in the format of "\??\C:\realPath\".
    If PathInformation.ToString().StartsWith("\??\") Then
        Dim RealRoot As String = PathInformation.ToString().Remove(0, 4)

        RealRoot += If(PathInformation.ToString().EndsWith("\"), "", "\")
        realPath = IO.Path.Combine(RealRoot, pathToTest.Replace(IO.Path.GetPathRoot(pathToTest), ""))

        Return True
    End If

    realPath = pathToTest
    Return False
End Function

Which I call like this for a drive I created using SUBST H: D:\substtest:

Dim TestFile As New IO.FileInfo("H:\0984\CPI.TXT")
Dim SubstPath As String = String.Empty
Dim FullPath As String = String.Empty

If IsSubstPath(FullPath, SubstPath) Then
    FullPath = SubstPath
End If

My expectation is that the IsSubstPath() method should return D:\substtest\0984\CPI.TXT via the realPath variable. Executing SUBST (without additional parameters) correctly showed the mapping in the command prompt (H:\: => D:\substtest). Checking the TestFile object while debugging shows that it's Exists() property returns True, so the file system does, apparently, know that it's there.

At this point, every time I execute the code, the QueryDosDevice() method call returns a value of 0, although I get varying results from the Marshal.GetLastWin32Error() call as I continue to try to get this working.

My first attempt after getting the SUBST-ed drive "properly" set up on my machine resulted in the Marshal.GetLastWin32Error() returning error code 1008 - ERROR_NO_TOKEN ("An attempt was made to reference a token that does not exist").

Further reading in the linked MS community thread indicated that running SUBST twice - once in a normal command prompt, and again in an elevated command prompt - should make the drive available to either a regular logged on user as well as any elevated user action. I re-ran SUBST in an elevated command prompt and tried again using the same testing code as above. This time, Marshal.GetLastWin32Error() returned error code 6 - ERROR_INVALID_HANDLE ("The handle is invalid.").

Thinking this particular operation might be dependent on the file/path actually existing on the system (as opposed to the .NET IO.FileInfo or IO.DirectoryInfo objects), I manually created the specific subfolder and file to represent what I was testing for in my code (H:\0984\CPI.TXT) and tried it once more (again, using the same code as above):

Once again, the QueryDosDevice() failed to correctly parse the real path (returned 0), but this time the Marshal.GetLastWin32Error() method returned a value of 0 - ERROR_SUCCESS ("The operation completed successfully."). Thinking that, perhaps there was some "flaw" in the code that might unintentionally be skipping a step or something, I checked the PathInformation variable - the Text.StringBuilder object that holds the results of the QueryDosDevice() - in break mode but, alas it's also empty.

NOTE: I also tried using a directory instead of a file, but H:\0984\ resulted in a Marshal.GetLastWin32Error() return value of 0 while H:\0984 resulted in a value of 6. Based on the previous testing, this all makes sense but it nonetheless results in an empty PathInformation variable (failure).

Reading all around the Interwebz, it seems many people are experiencing a variety of issues with SUBST-ed drives under Windows 10, so I'm left wondering at this point if that's the reason for these unexpected results. Has anyone else encountered these issues and, if so, have you been able to resolve them in code?

In case it matters (as I suppose it certainly might), here are some additional details:

  • I'm using Visual Studio 2017 CE and my project is compiling under .NET Framework 4.7.2
  • The physical drive to which I'm creating the SUBST path is a RAID 1 pair of drives formatted with NTFS and has plenty of space (178 GB).

    NOTE: I have also tried creating a SUBST-ed path to my OS drive (C:\) for the testing, but this gets the same results as above.

If I've left anything out, or if you require further clarification, please let me know in the comments and I'll update the question as needed.

1

There are 1 answers

0
G_Hosa_Phat On BEST ANSWER

DOUBLE/TRIPLE/QUADRUPLE CHECK CONVERTED CODE

Looks like it all comes down to a single line of code to which I wasn't paying close enough attention (line 14 in the code block above):

DriveLetter = IO.Path.GetPathRoot(PathToTest).Replace("\\", "")

The problem here is that the Path.GetPathRoot() method returns the drive letter in the format H:\ - there's only one backslash (\), so the .Replace() method didn't find anything to replace and was passing an "invalid" parameter value (H:\ instead of H:). The QueryDosDevice() method will apparently fail if the trailing backslash is there, so I made a quick code edit:

DriveLetter = IO.Path.GetPathRoot(PathToTest).Replace("\", "") ' <--Replace ANY/ALL backslashes

I again tested with my SUBST H: D:\substtest drive with an existing file/directory structure as above. This time, the QueryDosDevice() method returned a value of 19 and correctly parsed the real path as D:\substtest\0984\CPI.TXT.

Then, I deleted the subfolder/file I had created for testing and tried again. Again, it correctly returned the real path as D:\substtest\0984\CPI.TXT. So, apparently it all comes down to me overlooking a "typo" introduced during my conversion of the code from C# to VB.NET. My apologies. The full, corrected version of the converted code

<DllImport("kernel32.dll", SetLastError:=True)>
Private Shared Function QueryDosDevice(ByVal lpDeviceName As String, ByVal lpTargetPath As System.Text.StringBuilder, ByVal ucchMax As Integer) As UInteger
End Function

Private Shared Function IsSubstPath(ByVal pathToTest As String, <Out> ByRef realPath As String) As Boolean
    Dim PathInformation As System.Text.StringBuilder = New System.Text.StringBuilder(260)
    Dim DriveLetter As String = Nothing
    Dim WinApiResult As UInteger = 0

    realPath = Nothing

    Try
        ' Get the drive letter of the path without the trailing backslash
        DriveLetter = IO.Path.GetPathRoot(pathToTest).Replace("\", "")
    Catch ex As ArgumentException
        Return False
    End Try

    WinApiResult = QueryDosDevice(DriveLetter, PathInformation, 260)

    If WinApiResult = 0 Then
        Dim LastWinError As Integer = Marshal.GetLastWin32Error()

        Return False
    End If

    ' If the drive is SUBST'ed, the result will be in the format of "\??\C:\realPath\".
    If PathInformation.ToString().StartsWith("\??\") Then
        Dim RealRoot As String = PathInformation.ToString().Remove(0, 4)

        RealRoot += If(PathInformation.ToString().EndsWith("\"), "", "\")
        realPath = IO.Path.Combine(RealRoot, pathToTest.Replace(IO.Path.GetPathRoot(pathToTest), ""))

        Return True
    End If

    realPath = pathToTest
    Return False
End Function

As I said in the question, I intend to do some "tweaking" of this method, but this does work (now).