Thursday, 2 February 2012

Enumerating and using partitions and volumes in Windows operating system

One of the things I wanted to do for my latest project was to be able to display disks and partition layouts on them to the user. Of course the whole thing must allow the software to later work on those partitions as well.

It turns out that Windows API isn't exactly programmer friendly when it comes to working with physical disks and their partitions. Actually, the API provided is a mess: There are plenty of functions with seemingly the same functionality, but ultimately all the programmer gets with them is plenty of confusion, not to mention finding any concise info on the stuff is one of the hardest things I ever had to go find as a programmer. Luckily for me (and possibly you since you're reading this) I've gotten a bit better with google lately so I was able to find some information that ultimately helped me achieve my goal. So thanks to all the anonymous forum / social / whatever site users that posted various bits of info on the functions.

I should note first that I wasn't using Windows Management Instrumentation (WMI) functions to get here. I think I never got to really explore what those functions do since I was so close to my desired result using plain Windows API all the time. So this solution uses Volume Management functions and subfunctions of the DeviceIoControl function.

The code that utilizes the functions and prints out some partition information follows later, but first I'd like to explain a few things about the various function groups since explanation of the terms is also not quite readily available.

In the Volume Management functions there are actually two groups of functions, each providing access to each own set of data.
The first group is volume enumeration and information group. You have FindFirstVolume, FindNextVolume, FindVolumeClose and GetVolumeInformation functions in this group among others. These functions let you enumerate end obtain information about volumes that Windows OS recognizes. Volumes are logical "drives" that Windows can use for its file system operations. Normally you will see a volume as a drive letter in the windows explorer, but a volume could also be mounted as a folder in another drive or not mounted at all. Note that volumes do not correspond to actual partitions on physical drives. Any partition that windows does not recognize will not be listed in volumes. That's where brute force approach described below helps.
The second group allows you to examine info on volume mount points. You will find functions such as FindFirstVolumeMountPoint, FindNextVolumeMountPoint, FindVolumeMountPointClose and GetVolumePathNamesForVolumeName. These functions let you find out information about drive letters or paths to which a particular volume is mounted. Note that some of these functions require administrative privilege to work. See code below for more info.
Then there are various DeviceIoControl functions which allow one to query the drivers directly about what's available. Fortunately, Windows sorts available storage devices in a nice list that makes the enumeration process a bit easier, though a nice set of Find_X_Drive / Find_X_Partition functions would most certainly help a lot here. Blabbing about the particular functions would help noone here so I suppose you'll just have to get the info on them from code below and with a little searching through MSDN. Let's just say that \\.\PhysicalDrive1\Partition1 is a relatively neat way of accessing each and every partition on your storage devices. See the code below how you can turn this into a bit more information about the particular partition.

The main problem with all these functions is finding the linking point among them. How to get from \\.\PhysicalDrive1\Partition1 (which is nigh but unusable) to \\?\Volume{18e59d60-e102-11e0-a667-806e6f6e6963}\ (which is the way to use the partition and get data on it) - that is what is achieved and described in the attached code.

I must apologize for not cleaning up the code properly, but I hope it should be sufficient to see how one can get the information required. If you still don't get it, feel free to post me a mail of a comment to this article. The license, as usual, is Modified BSD.

#include "windows.h"
#include <stdio.h>
#include <iostream>

using namespace std;

