June 12, 2002
Walter, I've just noticed that the latest D distrubution still uses the old outdated stream module. Is there anything wrong with the new one, found on my site?


June 13, 2002
Pavel Minayev wrote:
> Walter, I've just noticed that the latest D distrubution still
> uses the old outdated stream module. Is there anything wrong
> with the new one, found on my site?

My stat isn't in yet either, and I submitted that a month and a half ago.  Not to mention random and fmt... but weirdly enough, the imports I needed for it are in Phobos.  Since I put this in one message a long time ago now, here's the code again, with some unittests added.

It won't work out-of-box because the following program:

    import path;

    unittest { }
    int main(char[][] argv) { return 0; }

Can't be linked when compiled with "c:\dmd\bin\dmd.exe -debug -unittest" - it gives a "Error 42: Symbol Undefined __ModuleInfo_path".  I managed to ensure the unittests were working by taking path apart.

-- /* For the string module */ --

/***************************************************
 * Determine whether the string ends with another.
 */

bit endswith(char[] string, char[] match)
{
    if (string.length < match.length)
        return 0;
    if (memcmp(&string [string.length - match.length], &match [0], match.length))
        return 0;
    return 1;
}

unittest
{
    assert(endswith("hello", "llo"));
    assert(!endswith("hello", "ell"));
    assert(!endswith("hello", "hhhhello"));
}

/***************************************************
 * Determine whether the string begins with another.
 */

bit startswith(char[] string, char[] match)
{
    if (string.length < match.length)
        return 0;
    if (memcmp(&string [0], &match [0], match.length))
        return 0;
    return 1;
}

unittest
{
    assert(startswith("hello", "hel"));
    assert(!startswith("hello", "ell"));
    assert(!startswith("hello", "helloooooo"));
}

-- /* For the file module */ --

import date;
import path;

/***************************************************
 * Structure of a file stat.
 */

class Stat
{
    char[] filename; /* Filename plus full path */
    char[] filebase; /* Filename without path */
    char[] alternate; /* Alternate (8.3) filename, no path */
    d_time creation; /* When this file was created */
    d_time access; /* When this file was last used (this will probably be writeTime in Linux) */
    d_time write; /* When this file was last written */
    long filesize; /* File size in bytes */

    bit archive; /* Is an archive file */
    bit directory; /* Is a directory */
    bit hidden; /* Is hidden (Linux should probably flag this if the file starts with .) */
    bit readonly; /* Is read-only (Linux should set this if the process can't write this file) */
    bit system; /* Is a system file */

