Dmitry
Dmitry

Reputation: 14622

How to find out if a directory exists on Delphi XE2 *correctly*?

I simply need to check if a directory exists! But if the directory is "E:\Test" where E: is CD/DVD drive and there is no disk inserted on it, I see the following Delphi and Windows issues.

The first method:

function DirExists(Name: string): Boolean;
var
  Code: Integer;
begin
  Code := GetFileAttributesW(PChar(Name));
  Result := (Code <> -1) and (FILE_ATTRIBUTE_DIRECTORY and Code <> 0);
end;

It gives Range Check Error. I can't use {$RANGECHECKS OFF}, {$RANGECHECKS ON} blocks because:

  1. It breaks the current state of $RANGECHECKS option.
  2. We will see another system error Drive is not ready instead Range Check Error. But I simply need to check if directory exists without any error dialogs for user.

The second method:

if DirectoryExists(Name, True) then ...

This function returns True for non-existing E:\Test directory on empty CD/DVD drive. So can't use it because it works incorrectly.

But then, how to find out if a directory exists?

P.S. I think the error exists with any CD/DVD drive. But I am using Windows 7 x64 on VMWare Fusion 5 under Mac OS X 10.8.4 with external CD/DVD drive.

Upvotes: 4

Views: 4650

Answers (3)

Remy Lebeau
Remy Lebeau

Reputation: 596437

David has the right answer in regards to avoiding the range check error. But if you don't want to do that, you can still turn off/on {$RANGECHECKS} manually, just use {$IFOPT} to do it conditionally so surrounding code is not affected, eg:

function DirExists(Name: string): Boolean;
var
  Code: Integer;
begin
  {$IFOPT R+}
    {$DEFINE _RPlusWasEnabled}
    {$R-}
  {$ENDIF}

  Code := GetFileAttributesW(PChar(Name));
  Result := (Code <> -1) and (FILE_ATTRIBUTE_DIRECTORY and Code <> 0);

  {$IFDEF _RPlusWasEnabled}
    {$UNDEF _RPlusWasEnabled}
    {$R+}
  {$ENDIF}
end;

With that said, checking the result of GetFileAttributes() for INVALID_FILE_ATTRIBUTES alone is not enough. A directory could exist but just not be accessible. That is why the RTL's DirectoryExists() function checks GetLastError() for multiple error codes (ERROR_PATH_NOT_FOUND, ERROR_BAD_NETPATH, ERROR_NOT_READY, etc) looking for that possible condition. Another thing DirectoryExists() can do is optionally check if the specified path is actually a shortcut to a directory, and if so check if the target directory exists or not.

Update: here is the implementation of SysUtils.DirectoryExists() in XE3:

function DirectoryExists(const Directory: string; FollowLink: Boolean = True): Boolean;
{$IFDEF MSWINDOWS}
var
  Code: Cardinal;
  Handle: THandle;
  LastError: Cardinal;