void volumeInfo(WCHAR *volName)
{
  {
    //First some basic volume info
    WCHAR volumeName[MAX_PATH + 1] = { 0 };
    WCHAR fileSystemName[MAX_PATH + 1] = { 0 };
    DWORD serialNumber = 0;
    DWORD maxComponentLen = 0;
    DWORD fileSystemFlags = 0;
    if (GetVolumeInformation(volName, volumeName, ARRAYSIZE(volumeName), &serialNumber, &maxComponentLen, &fileSystemFlags, fileSystemName, ARRAYSIZE(fileSystemName)))
    {
      wprintf(L"Label: [%s]  ", volumeName);
      wprintf(L"SerNo: %lu  ", serialNumber);
      wprintf(L"FS: [%s]\n", fileSystemName);
  //    wprintf(L"Max Component Length: %lu\n", maxComponentLen);
    }
    else
    {
      TCHAR msg[MAX_PATH + 1];
      FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), msg, MAX_PATH, NULL);
      wprintf(L"Last error: %s", msg);
    }
  }
  {
    //The following code finds all folders that are mount points on this volume (empty folder that has another volume mounted-in)
    //This requires administrative privileges so unless you run the app as an admin, the function will simply return nothing
    //It's pretty much useless anyway because the same info can be obtained in the following section where we get mount points for a volume - so reverse lookup is quite possible
    HANDLE mp;
    WCHAR volumeName[MAX_PATH + 1] = { 0 };
    bool success;
    mp = FindFirstVolumeMountPoint(volName, volumeName, MAX_PATH);
    success = mp != INVALID_HANDLE_VALUE;
    if (!success)
    { //This will yield "Access denied" unless we run the app in administrative mode
      TCHAR msg[MAX_PATH + 1];
      FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), msg, MAX_PATH, NULL);
      wprintf(L"Evaluate mount points error: %s", msg);
    }
    while (success)
    {
      wcout << L"Mount point: " << volumeName << endl;
      success = FindNextVolumeMountPoint(mp, volumeName, MAX_PATH) != 0;
    }
    FindVolumeMountPointClose(mp);
  }

  {
    //Now find the mount points for this volume
    DWORD charCount = MAX_PATH;
    WCHAR *mp = NULL, *mps = NULL;
    bool success;

    while (true)
    {
      mps = new WCHAR[charCount];
      success = GetVolumePathNamesForVolumeNameW(volName, mps, charCount, &charCount) != 0;
      if (success || GetLastError() != ERROR_MORE_DATA) 
        break;
      delete [] mps;
      mps = NULL;
    }
    if (success)
    {
      for (mp = mps; mp[0] != '\0'; mp += wcslen(mp))
        wcout << L"Mount point: " << mp << endl;
    }
    delete [] mps;
  }

  {
    //And the type of this volume
    switch (GetDriveType(volName))
    {
    case DRIVE_UNKNOWN:     wcout << "unknown"; break;
    case DRIVE_NO_ROOT_DIR: wcout << "bad drive path"; break;
    case DRIVE_REMOVABLE:   wcout << "removable"; break;
    case DRIVE_FIXED:       wcout << "fixed"; break;
    case DRIVE_REMOTE:      wcout << "remote"; break;
    case DRIVE_CDROM:       wcout << "CD ROM"; break;
    case DRIVE_RAMDISK:     wcout << "RAM disk"; break;
    }
    wcout << endl;
  }
  {
    //This part of code will determine what this volume is composed of. The returned disk extents are actual disk partitions
    HANDLE volH;
    bool success;
    PVOLUME_DISK_EXTENTS vde;
    DWORD bret;

    volName[wcslen(volName) - 1] = '\0';
    volH = CreateFile(volName, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);
    if (volH == INVALID_HANDLE_VALUE)
    {
      TCHAR msg[MAX_PATH + 1];
      FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), msg, MAX_PATH, NULL);
      wprintf(L"Open volume error: %s", msg);
      return;
    }
    bret = sizeof(VOLUME_DISK_EXTENTS) + 256 * sizeof(DISK_EXTENT);
    vde = (PVOLUME_DISK_EXTENTS)malloc(bret);
    success = DeviceIoControl(volH, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, NULL, 0, (void *)vde, bret, &bret, NULL) != 0;
    if (!success)
      return;
    for (unsigned i = 0; i < vde->NumberOfDiskExtents; i++)
      wcout << L"Volume extent: " << vde->Extents[i].DiskNumber << L" "<< vde->Extents[i].StartingOffset.QuadPart << L" - " << vde->Extents[i].ExtentLength.QuadPart << endl;
    free(vde);
    CloseHandle(volH);
  }
}

