hex.pp.ua

Создание символьной ссылки Windows на C/C++

Как создать символьную ссылку программным путём




Здесь описывается способ создания символьных ссылок Windows без использования стандартных функций создания ссылок WinAPI. Вместо стандартной функции CreateSymbolicLink будем напрямую формировать буфер reparse-данных и прикреплять его к файлу или каталогу. Это позволит нам лучше разобраться в том, как устроены внутри символьные ссылки Windows. О создании ссылок с помощью CreateSymbolicLink читайте в этой статье.

Символьные ссылки реализованы через технологию reparse points. С помощью этой технологии можно создавать символьные ссылки, символьные связи (junctions), точки монтирования и другие типы точек повторной обработки.

Задача стоит следующая: нужно правильно сформировать структуру REPARSE_DATA_BUFFER и вызвать функцию DeviceIoControl с запросом FSCTL_SET_REPARSE_POINT, что и позволит нам создать символьную ссылку или иной вид точки повторной обработки.

Определение структуры REPARSE_DATA_BUFFER и её максимального размера:

typedef struct _REPARSE_DATA_BUFFER
{
  ULONG ReparseTag;
  USHORT ReparseDataLength;
  USHORT Reserved;
  union
  {
    struct
    {
      USHORT SubstituteNameOffset;
      USHORT SubstituteNameLength;
      USHORT PrintNameOffset;
      USHORT PrintNameLength;
      ULONG Flags;
      WCHAR PathBuffer[1];
    }
    SymbolicLinkReparseBuffer;
    struct
    {
      USHORT SubstituteNameOffset;
      USHORT SubstituteNameLength;
      USHORT PrintNameOffset;
      USHORT PrintNameLength;
      WCHAR PathBuffer[1];
    }
    MountPointReparseBuffer;
    struct
    {
      UCHAR  DataBuffer[1];
    }
    GenericReparseBuffer;
  };
}
REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
 
#define REPARSE_DATA_BUFFER_HEADER_SIZE \
  FIELD_OFFSET(REPARSE_DATA_BUFFER, GenericReparseBuffer)

Получение привилегий

Но для начала нужно получить привилегии, чтобы можно было пользоваться DeviceIoControl. Следует получить привилегии SE_BACKUP_NAME, SE_RESTORE_NAME и на всякий случай, SE_CREATE_SYMBOLIC_LINK_NAME.

Привилегии получаются с помощью следующей функции:

void GetPrivilege(LPCWSTR priv)
{
  HANDLE hToken;
  TOKEN_PRIVILEGES tp;
  OpenProcessToken(GetCurrentProcess(), 
    TOKEN_ADJUST_PRIVILEGES, &hToken;);
  LookupPrivilegeValue(NULL, priv, &tp.Privileges;[0].Luid);
  tp.PrivilegeCount = 1;
  tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
  AdjustTokenPrivileges(hToken, FALSE, &tp;, 
    sizeof(TOKEN_PRIVILEGES), NULL, NULL);
  CloseHandle(hToken);
}

Объявление функции

Итак, получили все необходимые привилегии, что дальше? Дальше делаем функцию создания символической ссылки, которая будет иметь такие параметры:

BOOL CreateLink(
  LPCWSTR link_name, // Путь ссылки
  LPCWSTR target_name, // Путь того, на что ссылаемся
  BOOL is_relative, // Признак относительной ссылки
  BOOL is_dir, // Признак ссылки на каталог
  // Признак создания символьной связи (junction)
  BOOL is_junction 
);

Определяем два макроса, чтобы в дальнейшем вызовы функции выглядели более понятно:

#define CreateSymlink(x,y,a,b) CreateLink(x, y, a, b, FALSE)
#define CreateJunction(x,y) CreateLink(x, y, FALSE, TRUE, TRUE)

Программирование функции

Объявляем структуру REPARSE_DATA_BUFFER и выделяем под неё память.

// Буфер reparse
char reparse_buf[MAX_PATH * 3];
PREPARSE_DATA_BUFFER rd 
= (PREPARSE_DATA_BUFFER) reparse_buf;

Получаем полный путь новой ссылки с помощью функции GetFullPathName.

