Link to home
Start Free TrialLog in
Avatar of pepr
pepr

asked on

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

SOLUTION
Avatar of jkr
jkr
Flag of Germany image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of pepr
pepr

ASKER

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".  
ASKER CERTIFIED SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of pepr

ASKER

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
SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of pepr

ASKER

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.
Avatar of pepr

ASKER

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.
SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of pepr

ASKER

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

Avatar of pepr

ASKER

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

Avatar of pepr

ASKER

Well, ignore the comment at...

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

the code was reorganized.

"R:\b\" makes no sense.  GetVolumePathName("R:\b\c\file.txt") should return "R:\", assuming that R:\ is indeed a mount point.  
Avatar of pepr

ASKER

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>
Avatar of pepr

ASKER

If interested, I have asked the new question focused on unfold3() code -- see http:Q_24149445.html
Avatar of pepr

ASKER

Thanks to both,
    Petr