bool findVolume(WCHAR *volName, int diskno, long long offs, long long len)
{
  HANDLE vol;
  bool success;

  vol = FindFirstVolume(volName, MAX_PATH); //I'm cheating here! I only know volName is MAX_PATH long because I wrote so in enumPartitions findVolume call
  success = vol != INVALID_HANDLE_VALUE;
  while (success)
  {
    //We are now enumerating volumes. In order for this function to work, we need to get partitions that compose this volume
    HANDLE volH;
    PVOLUME_DISK_EXTENTS vde;
    DWORD bret;

    volName[wcslen(volName) - 1] = '\0'; //For this CreateFile, volume must be without trailing backslash
    volH = CreateFile(volName, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);
    volName[wcslen(volName)] = '\\';
    if (volH != INVALID_HANDLE_VALUE)
    {
      bret = sizeof(VOLUME_DISK_EXTENTS) + 256 * sizeof(DISK_EXTENT);
      vde = (PVOLUME_DISK_EXTENTS)malloc(bret);
      if (DeviceIoControl(volH, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, NULL, 0, (void *)vde, bret, &bret, NULL))
      {
        for (unsigned i = 0; i < vde->NumberOfDiskExtents; i++)
          if (vde->Extents[i].DiskNumber == diskno &&
              vde->Extents[i].StartingOffset.QuadPart == offs &&
              vde->Extents[i].ExtentLength.QuadPart == len)
          {
            free(vde);
            CloseHandle(volH);
            FindVolumeClose(vol);
            return true;
          }
      }
      free(vde);
      CloseHandle(volH);
    }

    success = FindNextVolume(vol, volName, MAX_PATH) != 0;
  }
  FindVolumeClose(vol);
  return false;
}