WCHAR link_name_full[MAX_PATH];
LPWSTR file_part;
 
if (!GetFullPathName(link_name, MAX_PATH, 
  link_name_full, &file;_part)) 
{
  return FALSE;
} 

Теперь нужно получить полный путь на то, на что мы ссылаемся. Но если создаём относительную ссылку, то полный путь не нужен, просто копируем то имя файла или каталога, которое передано функции в параметре target_name. Полный путь получаем опять с помощью функции GetFullPathName.

// Полный путь к файлу с префиксом (если не относительный)
WCHAR target_name_prefixed[MAX_PATH]; 
// Полный путь к файлу. 
WCHAR target_name_full[MAX_PATH]; 
 
if (is_relative)
{
  wcscpy(target_name_full, target_name);
} 
  else
{
  if (!GetFullPathName(target_name, MAX_PATH, 
        target_name_full, &file;_part)) 
  {
    return FALSE;
  }
}

Теперь формируем путь к файлу в NT-формате, если это полный путь. Это делается путём добавления префикса \??\ к полному пути. Если путь относительный, то префикс не нужен.

if (is_relative)
{
  wcscpy(target_name_prefixed, target_name_full);
} 
  else
{
  wsprintf(target_name_prefixed, 
  L"\\??\\%s", target_name_full);    
} 

Заполнение буфера

Дальше нужно заполнить поля структуры REPARSE_DATA_BUFFER и правильно задать тип ссылки в поле ReparseTag. Символьные связи и точки монтирования томов имеют один и тот же тип - IO_REPARSE_TAG_MOUNT_POINT. Символьные ссылки имеют другой тип - IO_REPARSE_TAG_SYMLINK. Для них тип одинаков и в случае, когда мы ссылаемся на файл, и в случае, когда на каталог. Отличие только в том, что если ссылаемся на каталог, reparse-данные надо присоединять к каталогу, если ссылаемся на файл, то к файлу.

Для помещение в структуру пути к ссылке и пути к целевому файлу нужно использовать функцию FillREPARSE_DATA_BUFFER, описание которой дано ниже. Эта функция помещает в структуру два вида имени целевого файла - одно имя называется отображаемым, оно служит для отображения на экране и не содержит префикса \??\. Второе имя - полное имя, на которое ссылаемся, в формате NT (то есть с префиксом).

Если создаём относительную символьную ссылку, следует заполнить поле SymbolicLinkReparseBuffer.Flags значением SYMLINK_FLAG_RELATIVE.

rd->ReparseTag = is_junction 
  ? IO_REPARSE_TAG_MOUNT_POINT 
  : IO_REPARSE_TAG_SYMLINK;
 
FillREPARSE_DATA_BUFFER(rd, 
  target_name_full, wcslen(target_name_full),
  target_name_prefixed, wcslen(target_name_prefixed)
  );
 
if (!is_junction && is_relative)
{
  rd->SymbolicLinkReparseBuffer.Flags = SYMLINK_FLAG_RELATIVE;
}

Создание файла-ссылки

Дальше создаём файл или каталог, который и станет символьной ссылкой или точкой монтирования. Напомню, что создание символьных связей (junctions) возможно только для каталогов.

HANDLE hHandle;
DWORD dwLen;
 
