. .
. .
. .
.

Logging a directory, part 3.

I started part 2 of the tutorial by changing the file mask I was searching for, (from .exe to .exx), I did that to demonstrate the behaviour if there were no matching files. I'm going to start this page by changing the path/mask from C: to G:, (I do not have a G: drive). When I do this, the program we finished up with on page 2 outputs this...

Error 3

... because FindFirstFile() differentiates between not being able to find a matching file on a valid path, and attempting to search an invalid path. Here, I have extended the test to allow for this eventuality...

    if(hFind == INVALID_HANDLE_VALUE)
    {
        ErrorCode = GetLastError();
        if (ErrorCode == ERROR_FILE_NOT_FOUND)
        {
            cout << "There are no files matching that path/mask\n" << endl;
        }
        else if (ErrorCode == ERROR_PATH_NOT_FOUND)
        {
            cout << "The specified path is invalid\n" << endl;
        }
        else
        {
            cout << "FindFirstFile() returned error code " << ErrorCode << endl;
        }
        Continue = FALSE;
    }
    else
    {
        cout << FindData.cFileName << endl;
    }

... this program outputs this with the G: path...

Invalid Path

... as expected.

----------

Another thing that people have asked about is the way I specified the path/mask, in this case, "C:\\Windows\\*.exe". It is, of course, possible to use a variable there rather than a literal. These tutorials are to demostrate techniques, not to produce finished programs. In your applications you will almost certainly want to have a variable containing your path/mask in the call to FindFirstFile().

Perhaps suprisingly, a number of people asked why I used 2 backslash characters in the pathname. This is, of course, because the C and C++ languages use the backslash character to introduce a "special character" into strings, "\n" for example is interpretted as a newline character. To get a backslash character into a string literal, you must use 2 backslashes, thus "\\" is interpretted as "\".

If you don't like the double backslash characters, you can use a single forward slash character a la Unix, i.e. "C:\\Windows\\*.exe" and "C:/Windows/*.exe" will both work and produce the same result.

----------

What about the other fields in the WIN32_FIND_DATA structure, how are they used? Here, again, is the definition of the structure...

typedef struct _WIN32_FIND_DATA {
    DWORD    dwFileAttributes; 
    FILETIME ftCreationTime; 
    FILETIME ftLastAccessTime; 
    FILETIME ftLastWriteTime; 
    DWORD    nFileSizeHigh; 
    DWORD    nFileSizeLow; 
    DWORD    dwReserved0; 
    DWORD    dwReserved1; 
    TCHAR    cFileName[ MAX_PATH ]; 
    TCHAR    cAlternateFileName[ 14 ]; 
} WIN32_FIND_DATA; 

... let's have a more detailed look at these.

Attributes are associated with all files, they tell you something about the file. There are a whole list of these attributes, although I doubt you'll ever use most of them, I have only ever used a few. The full list is, at present...

FILE_ATTRIBUTE_ARCHIVE 
FILE_ATTRIBUTE_COMPRESSED 
FILE_ATTRIBUTE_DIRECTORY 
FILE_ATTRIBUTE_ENCRYPTED 
FILE_ATTRIBUTE_HIDDEN 
FILE_ATTRIBUTE_NORMAL 
FILE_ATTRIBUTE_OFFLINE 
FILE_ATTRIBUTE_READONLY 
FILE_ATTRIBUTE_REPARSE_POINT 
FILE_ATTRIBUTE_SPARSE_FILE 
FILE_ATTRIBUTE_SYSTEM
FILE_ATTRIBUTE_TEMPORARY 

... I say at present, because Windows is a developing product, and the list may be added too at some future point. If you are interested in what they all do, look them up in your compilers help, or at MSDN. (If the link to MSDN doesn't work, search Google for "MSDN" then navigate to the library - for some reason, Microsoft are forever changing their URL's around making it difficult to keep links up to date!).

FILE_ATTRIBUTE_DIRECTORY is a useful one, this is TRUE if the file returned by FindFirstFile() or FindNextFile() is actually a directory rather than a normal file. You will want to use this if you plan to process files in a directory or any of it's sub-directories. FILE_ATTRIBUTE_READONLY is another which may be of use at some time, predictably, it is TRUE if the file has been set to "read only".

