[Last Call] Learn how to a build a cloud-first strategyRegister Now

x
  • Status: Solved
  • Priority: Medium
  • Security: Public
  • Views: 2363
  • Last Modified:

How to unfold a Windows path to get a kind of canonical form?

Hi,

Having a path in whatever (but absolute) form, I would like to get a canonical form. The goal is to bind the license to the data on that path and to check, whether the path was not redirected via "subst" or "net use" commands. If the path is related to the directory at the network drive, the result should be UNC form of the path. If the path is related to a local drive, then the result should be the path that uses the non-virtual drive letter (i.e. c:\some\app\root\subdir instead of p:\subdir, for example).

The following text to be commented is rather long as it explains behaviour of the attached fully working example. I still need someone more experienced. There may be better approach to be used. Please, tear the code to pieces. Any objections are welcome.

I have found the following functions to be used: WNetGetConnection(), QueryDosDevice(), and GetDriveType().

The complication is that the "net use" and "subst" commands may be combined together. This is not the desired case; however, you can never be sure what the customer does at his computer.

Let's have the following context. The I: is the network drive mapped to...

=================================================
I:\PRIKRYL>net use i:
Local name        I:
Remote name       \\skil02\demo
Resource type     Disk
Status            OK
...
=================================================


Now the user creates subdirectories and puts the file inside:
=================================================
i:\prikryl\subdir\a\b\c\file.txt
=================================================

He also created subst drive letters like that:
=================================================
I:\PRIKRYL>subst p: i:\prikryl

I:\PRIKRYL>subst q: p:\subdir

I:\PRIKRYL>subst r: q:\a

I:\PRIKRYL>subst
P:\: => I:\prikryl
Q:\: => P:\subdir
R:\: => Q:\a
=================================================

The sample program below is given the path through the R: drive and it displays
=================================================
     path: R:\b\c\file.txt
    drive: R:
driveRoot: R:\
driveType: 4 (DRIVE_REMOTE)

WNetGetConnection("R:", ....): failed (67)
QueryDosDevice("R:", ....): \??\Q:\a

new drive: Q:
 new path: Q:\a\b\c\file.txt

WNetGetConnection("Q:", ....): failed (67)
QueryDosDevice("Q:", ....): \??\P:\subdir

new drive: P:
 new path: P:\subdir\a\b\c\file.txt

WNetGetConnection("P:", ....): failed (67)
QueryDosDevice("P:", ....): \??\I:\prikryl

new drive: I:
 new path: I:\prikryl\subdir\a\b\c\file.txt

WNetGetConnection("I:", ....): succeeded!

The wanted path: \\skil02\demo\prikryl\subdir\a\b\c\file.txt
=================================================

The last one is the wanted form. Now the substed drives are deleted
and the same is done via "net use". It is rather simpler case, because
there cannot be created the chain of substed drives via "net use"...
=================================================
I:\PRIKRYL>net use p: i:\prikryl
System error 67 has occurred.

The network name cannot be found.
=================================================

so only R:
=================================================
I:\PRIKRYL>net use r: \\skil02\demo\prikryl\subdir\a
The command completed successfully.
=================================================

The program returns...
=================================================

     path: R:\b\c\file.txt
    drive: R:
driveRoot: R:\
driveType: 4 (DRIVE_REMOTE)

WNetGetConnection("R:", ....): succeeded!

The wanted path: \\skil02\demo\prikryl\subdir\a\b\c\file.txt
=================================================

After removing all mapping of R: (no such drive)
=================================================

     path: R:\b\c\file.txt
    drive: R:
driveRoot: R:\
driveType: 1 (DRIVE_NO_ROOT_DIR)

QueryDosDevice("R:", ....): failed (2)

The wanted path: R:\b\c\file.txt
=================================================

Of course, it is not correct (simplified program), but it can be detected
even earlier by simple asking for existence of the directory or of the file.

Now, all mapping of the letter-drives are removed, the directories
are moved to the local disk and substed (to mimic the net mapping,
to pretend the situation is the same)...
=================================================
I:\PRIKRYL>subst p: c:\tmp\test

I:\PRIKRYL>subst q: p:\subdir

I:\PRIKRYL>subst r: q:\a

I:\PRIKRYL>subst
P:\: => C:\tmp\test
Q:\: => P:\subdir
R:\: => Q:\a
=================================================