    this (char[] path, WIN32_FIND_DATA finder)
    {
        SYSTEMTIME systemtime;
        FILETIME filetime;

        /* Get the filename and file size */
        filebase = ((char []) finder.cFileName).dup;
        filename = path ~ filebase;
        alternate = ((char []) finder.cAlternateFileName).dup;
        filesize = (long) finder.nFileSizeHigh * (long) 0x100000000 + finder.nFileSizeLow;

        /* Get the attributes */
        if (finder.dwFileAttributes & FILE_ATTRIBUTE_ARCHIVE)
            archive = 1;
        if (finder.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
            directory = 1;
        if (finder.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)
            hidden = 1;
        if (finder.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
            readonly = 1;
        if (finder.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM)
            system = 1;

        /* Get the times */

        filetime = finder.ftCreationTime;
        if (filetime.dwLowDateTime == 0 && filetime.dwHighDateTime == 0)
            filetime = finder.ftLastWriteTime;
        if (!FileTimeToSystemTime(&filetime, &systemtime))
            throw new FileError(filename, GetLastError());
        creation = SYSTEMTIME2d_time(&systemtime, 0);

        filetime = finder.ftLastAccessTime;
        if (filetime.dwLowDateTime == 0 && filetime.dwHighDateTime == 0)
            filetime = finder.ftLastWriteTime;
        if (!FileTimeToSystemTime(&filetime, &systemtime))
            throw new FileError(filename, GetLastError());
        access = SYSTEMTIME2d_time(&systemtime, 0);

        filetime = finder.ftLastWriteTime;
        if (!FileTimeToSystemTime(&filetime, &systemtime))
            throw new FileError(filename, GetLastError());
        write = SYSTEMTIME2d_time(&systemtime, 0);
    }
}

/***************************************************
 * Collate all files in a directory, returning an
 * array of strings.  The strings do not contain the
 * path.  Throws FileError if the directory does not
 * exist, if there was some file problem during the
 * collation, or if the path is malformed (generally
 * trying to use nonportable features).
 */

char[][] listdir(char[] path)
{
    WIN32_FIND_DATA finder;
    HANDLE handle;
    char[][] list;
    bit done = 0;

    /* Since we want to flatten out platform differences,
       first we cry if the user tried to use Windows. */

    if (find (path, (char) "*") >= 0 || find (path, (char) "?") >= 0)
        throw new FileError(path, "Path contains invalid characters");

    /* Form the search path for Windows */
    if (endswith(path, "/") || endswith(path, "\\"))
        path ~= "*";
    else
        path ~= "/*";

    /* Start the file search */
    handle = FindFirstFileA(toStringz(path ~ "*"), &finder);
    if (handle == (HANDLE)-1)
        throw new FileError(path, GetLastError());

    while (!done)
    {
        char[] filename;

        /* Handle this file in our own inimitable fashion */
        filename = (char[]) finder.cFileName;
        list ~= filename.dup;

        /* Go to the next file or bust */
        if (!FindNextFileA(handle, &finder))
        {
            if (GetLastError() == ERROR_NO_MORE_FILES)
                done = 1;
            else
            {
                FindClose(handle);
                throw new FileError(path, GetLastError());
            }
        }
    }

    /* Finished, clean up */
    if (!FindClose(handle))
        throw new FileError(path, GetLastError());

    return list;
}

/***************************************************
 * Collate all files in a directory, returning an
 * array of stats.  Throws FileError if the
 * directory does not exist, if there was some file
 * problem during the collation, or if the path is
 * malformed (generally trying to use nonportable
 * features).
 */

Stat[] statdir(char[] path)
{
    WIN32_FIND_DATA finder;
    HANDLE handle;
    Stat[] list;
    bit done = 0;

    /* Since we want to flatten out platform differences,
       first we cry if the user tried to use Windows. */

    if (find (path, (char) "*") >= 0 || find (path, (char) "?") >= 0)
        throw new FileError(path, "Path contains invalid characters");

    /* Form the search path for Windows */
    if (!endswith(path, "/") && !endswith(path, "\\"))
        path ~= "/";

    /* Start the file search */
    handle = FindFirstFileA(toStringz(path ~ "*"), &finder);
    if (handle == (HANDLE)-1)
        throw new FileError(path, GetLastError());

    while (!done)
    {
        /* Handle this file in our own inimitable fashion */
        list ~= new Stat (path, finder);

        /* Go to the next file or bust */
        if (!FindNextFileA(handle, &finder))
        {
            if (GetLastError() == ERROR_NO_MORE_FILES)
                done = 1;
            else
            {
                FindClose(handle);
                throw new FileError(path, GetLastError());
            }
        }
    }

    /* Finished, clean up */
    if (!FindClose(handle))
        throw new FileError(path, GetLastError());

    return list;
}

/***************************************************
 * Attempt to stat a single file, returning the
 * file details.  This will throw FileError when the
 * file cannot be found or when the path is badly
 * formed.
 */

Stat stat(char[] filename)
{
    WIN32_FIND_DATA finder;
    HANDLE handle;
    char[] path;
    Stat stat;

    /* Since we want to flatten out platform differences,
       first we cry if the user tried to use Windows. */

    if (find (filename, (char) "*") >= 0 || find (filename, (char) "?") >= 0)
        throw new FileError(filename, "Path contains invalid characters");

    /* Start the file search */
    handle = FindFirstFileA(toStringz(filename), &finder);
    if (handle == (HANDLE)-1)
        throw new FileError(filename, GetLastError());

    /* Construct the path */
    if (filename [0] != "/" && filename [0] != "\\")
        path = curdir;
    else
        path = getDirName(filename);

    /* Statify the sucker */
    stat = new Stat(path, finder);

    /* Finish up */
    if (!FindClose(handle))
        throw new FileError(path ~ pathsep, GetLastError());

    return stat;
}

unittest
{
    /* Check that listdir, statdir, and stat are equivalent */
    char[][] ls = listdir(".");
    Stat[]   ss = statdir(".");
    Stat st;
    int c, d;

    /* They should have the same length */
    assert(ls.length == ss.length);

    /* Do some cross-checking */
    for (c = 0; c < ss.length; c ++)
    {
        /* Ensure it's in the other list */
        for (d = 0; d < ls.length; d ++)
        {
            if (ls [d] == ss [c].filebase)
                break;
        }

        /* Ran off the end of the list */
        if (d >= ls.length)
        {
            printf ("%.*s was not found in list", ss [c].filebase);
            assert(d < ls.length); /* If it isn't, we finished the loop */
        }

        /* Now compare it to a manual stat */
        st = stat(ss [c].filename);

        assert(st.filename == ss [c].filename);
        assert(st.filebase == ss [c].filebase);
        assert(st.alternate == ss [c].alternate);
        assert(st.creation == ss [c].creation);
        assert(st.access == ss [c].access);
        assert(st.write == ss [c].write);
        assert(st.filesize == ss [c].filesize);

        /* Make some sanity checks on the times */
        assert(st.write >= st.creation);
        assert(st.access >= st.write);
    }
}