if (is_junction || is_dir)
{
  if (!CreateDirectory(link_name_full, NULL))
  {
    return FALSE;
  }
} 
  else
{
  HANDLE hFile = CreateFile(link_name_full, 
      GENERIC_READ | GENERIC_WRITE, 
      0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
  if (hFile == INVALID_HANDLE_VALUE)
  {
    return FALSE;
  }
  CloseHandle(hFile);
} 

Созданный файл или каталог необходимо переоткрыть с флагами FILE_FLAG_OPEN_REPARSE_POINT и FILE_FLAG_BACKUP_SEMANTICS. Именно для того, чтобы можно было таким образом открыть файл, в самом начале программа получала привилегии. Для переоткрытия используем такую функцию:

HANDLE OpenLinkHandle(LPCTSTR pszPath) {  
  return CreateFile(pszPath, GENERIC_READ | GENERIC_WRITE,
    0, NULL, OPEN_EXISTING,
    FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, NULL);
}

Запись reparse данных

Последний этап создания ссылки - прикрепление данных reparse к файлу или каталогу. Для этого вызывается функция DeviceIoControl с запросом FSCTL_SET_REPARSE_POINT, этой функции передаётся reparse-буфер и его длина, которая вычисляется из значения поля rd->ReparseDataLength и константы REPARSE_DATA_BUFFER_HEADER_SIZE, означающей длину заголовка reparse-буфера. Если попытка создания ссылки не увенчалась успехом, следует удалить файл или каталог, к которому мы пытались прикрепить reparse-данные.

hHandle = OpenLinkHandle(link_name_full);
 
if (INVALID_HANDLE_VALUE == hHandle)
{
  return FALSE;    
}
 
if (!DeviceIoControl( hHandle, FSCTL_SET_REPARSE_POINT,
      rd, rd->ReparseDataLength + REPARSE_DATA_BUFFER_HEADER_SIZE,
      NULL, 0, &dwLen;, NULL ))
{  
  CloseHandle(hHandle);
  if (is_junction || is_dir)
  {
    RemoveDirectory(link_name_full);
  } else
  {
    DeleteFile(link_name_full);
  }
  return FALSE;
}  
 
CloseHandle(hHandle);
return TRUE;

Подробнее о reparse данных

Так как же всё-таки происходит формирование reparse-структуры? Для этого используется функция FillREPARSE_DATA_BUFFER, которая имеет такой прототип:

BOOL FillREPARSE_DATA_BUFFER(
  PREPARSE_DATA_BUFFER rdb, // буфер reparse-данных
  LPCWSTR PrintName, // отображаемое имя
  size_t PrintNameLength, // длина отображаемого имени
  LPCWSTR SubstituteName, // имя для подстановки
  size_t SubstituteNameLength // длина имени для подстановки
  );

Отображаемое имя показывается пользователю, например файловым менеджером, если пользователь хочет узнать, куда ведёт ссылка. Имя для подстановки это имя, которое передаётся драйверу файловой системы. Именно это имя указывает на реальное расположение файла в файловой системе.

В самой функции очищается зарезервированное поле Reserved, проверяется тип ReparseTag (symlink это или junction).

Отличие junction от symlink в том, что PrintName и SubstituteName в буфере reparse_buf расположены в другом порядке, а длины строк задаются с учётом того, что строки содержат конечные символы 0x00. В случае с symlink эти нули не включаются в длину строк и не пишутся в буфер.

Интересно, что если сравнить MSDN-описания структур данных (Symlink, Junction), то они описаны совершенно одинаково, за исключением наличия поля Flags в структуре данных symlink. В обоих случаях написано, что порядок PrintName и SubstituteName в буфере не важен, и что нули в конце строк не должны учитываться. Однако, если следовать этой рекомендации и заполнять буфер junction точно также, как symlink, создать связь не удастся - DeviceIoControl выдаёт ошибку "Данные, находящиеся в буфере точки повторной обработки явлются неверными".

В ReparseDataLength вычисляется общая длина PathBuffer'а сложением длин двух строк со смещением поля PathBuffer в структуре и вычитанием из этого значения размера заголовка reparse-данных. Проверяется, чтобы буфер не превысил максимального размера reparse-буфера, определённого константой MAXIMUM_REPARSE_DATA_BUFFER_SIZE. После этого в PathBuffer копируются строки.

Функция выглядит так:

BOOL FillREPARSE_DATA_BUFFER(
  PREPARSE_DATA_BUFFER rdb,
  LPCWSTR PrintName, 
  size_t PrintNameLength, 
  LPCWSTR SubstituteName, 
  size_t SubstituteNameLength)
{
  BOOL Result=FALSE;
 
  rdb->Reserved=0;
 
  switch (rdb->ReparseTag)
  {
    case IO_REPARSE_TAG_SYMLINK:
      // Указываются смещения строк имени 
      // ссылки в PathBuffer и длины этих строк
 
      rdb->SymbolicLinkReparseBuffer.PrintNameOffset = 0;
      rdb->SymbolicLinkReparseBuffer.PrintNameLength 
        = (USHORT)(PrintNameLength * sizeof(wchar_t));
      rdb->SymbolicLinkReparseBuffer.SubstituteNameOffset 
        = rdb->SymbolicLinkReparseBuffer.PrintNameLength;
      rdb->SymbolicLinkReparseBuffer.SubstituteNameLength 
        = (USHORT)(SubstituteNameLength * sizeof(wchar_t));
 
      rdb->ReparseDataLength = FIELD_OFFSET(REPARSE_DATA_BUFFER, 
        SymbolicLinkReparseBuffer.PathBuffer)
        + rdb->SymbolicLinkReparseBuffer.SubstituteNameOffset
        + rdb->SymbolicLinkReparseBuffer.SubstituteNameLength 
        - REPARSE_DATA_BUFFER_HEADER_SIZE;
 
      if (rdb->ReparseDataLength + REPARSE_DATA_BUFFER_HEADER_SIZE
        <= (USHORT)(MAXIMUM_REPARSE_DATA_BUFFER_SIZE
         / sizeof(wchar_t)))
      {
        // В PathBuffer копируются строки.
        wmemcpy(
        &rdb-;>SymbolicLinkReparseBuffer.PathBuffer[
        rdb->SymbolicLinkReparseBuffer.SubstituteNameOffset
         / sizeof(wchar_t)], 
        SubstituteName, SubstituteNameLength
        );
        wmemcpy(
        &rdb-;>SymbolicLinkReparseBuffer.PathBuffer[
        rdb->SymbolicLinkReparseBuffer.PrintNameOffset
         / sizeof(wchar_t)], 
        PrintName, PrintNameLength
        );
        Result = TRUE;
      }
    break;
 
 
    case IO_REPARSE_TAG_MOUNT_POINT:
      rdb->MountPointReparseBuffer.SubstituteNameOffset = 0;
      rdb->MountPointReparseBuffer.SubstituteNameLength 
        = (USHORT)(SubstituteNameLength * sizeof(wchar_t));
      rdb->MountPointReparseBuffer.PrintNameOffset 
        = rdb->MountPointReparseBuffer.SubstituteNameLength + 2;
      rdb->MountPointReparseBuffer.PrintNameLength 
        = (USHORT)(PrintNameLength * sizeof(wchar_t));
 
      rdb->ReparseDataLength = FIELD_OFFSET(REPARSE_DATA_BUFFER, 
        MountPointReparseBuffer.PathBuffer)
        + rdb->MountPointReparseBuffer.PrintNameOffset
        + rdb->MountPointReparseBuffer.PrintNameLength
        + 1 * sizeof(wchar_t) - REPARSE_DATA_BUFFER_HEADER_SIZE;
 
      if (rdb->ReparseDataLength+REPARSE_DATA_BUFFER_HEADER_SIZE
        <= (USHORT)(MAXIMUM_REPARSE_DATA_BUFFER_SIZE
         / sizeof(wchar_t)))
      {
        wmemcpy(&rdb-;>MountPointReparseBuffer.PathBuffer[
        rdb->MountPointReparseBuffer.SubstituteNameOffset
         / sizeof(wchar_t)],
        SubstituteName, SubstituteNameLength + 1);
        wmemcpy(&rdb-;>MountPointReparseBuffer.PathBuffer[
        rdb->MountPointReparseBuffer.PrintNameOffset
         / sizeof(wchar_t)],
        PrintName, PrintNameLength + 1);
        Result = TRUE;
      }
 
    break;
  }
 
  return Result;
}

С помощью этого можно создавать стандартные виды ссылок Windows. О том, как создавать и записывать свои собственные виды reparse-данных будет рассказано в отдельной статье.

Скачать код, приведённый в статье в виде проекта для Visual Studio 2010: архив RAR, 15 Кб.

Ссылки

По теме точек повторной обработки также есть следующее:



Автор: амдф
Дата: 03.02.2011


При копировании материалов хорошим тоном будет указание авторства и ссылка на сайт. По поводу рекламы обращайтесь на почту [email protected]