Fable 5 and ChatGPT 5.6 Sol agreed on this:
Summary
hb_vfExists() returns .F. both when a file genuinely does not exist and when the operating system cannot query the file because a network share is unavailable.
Although HB_VFEXISTS copies hb_fsError() into FError(), the underlying Windows implementation of hb_fsFileExists() calls GetFileAttributes() without calling hb_fsSetIOError(). Consequently, FError() and hb_osError() retain the result of an earlier I/O operation instead of reporting the failure from the current existence check. The same applies to the two-argument form hb_vfExists( cFile, @cFound ), because hb_spFileExists() also resolves through hb_fsFileExists().
This makes a missing file indistinguishable from an inaccessible network path. A common check-then-create pattern can therefore become destructive:
if !hb_vfExists( cFile )
dbCreate( cFile, aStructure, "DBFCDX" )
endif
If the network share temporarily fails during hb_vfExists() but becomes reachable again before dbCreate(), the create operation may truncate an already existing file.
Environment
- Harbour 3.2.1dev
- Commit: 6df4c08b98c808904e0c19effbc09523af010ed6
- Windows
- SMB network share, using either a mapped drive or a UNC path
Reproduction
1. Place an existing file on an SMB share.
2. Ensure the file is normally accessible.
3. Disconnect the share, interrupt the network, or stop the SMB server.
4. Run the following program with cNetworkFile pointing to that existing file:
procedure Main()
local cNetworkFile := "\\server\share\existing.dbf"
local lExists
local nAttributes := 0
local nFileError
local nOperatingSystemError
// Clear the previous I/O error state with a successful local operation.
hb_vfAttrGet( hb_ProgName(), @nAttributes )
lExists := hb_vfExists( cNetworkFile )
nFileError := FError()
nOperatingSystemError := hb_osError()
? "hb_vfExists():", lExists
? "FError():", nFileError
? "hb_osError():", nOperatingSystemError
lExists := hb_vfAttrGet( cNetworkFile, @nAttributes )
nFileError := FError()
nOperatingSystemError := hb_osError()
? "hb_vfAttrGet():", lExists
? "FError():", nFileError
? "hb_osError():", nOperatingSystemError
return
Actual result
hb_vfExists() returns .F., but FError() and hb_osError() remain zero or contain stale values from a previous operation:
hb_vfExists(): .F.
FError(): 0
hb_osError(): 0
The subsequent hb_vfAttrGet() call returns .F. and correctly reports the network error, such as 53, 64, or 67.
Expected result
When the underlying metadata query fails, FError() and hb_osError() should describe that operation's failure. They must not retain unrelated values from an earlier I/O operation.
Root cause
HB_VFEXISTS calls hb_fileExists(), reads hb_fsError(), and stores it in FError() (vfile.c#L138-L162 (
https://github.com/harbour/core/blob/6df4c08b98c808904e0c19effbc09523af010ed6/src/rtl/vfile.c#L138-L162)).
For a plain path with no registered file driver, hb_fileExists() resolves to hb_fsFileExists() (directly for the one-argument call, or through hb_spFileExists() for the two-argument call). On Windows, hb_fsFileExists() calls GetFileAttributes() but does not call hb_fsSetIOError(), leaving both the translated and raw OS error states unchanged (hbfsapi.c#L620-L638 (
https://github.com/harbour/core/blob/6df4c08b98c808904e0c19effbc09523af010ed6/src/common/hbfsapi.c#L620-L638)). The POSIX branch has the same defect: the stat()/stat64() result is never propagated into the Harbour error state.
By comparison, hb_vfAttrGet() reaches hb_fsGetAttr(), which calls hb_fsSetIOError() immediately after GetFileAttributes(). It therefore provides a reliable FError() and raw hb_osError() (filesys.c#L2044-L2073 (
https://github.com/harbour/core/blob/6df4c08b98c808904e0c19effbc09523af010ed6/src/rtl/filesys.c#L2044-L2073)).
The same stale-error problem also applies to hb_vfDirExists() through hb_fsDirExists().
Possible Harbour fix
The existence implementation should preserve the result of the underlying OS metadata query, following the pattern already used in hb_fsGetAttr().
On Windows, the error state should be set according to whether dwAttr != INVALID_FILE_ATTRIBUTES, immediately after GetFileAttributes(). The operation-success flag should not be based on the final file/directory type predicate: a successful query for a directory during a file check should return .F. with error zero, rather than reading a stale GetLastError() value.
Equivalent error preservation should be applied on the other supported platforms (e.g. based on the stat() result on POSIX systems).
Application-level workaround
Use hb_vfAttrGet() and capture both error values immediately:
- If the call succeeds, inspect the attributes to distinguish a file from a directory.
- If it fails with raw OS error 2, the target may be absent.
- Any other OS error must be treated as "state unknown"; creation must stop.
- Before creation, validate the parent directory with another hb_vfAttrGet() and repeat the target-file probe.
- Do not use hb_vfDirExists() as the workaround because it currently has the same stale-error problem.
This workaround prevents network errors from being interpreted as missing files. It does not independently eliminate the separate check-then-create concurrency race.