begin
  Result := False;
  Code := GetFileAttributes(PChar(Directory));

  if Code <> INVALID_FILE_ATTRIBUTES then
  begin
    if faSymLink and Code = 0 then
      Result := faDirectory and Code <> 0
    else
    begin
      if FollowLink then
      begin
        Handle := CreateFile(PChar(Directory), GENERIC_READ, FILE_SHARE_READ, nil,
          OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
        if Handle <> INVALID_HANDLE_VALUE then
        begin
          CloseHandle(Handle);
          Result := faDirectory and Code <> 0;
        end;
      end
      else if faDirectory and Code <> 0 then
        Result := True
      else
      begin
        Handle := CreateFile(PChar(Directory), GENERIC_READ, FILE_SHARE_READ, nil,
          OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
        if Handle <> INVALID_HANDLE_VALUE then
        begin
          CloseHandle(Handle);
          Result := False;
        end
        else
          Result := True;
      end;
    end;
  end
  else
  begin
    LastError := GetLastError;
    Result := (LastError <> ERROR_FILE_NOT_FOUND) and
      (LastError <> ERROR_PATH_NOT_FOUND) and
      (LastError <> ERROR_INVALID_NAME) and
      (LastError <> ERROR_BAD_NETPATH) and
      (LastError <> ERROR_NOT_READY);
  end;
end;
{$ENDIF MSWINDOWS}
{$IFDEF POSIX}
var
  StatBuf, LStatBuf: _stat;
  Success: Boolean;
  M: TMarshaller;
begin
  Success := stat(M.AsAnsi(Directory, CP_UTF8).ToPointer, StatBuf) = 0;
  Result := Success and S_ISDIR(StatBuf.st_mode);

  if not Result and (lstat(M.AsAnsi(Directory, CP_UTF8).ToPointer, LStatBuf) = 0) and
    S_ISLNK(LStatBuf.st_mode) then
  begin
    if Success then
      Result := S_ISDIR(StatBuf.st_mode)
    else if not FollowLink then
      Result := True;
  end;
end;
{$ENDIF POSIX}

The implementation in XE4 is the same with only one difference - the Windows version also includes a check for LastError <> ERROR_BAD_NET_NAME when calling GetLastError().

Upvotes: 3

David Heffernan
David Heffernan

Reputation: 612993

You can just fix your function so that it doesn't cause range check errors:

function DirExists(Name: string): Boolean;
var
  Code: DWORD;
begin
  Code := GetFileAttributes(PChar(Name));
  Result := (Code <> INVALID_FILE_ATTRIBUTES) 
    and (FILE_ATTRIBUTE_DIRECTORY and Code <> 0);
end;

The range check errors are due to you mixing signed and unsigned types. Remy also points out the very useful trick to set compiler options and then restore to the prevailing state. That's a good trick to learn, but you don't need it here.

The XE3 implementation of DirectoryExists is modified to address the problem that you have encountered. So, if using XE3+ was an option, you should take it.


To suppress the system error dialogs call this at process startup:

procedure SetProcessErrorMode;
var
  Mode: DWORD;
begin
  Mode := SetErrorMode(SEM_FAILCRITICALERRORS);
  SetErrorMode(Mode or SEM_FAILCRITICALERRORS);
end;

Doing this is best practise as described on MSDN:

Best practice is that all applications call the process- wide SetErrorMode function with a parameter of SEM_ FAILCRITICALERRORS at startup. This is to prevent error mode dialogs from hanging the application.

Upvotes: 4

Dmitry
Dmitry

Reputation: 14622

Update Delphi XE2 to Delphi XE3+ or use the following function:

function DirectoryExistsDelphiXE2(const Directory: string; FollowLink: Boolean = True): Boolean;
var
  Code: Cardinal;
  Handle: THandle;
  LastError: Cardinal;
begin
  Result := False;
  Code := GetFileAttributes(PChar(Directory));

  if Code <> INVALID_FILE_ATTRIBUTES then
  begin
    if faSymLink and Code = 0 then
      Result := faDirectory and Code <> 0
    else
    begin
      if FollowLink then
      begin
        Handle := CreateFile(PChar(Directory), GENERIC_READ, FILE_SHARE_READ, nil,
          OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
        if Handle <> INVALID_HANDLE_VALUE then
        begin
          CloseHandle(Handle);
          Result := faDirectory and Code <> 0;
        end;
      end
      else if faDirectory and Code <> 0 then
        Result := True
      else
      begin
        Handle := CreateFile(PChar(Directory), GENERIC_READ, FILE_SHARE_READ, nil,
          OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
        if Handle <> INVALID_HANDLE_VALUE then
        begin
          CloseHandle(Handle);
          Result := False;
        end
        else
          Result := True;
      end;
    end;
  end
  else
  begin
    LastError := GetLastError;
    Result := (LastError <> ERROR_FILE_NOT_FOUND) and
      (LastError <> ERROR_PATH_NOT_FOUND) and
      (LastError <> ERROR_INVALID_NAME) and
      (LastError <> ERROR_BAD_NETPATH) and
      (LastError <> ERROR_NOT_READY);
  end;
end;

Upvotes: 1

Related Questions