To test if any particular attribute is set for a file, do a bitwise AND between the dwFileAttributes member of the WIN32_FIND_DATA structure, and the attribute you are interested in. Here, I have modified the logging program developed in the first 2 parts of the tutorial to flag READONLY files, remember, this modification must be made to both the FindFirstFile() and the FindNextFile() blocks.

    {
        cout << FindData.cFileName;
        if (FindData.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
        {
            cout << " File is READONLY!";
        }
        cout << endl;
    }

... on my Windows directory, the program gave this output...

ReadOnly Flagged

... note the file SiSUSBrg.exe is flagged as READ ONLY.

The files creation, last write and last access date/times are returned in a FILETIME structure. This is not terribly useful for output, but the API provides a routine FileTimeToSystemTime() which converts these values into a SYSTEMTIME structure which is contains various fields you can output. The SYSTEMTIME structure is declared as...

typedef struct _SYSTEMTIME {
    WORD wYear; 
    WORD wMonth; 
    WORD wDayOfWeek; 
    WORD wDay; 
    WORD wHour; 
    WORD wMinute; 
    WORD wSecond; 
    WORD wMilliseconds; 
} SYSTEMTIME;

... this example shows the conversion and output of the creation time, however all three of the files date/time statistics can be treated in the same manner. I've added this line to the declarations...

    SYSTEMTIME SysTime;

... and this to the output sections...

    {
        cout << FindData.cFileName << " ";
        FileTimeToSystemTime(&FindData.ftCreationTime, &SysTime);
        cout.width(2);
        cout.fill('0');
        cout << SysTime.wDay << "/";
        cout.width(2);
        cout.fill('0');
        cout << SysTime.wMonth << "/";
        cout << SysTime.wYear << " ";
        cout.width(2);
        cout.fill('0');
        cout << SysTime.wHour << ":";
        cout.width(2);
        cout.fill('0');
        cout << SysTime.wMinute << ":";
        cout.width(2);
        cout.fill('0');
        cout << SysTime.wSecond;
        cout << endl;
    }

... the output from this program now looks like this...

Creation Date

... yeah, I know it's not the nicest formatting job - that is left as an exercise for the reader! There are a couple of other fields available in the SYSTEMTIME structure, (above), which could be of interest to some.

The way the API developers chose to return the file size is a little odd. True, using only a single 32bit value would break with any file larger than 4GB, however, they use other 64bit constructions in the WIN32_FIND_DATA structure so why not for this field? Ho hum, what they have done is returned two 32 bit values, the first representing the most significant 32 bits of the size, the second, the least significant 32 bits, and we'll have to live with that because that is how it is!

If you are absolutely certain your files will be less than 4GB, you can ignore the nFileSizeHigh member, and use the nFileSizeLow as the file size. I am certain the files in my Windows directory are less than 4GB so I can output their size using...

    {
        cout << FindData.cFileName;
        cout << " " << FindData.nFileSizeLow << endl;
    }

... the output from this program looks like this...

File Sizes

... if you need to use a 64bit value, load the 32bit values into a LARGE_INTEGER union. This is declared as...

typedef union _LARGE_INTEGER { 
    struct {
        DWORD LowPart; 
        LONG  HighPart; 
    };
    LONGLONG QuadPart;
} LARGE_INTEGER; 

... declare the Union like this...

    ULARGE_INTEGER FileSize;

... you could also use LARGE_INTEGER but since a negative file size is meaningless, using the unsigned variant gives you more scope. Load the union like this...

        FileSize.LowPart = FindData.nFileSizeLow;
        FileSize.HighPart = FindData.nFileSizeHigh;

... then use FileSize.QuadPart for 64bit operations.

The last field we'll use is the cAlternateFileName field. This simply contains the "8.3" format equivalent filename. I've now modified the program to output both the full, and the alternate filenames, thus...

    {
        cout << FindData.cFileName;
        cout << " " << FindData.cAlternateFileName << endl;
    }

... which gives this output...

Alternate FileName

... note the long filenames have an equivalent "8.3" filename. Those that were already in 8.3 format have an empty cAlternateFileName field.

----------

The last thing I want to cover in this, already long supplement to the tutorial, involves case sensitivity. The mask passed to FindFirstFile() is not case sensitive, if you look back at any of the results I've shown so far, they include both .exe and .EXE files, even though I have always passed *.exe as the search mask.

There is a second API function, FindFirstFileEx(), which is broadly similar to FindFirstFile() we have used throughout the tutorial. I was drawn to it because I wanted to do case sensitive file searches, and the documentation that comes with VC++ 6.0 Pro says...

dwAdditionalFlags 
    Specifies additional flags for controlling the search.
    You can use the FIND_FIRST_EX_CASE_SENSITIVE flag for 
    case-sensitive searches. The default search is case 
    insensitive. At this time, no other flags are defined.

... I could never get this to work. The most up to date API documentation, (i.e. MSDN today, 28/04/2003), says something different...

dwAdditionalFlags 
    Reserved for future use. This parameter must be zero.

... so I guess they couldn't get it to work either! The bottom line would seem to be that we are stuck with case insensitvity for now!


.
. .
.
Previous Index Next Page
Site Home
.
. .
Copyright © adrianxw, 1997 - 2003.