However, canonization routine is here to reveal the attempt. The final
detection is not correct and should be enhanced somehow. The
C:\tmp\test\subdir\a\b\c\file.txt
should be the result. Using the QueryDosDevice() once more leads
to the situation when the result cannot be used as a full path...
=================================================
     path: R:\b\c\file.txt
    drive: R:
driveRoot: R:\
driveType: 3 (DRIVE_FIXED)

QueryDosDevice("R:", ....): \??\Q:\a

new drive: Q:
 new path: Q:\a\b\c\file.txt

QueryDosDevice("Q:", ....): \??\P:\subdir

new drive: P:
 new path: P:\subdir\a\b\c\file.txt

QueryDosDevice("P:", ....): \??\C:\tmp\test

new drive: C:
 new path: C:\tmp\test\subdir\a\b\c\file.txt

QueryDosDevice("C:", ....): \Device\HarddiskVolume3

new drive: ic
 new path: ice\HarddiskVolume3\tmp\test\subdir\a\b\c\file.txt

QueryDosDevice("ic", ....): failed (2)

The wanted path: ice\HarddiskVolume3\tmp\test\subdir\a\b\c\file.txt
=================================================

Do you have some experience with that? Is there any better approach?
Please point me to a documentation that discusses the

\??\C:\tmp\test

form of the path (namely the \??\ prefix and whether there can be another
variations of the prefix...).

Thanks for reading it to this line ;)
   Petr

#include <Windows.h>
#include <Winnetwk.h>  // WNetGetConnection()
#include <iostream>
#include <string>
 
using namespace std;
 
std::string driveTypeStr(UINT driveType);
 
int main()
{
    string drive("R:");
    string driveRoot(drive + "\\");
    string path(drive + "\\b\\c\\file.txt");
    UINT driveType = ::GetDriveType(driveRoot.c_str());
 
    cout << "     path: " << path << "\n"
         << "    drive: " << drive << "\n"
         << "driveRoot: " << driveRoot << "\n"
         << "driveType: " << driveType << " (" 
                          << driveTypeStr(driveType) << ")" << "\n"
         << "\n";
 
    char buf[1000];
    DWORD size = sizeof(buf);
    DWORD result = 0;
    string lastDrive;
 
    while (true)   
    {
        if (driveType == DRIVE_REMOTE)
        {
            cout << "WNetGetConnection(\"" << drive << "\", ....): ";
            result = WNetGetConnection(drive.c_str(), buf, &size);
            if (result != NO_ERROR)
                cout << "failed (" << result << ")\n";
            else
            {   
                cout << "succeeded!\n\n";
                string path2(buf);
                path = path2 + path.substr(2);
                break;  // the path was found
            }
        }
 
        // Here we get only when the WNetGetConnection failed.
        //
        cout << "QueryDosDevice(\"" << drive << "\", ....): ";   
        result = QueryDosDevice(drive.c_str(), buf, size);  
                                  // returns 0 or number of chars
        string path2(buf);
        if (result == 0)
        {
            cout << "failed (" << GetLastError() << ")\n\n";
            break;  // when this can fail?
        }
        else
        {
            cout << path2 << "\n";  // returned by QueryDosDevice
            drive = path2.substr(4, 2);
            if (lastDrive == drive)
                break;  // the path was found in the previous loop
            else
            {
                lastDrive = drive;
                cout << "\nnew drive: " << drive << "\n";
                path = path2.substr(4) + path.substr(2);
                cout << " new path: " << path << "\n\n";
            }
        }
 
    }
    cout << "The wanted path: " << path << "\n\n";
 
    return 0;
}
 
 
std::string driveTypeStr(UINT driveType)
{
    #define CASE(x) case x: return #x;
    switch (driveType)
    {
      CASE(DRIVE_UNKNOWN)
      CASE(DRIVE_NO_ROOT_DIR)
      CASE(DRIVE_REMOVABLE)
      CASE(DRIVE_FIXED)
      CASE(DRIVE_REMOTE)
      CASE(DRIVE_CDROM)
      CASE(DRIVE_RAMDISK)
    }
    return "unknown type";
}

Open in new window

0
pepr
Asked:
pepr
  • 10
  • 4
4 Solutions
 