void enumPartitions()
{
  GUID PARTITION_BASIC_DATA_GUID; //This one is actually defined in windows DDK headers, but the linker kills me when I try to use it in a normal (non driver) program
  PARTITION_BASIC_DATA_GUID.Data1 = 0xEBD0A0A2L;
  PARTITION_BASIC_DATA_GUID.Data2 = 0xB9E5;
  PARTITION_BASIC_DATA_GUID.Data3 = 0x4433;
  PARTITION_BASIC_DATA_GUID.Data4[0] = 0x87;
  PARTITION_BASIC_DATA_GUID.Data4[1] = 0xC0;
  PARTITION_BASIC_DATA_GUID.Data4[2] = 0x68;
  PARTITION_BASIC_DATA_GUID.Data4[3] = 0xB6;
  PARTITION_BASIC_DATA_GUID.Data4[4] = 0xB7;
  PARTITION_BASIC_DATA_GUID.Data4[5] = 0x26;
  PARTITION_BASIC_DATA_GUID.Data4[6] = 0x99;
  PARTITION_BASIC_DATA_GUID.Data4[7] = 0xC7;

  //These structures are needed for IOCTL_DISK_GET_DRIVE_LAYOUT_EX below, but we allocate here so that we don't have to worry about freeing until end of function
  PDRIVE_LAYOUT_INFORMATION_EX partitions;
  DWORD partitionsSize = sizeof(DRIVE_LAYOUT_INFORMATION_EX) + 127 * sizeof(PARTITION_INFORMATION_EX);
  partitions = (PDRIVE_LAYOUT_INFORMATION_EX)malloc(partitionsSize);

  for (int i = 0; ; i++)
  { //The main drive loop. This loop enumerates all physical drives windows currently sees
    //Note that empty removable drives are not enumerated with this method
    //Only volume enumeration will allow you to see those
    WCHAR volume[MAX_PATH];
    wsprintf(volume, L"\\\\.\\PhysicalDrive%d", i);

    //We are enumerating registered physical drives here. So the following CreateFile verifies if this disk even exists. If it does not, we have finished our enumeration
    //We also need the handle to get further info on the storage device
    HANDLE h = CreateFile(volume, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);
    bool success = h != INVALID_HANDLE_VALUE;
    if (!success)
      break;

    wcout << endl << endl << endl << L"Disk #" << i << endl;
    //Get information on storage device itself
    DISK_GEOMETRY_EX driveGeom;
    DWORD ior;
    success = DeviceIoControl(h, IOCTL_DISK_GET_DRIVE_GEOMETRY_EX, NULL, 0, &driveGeom, sizeof(driveGeom), &ior, NULL) != 0;
    if (success)
      wcout << L"Size: " << driveGeom.DiskSize.QuadPart << L"   " << driveGeom.DiskSize.QuadPart / 1024 / 1024 / 1024 << L" GB" << endl; //We could take other info from structure, but let's say disk size is enough
    //Get info on partitions
    success = DeviceIoControl(h, IOCTL_DISK_GET_DRIVE_LAYOUT_EX, NULL, 0, partitions, partitionsSize, &ior, NULL) != 0;
    if (success)
    {
      switch (partitions->PartitionStyle)
      {
      case PARTITION_STYLE_MBR: wcout << L"Partition type " << L"MBR" << endl; break;
      case PARTITION_STYLE_GPT: wcout << L"Partition type " << L"GPT" << endl; break;
      default: wcout << L"Partition type " << L"unknown" << endl; break;
      }

      for (int iPart = 0; iPart < int(partitions->PartitionCount); iPart++)
      { //Loop through partition records that we found
        bool partGood = false;
        if (partitions->PartitionEntry[iPart].PartitionStyle == PARTITION_STYLE_MBR && partitions->PartitionEntry[iPart].Mbr.PartitionType != PARTITION_ENTRY_UNUSED && partitions->PartitionEntry[iPart].Mbr.RecognizedPartition)
        { //For MBR partitions, the partition record must not be unused. That way we know it's a valid partition
          wcout << endl << endl << L"Partition " << iPart + 1 << L" offset: " << partitions->PartitionEntry[iPart].StartingOffset.QuadPart << L" length: " << partitions->PartitionEntry[iPart].PartitionLength.QuadPart << L"  " << partitions->PartitionEntry[iPart].PartitionLength.QuadPart / 1024 / 1024 << " MB" << endl;
          partGood = true;
        }
        else if (partitions->PartitionEntry[iPart].PartitionStyle == PARTITION_STYLE_GPT && partitions->PartitionEntry[iPart].Gpt.PartitionType == PARTITION_BASIC_DATA_GUID)
        { //For GPT partitions, partition type must be PARTITION_BASIC_DATA_GUID for it to be usable
          //Quite frankly, windows is doing some shady stuff here: for each GPT disk, windows takes some 128MB partition for its own use. 
          //I have no idea what that partition is used for, but it seems like an utter waste of space. My first disk was 10MB for crist's sake :(
          wcout << endl << endl << L"Partition " << iPart + 1 << L" offset: " << partitions->PartitionEntry[iPart].StartingOffset.QuadPart << L" length: " << partitions->PartitionEntry[iPart].PartitionLength.QuadPart << L"  " << partitions->PartitionEntry[iPart].PartitionLength.QuadPart / 1024 / 1024 << " MB" << endl;
          partGood = true;
        }
        if (partGood == true)
        {
          WCHAR volume[MAX_PATH];
          if (findVolume(volume, i, partitions->PartitionEntry[iPart].StartingOffset.QuadPart, partitions->PartitionEntry[iPart].PartitionLength.QuadPart))
          {
            wcout << endl << volume <<endl;
            volumeInfo(volume);
          }
        }
      }
    }
    CloseHandle(h);
  }
  free(partitions);
}

int main(int argc, char* argv[])
{
  enumPartitions();
  bool success; //Just so that the app waits before terminating
  cin >> success;
  return 0;
}