четверг, 15 апреля 2010 г.

SHFileOperation и двойной бэкслеш в MS Vista+

   Код Windows Vista был значительно переработан по сравнению с XP. Я думаю, это хорошо - внутренние ошибки исправляются (а на их место приходят другие - но это уже совсем другая история :)).
   И внешние проявление внутренних переработок могут быть весьма многообразны и совершенно не очевидны.

   Здесь я расскажу об одной особенности функции SHFileOperation, которая проявляется на Висте и выше.


   Рассмотрим такую фукнцию (Delphi):
function FileBackup(const fromDir, toDir, fileName: string; IsMove: Boolean): Integer;
var
  fos: TSHFileOpStruct;
  _fromDir, _toDir: string;
begin
  ForceDirectories(toDir);
  ZeroMemory(@fos, SizeOf(fos));
  if IsMove then
    fos.wFunc := FO_MOVE 
  else
    fos.wFunc := FO_COPY;
  
  fos.fFlags := FOF_NOERRORUI or FOF_NOCONFIRMATION or FOF_NOCONFIRMMKDIR;
  _fromDir := AddBackslash(fromDir);
  _toDir := AddBackslash(toDir);
  
  _fromDir := _fromDir + fileName + #0#0;
  _toDir := _toDir   + fileName + #0#0;
  fos.pFrom := PChar(_fromDir);
  fos.pTo := PChar(_toDir);
  
  Result := SHFileOperation(fos);
end;
   И такой тестовый вызов:
FileBackup('C:\test\folder1\', 'C:\test\folder\\folder2\destination\', 
  'Project21.dll', False);

   Первое, что которая бросается в глаза - это двойной бэкслеш в пути-приемнике. А так вроде бы ничего военного нет. Закроем пока глаза на двойной бэкслеш и запустим тестовое приложение на машине под управлением ОС Windows XP. Код отработал без ошибок и файл был успешно скопирован.

   А вот при выполнении того же самого тестового примера на Windows Vista/7 SHFileOperation возвращает ошибку 183 (0xb7). Согласно MSDN это соответствует коду DE_ERROR_MAX.
DE_ERROR_MAX (0xb7) - MAX_PATH was exceeded during the operation

   GetLastError() возвращает 0. Да и согласно документации GetLastError не надо использовать для этой функции.


   В тоже время, если удалить лишний бэкслеш все работает, как часы - т.е. корень проблемы именно в этом. Стало интересно, каким таким образом два бэкслеша подряд могут вызвать превышение MAX_PATH? Ну что ж, вооружившись Process Monitor-ом, посмотрим, что же происходит на самом деле.

   Выяснилось, что происходит следующее: последовательно перебираются все каталоги в пути и проверяется факт их существования - и если каталога нет, он создается.


   Такое поведение (последовательный перебор каталогов) - следствие ограничения функции CreateDirectory, т.к. она может создавать только последний каталог в пути.


   Так, в случае отсутствия двойного бэкслеша (C:\test\folder\folder2\destination\) система происводит такой перебор:

   Если же двойной бэкслеш есть (C:\test\folder\\folder2\destination\), то перебор будет выглядеть таким образом:

   Как видно, разбор пути обрывается как раз на том месте, где стоит два бэкслеша подряд. Далее следует вызов CreateDirectoryW("C:\test\folder\folder2\destination"). Функция возвращает False и LastError устанавливается равным 183 (0xb7, ERROR_ALREADY_EXISTS), что и понятно, т.к. такой каталог уже существует.

   Похоже происходит следующее: во время перебора директорий встреченный двойной бэкслеш прекращает дальнейший разбор пути-приемника. Система видит, что разбор завершен не до конца и "принудительно" вызывает CreateDirectoryW для конечного пути, которая возвращает ошибку ERROR_ALREADY_EXISTS. После этого осуществляется выход из SHFileOperation функции, которая в качестве результата своей работы возвращает код последней ошибки, т.е. 183 (от последнего вызова CreateDirectoryW).
   А это значение случайно совпало с DE_ERROR_MAX, что и может ввести в заблуждение неискушенного программиста. Ведь реально произошла ошибка "Не смог создать каталог-приемник, т.к. он уже существует", а не "Во время операции был превышен MAX_PATH".

   Таким образом, данный случай - один из примеров того, как возвращенный код ошибки говорит совсем не о том, что произошло в действительности.

Комментариев нет:

Отправить комментарий