jkrCommented:
Have you tried 'PathCanonicalize()' (http://msdn.microsoft.com/en-us/library/bb773569.aspx)?
0
 
peprAuthor Commented:
Well, I did not know the PathCanonalize(). However, it seems to solve another problem. I do use a different implementation of what the PathCanonalize() probably is. I guess the PathCanonalize() is based on parsing of the path string and polishing it.

I assume that the PathCanonalize() does not change the drive letter which is the main goal... if the drive was created using "subst" or "net use".  
0
 
Gideon7Commented:
Windows has many more methods of indirection beyond SUBST.  There are reparse points, symbolic links, and hard links to deal with.  NTFS volumes dont have to be mounted on a drive letter; they can be mounted at any folder level.
To catch redirection (reparse points, non-letter mount points, etc), you can use GetVolumeNameForVolumeMountPoint on the full path of your file to get the 'true' name of the volume (\\?\Volume{GUID}) where it lives.  Next, call GetVolumePathNamesForVolumeName to get a list of all active mount points (the same NTFS volume can be mounted in more than one place).  Choose a canonical policy, such as picking the shortest mount point from the returned list, with alphabetic sort-order being the tie breaker.  It doesn't matter what policy you use as long as it is consistent.
Canonicalizing network paths is generally not possible.  A server can export the same local folder under two different UNC paths. Or it can nest them.  A server can appear under multiple names (\\foo.acme.com\share vs \\foo\share), including DNS CNAME aliases (\\baz\share).  DFS complicates it futher (\\acme.com\share) as multiple servers publish the same share name with a first-response-wins algorithm.  Checking the IP may work, but servers often are multi-homed.
The only way to get a definitive answer is to somehow make the server run your code to cycle through the GetVolumeXxx calls as described above, and then return the result back to the client.  Uniqueness can be verified to a high probability using the unique 64-bit volume index number of the file in the MFT.
0
Industry Leaders: We Want Your Opinion!

We value your feedback.

Take our survey and automatically be enter to win anyone of the following:
Yeti Cooler, Amazon eGift Card, and Movie eGift Card!

 
peprAuthor Commented:
Gideon7: I guess you understand my goal. I want to generate a kind of certificate stored with the date. The certificate should capture the path to itself somehow. The application should be able to check whether the certificate (the file with fixed name) really captures the path from where it was read. The applicaiton will us the path to the certificate to reconstruct the unique information about its location and it will be compared with the information stored in the certificate. Until now, I am able to get SID prefix for the computer. I want to enhance it by getting similar information about that exact directory. The form does not matter very much, only it must be possible to reconstruct it.

In other words, I want to detect whether the directory is the original directory where the data is officially stored (in files). The reason is that I do not have a server yet (in the sense of client-server) that would control the access to the data. Because of that I need the application to check the data location. To simplify it further, I want the application to detect whether the data was stealed.

The information you gave me seems promissing for solving the problem. I will try next week.

Thanks a lot, so far,

         Petr
0
 
Gideon7Commented:
If all you care about is detecting if the file was copied, you can query for the 64-bit file index number of the file.  This number is unique to a high probability.  Open the file and use GetFileInformationByHandle (http://msdn.microsoft.com/en-us/library/aa364952.aspx).  It works anywhere, including a UNC path.
If the file is copied the index number will change.
You will need to think about how you want to handle backup/restore, as that will change the index number also.
0
 
peprAuthor Commented:
Thanks for another information that I did not know. Anyway, the file index number seems to be too volatile for the purpose. I will close the question probably in Monday.
0
 
peprAuthor Commented:
Gideon7: I tried to "use GetVolumeNameForVolumeMountPoint on the full path of your file". I tried to call it for
I:\PRIKRYL\subdir\a\b\c\file.txt where I: is mapped to \\skil02\demo. It failed with error 123 ERROR_INVALID_NAME ("The filename, directory name, or volume label syntax is incorrect.")

When calling it for I:\, it fails with 87 ERROR_INVALID_PARAMETER

For C:\ it works fine and returns \\?\Volume{2c5955cb-1bc9-11dc-8a07-806e6f6e6963}\

For C:\tmp\test\subdir\a\b\c\file.txt it fails (123)

If P: is substed via  subst p: C:\tmp\test, it fails (87).

This way it seems that it works only for letter-paths that represent the root of the "physical" drive. It does not work for the substed drive letters. Any other idea how to "unsubst"?

It is not a problem for me to unsubst it via the QueryDosDevice() as shown above. I only need to understand how to do that reliably and correctly. For example I would like to stop the loop when getting \??\C:\somepath, but I am not sure how to detect the case. I have found nothing about \??\ prefix of the path.
0
 
Gideon7Commented:
Call GetVolumePathName("I:\PRIKRY\subdir\a\b\c\file.txt") to get the mount point ("I:\"), then call GetVolumeNameForVolumeMountPoint("I:\") to get \\?\Volume{GUID}\.
0
 
peprAuthor Commented:
Well, when I: is \\skil02\demo and the next drives were created using subst...

D:\...>subst
P:\: => I:\prikryl
Q:\: => P:\subdir
R:\: => Q:\a

then the unfold2() code -- see below -- fails.

Unfold 2
========
     path: R:\b\c\file.txt
    drive: R:
driveRoot: R:\
GetVolumePathName("R:\b\c\file.txt", ....): R:\
GetVolumeNameForVolumeMountPoint("R:\", ....): failed (87)

The reason probably is that R: was created using subst.
void unfold2()
{
    string drive("R:");
    string driveRoot(drive + "\\");
    string path(drive + "\\b\\c\\file.txt");
    UINT driveType = ::GetDriveType(driveRoot.c_str());
 
    cout << "Unfold 2\n"
            "========\n"
            "     path: " << path << "\n"
         << "    drive: " << drive << "\n"
         << "driveRoot: " << driveRoot << "\n";
 
    char buf[1000];
    DWORD size = sizeof(buf);
    BOOL resultOK = GetVolumePathName(path.c_str(), buf, size);
 
    string path2;
    cout << "GetVolumePathName(\"" << path 
         << "\", ....): ";
    if (resultOK)
    {
        path2 = buf;
        cout << path2 << "\n";
    }
    else
        cout << "failed (" << GetLastError() << ")\n";
 
    if ( ! path2.empty())
    {
        resultOK = GetVolumeNameForVolumeMountPoint(path2.c_str(), buf, size);
        cout << "GetVolumeNameForVolumeMountPoint(\"" << path2 
             << "\", ....): ";
        if (resultOK)
        {
            path2 = buf;
            cout << path2 << "\n";
        }
        else
            cout << "failed (" << GetLastError() << ")\n";
        }
}

Open in new window

0
 
peprAuthor Commented:
If I use the same code as above (unfold2) after deleting subst P: to the remote drive and substing the local directory...

P:\: => C:\tmp\test
Q:\: => P:\subdir
R:\: => Q:\a

... then I get

Unfold 2
========
     path: R:\b\c\file.txt
    drive: R:
driveRoot: R:\
GetVolumePathName("R:\b\c\file.txt", ....): R:\b\
GetVolumeNameForVolumeMountPoint("R:\b\", ....): failed (4390)

ERROR_NOT_A_REPARSE_POINT
4390


So far, the only way that works for me is shown below. It works both for network drives and for local drives (substed or not). I only want to make sure whether I did not overlooked some error.

I probably do not want to get the volume name in the form \\?\Volume{GUID}\. I prefer a kind of directly usable full path with any mapping removed. I understand that there may be no unique or prefered path like this if it is not located on local disk.


void unfold3()
{
    string drive("R:");
    string driveRoot(drive + "\\");
    string path(drive + "\\b\\c\\file.txt");
    UINT driveType = ::GetDriveType(driveRoot.c_str());
 
    cout << "Unfold 3\n"
            "========\n"
            "     path: " << path << "\n"
         << "    drive: " << drive << "\n"
         << "driveRoot: " << driveRoot << "\n"
         << "driveType: " << driveType << " (" 
                          << driveTypeStr(driveType) << ")" << "\n"
         << "\n";
 
    char buf[1000];
    DWORD size = sizeof(buf);
    DWORD result = 0;
 
    while (true)   
    {
        // Here we get only when the WNetGetConnection failed.
        //
        cout << "QueryDosDevice(\"" << drive << "\", ....): ";   
        result = QueryDosDevice(drive.c_str(), buf, size);  
                                  // returns 0 or number of chars
        string path2(buf);
        if (result == 0)
        {
            cout << "failed (" << GetLastError() << ")\n\n";
            break;  // when this can fail?
        }
        else
        {
            cout << path2 << "\n";  // returned by QueryDosDevice
            if (path2.find("\\??\\") == string::npos)
                break;              // it went too far. Stick with previous 
            
            // Get the path2 without the \??\ prefix and replace the 
            // previous drive (R:) letter by the substitution path.
            //
            path = path2.substr(4) + path.substr(2);
            drive = path.substr(0, 2);
            cout << "\nnew drive: " << drive << "\n";
            cout << " new path: " << path << "\n\n";
        }
    }
 
    if (driveType == DRIVE_REMOTE)
    {
        cout << "WNetGetConnection(\"" << drive << "\", ....): ";
        result = WNetGetConnection(drive.c_str(), buf, &size);
        if (result != NO_ERROR)
            cout << "failed (" << result << ")\n";
        else
        {   
            cout << "succeeded!\n\n";
            string path2(buf);
            path = path2 + path.substr(2);
        }
    }
 
    cout << "The wanted path: " << path << "\n\n";
}

Open in new window

0
 
peprAuthor Commented:
Well, ignore the comment at...

    while (true)  
    {
        // Here we get only when the WNetGetConnection failed.
        //

the code was reorganized.

0
 
Gideon7Commented:
"R:\b\" makes no sense.  GetVolumePathName("R:\b\c\file.txt") should return "R:\", assuming that R:\ is indeed a mount point.  
0
 
peprAuthor Commented:
I agree with you if the subst command creates mount points. The subst result looks more like symbolic links to me. Anyway, the GetVolumePathName() did not flagged any error.

The output text was copy/pasted from the console window. It is exactly what the code returned when the  mentioned substitution was used. The R:\b was the existing directory (substed) in the time. See the captured steps from console. All the substitutions were deleted and redone -- still the same. See also that the drives are not mapped via net use. Yes, I am confused ;)

P.S. Windows Vista Enterprise, SP1


D:\Tutorial\PathUnfold\Debug>R:

R:\>cd b

R:\b>subst
P:\: => C:\tmp\test
Q:\: => P:\subdir
R:\: => Q:\a

R:\b>d:

D:\Tutorial\PathUnfold\Debug>subst p: /d

D:\Tutorial\PathUnfold\Debug>subst q: /d

D:\Tutorial\PathUnfold\Debug>subst r: /d

D:\Tutorial\PathUnfold\Debug>subst

D:\Tutorial\PathUnfold\Debug>subst p: c:\tmp\test

D:\Tutorial\PathUnfold\Debug>subst q: p:\subdir

D:\Tutorial\PathUnfold\Debug>subst r: q:\a

D:\Tutorial\PathUnfold\Debug>subst
P:\: => C:\tmp\test
Q:\: => P:\subdir
R:\: => Q:\a

D:\Tutorial\PathUnfold\Debug>net use
New connections will not be remembered.


Status       Local     Remote                    Network

-------------------------------------------------------------------------------
Disconnected H:        \\skil02\skil_fp          Microsoft Windows Network
Disconnected I:        \\skil02\demo             Microsoft Windows Network
Disconnected J:        \\skil02\skup01           Microsoft Windows Network
Disconnected K:        \\skil02\skup02           Microsoft Windows Network
Disconnected L:        \\skil02\ladeni           Microsoft Windows Network
Disconnected M:        \\skil02\ostry            Microsoft Windows Network
Disconnected N:        \\skil02\archiv           Microsoft Windows Network
Disconnected O:        \\skil02\sap              Microsoft Windows Network
The command completed successfully.


D:\Tutorial\PathUnfold\Debug>PathUnfold.exe
Unfold 2
========
     path: R:\b\c\file.txt
    drive: R:
driveRoot: R:\
GetVolumePathName("R:\b\c\file.txt", ....): R:\b\
GetVolumeNameForVolumeMountPoint("R:\b\", ....): failed (4390)

D:\Tutorial\PathUnfold\Debug>
0
 
peprAuthor Commented:
If interested, I have asked the new question focused on unfold3() code -- see http:Q_24149445.html
0
 
peprAuthor Commented:
Thanks to both,
    Petr
0

Featured Post

Industry Leaders: We Want Your Opinion!

We value your feedback.

Take our survey and automatically be enter to win anyone of the following:
Yeti Cooler, Amazon eGift Card, and Movie eGift Card!

  • 10
  • 4
Tackle projects and never again get stuck behind a technical roadblock.
Join Now