findfirst, findnext et findclose sur Linux

Les fonctions _findfirst, _findnext et _findclose de la CRT de Visual Studio permettent de lister les entrées d'un répertoire.

Comme ces fonctions sont spécifiques à la CRT de Visual Studio, les librairies standards fournies avec les systèmes d'exploitation UNIX en sont dépourvues. Leur absence se fait particulièrement sentir lorsque du code Windows doit être porté sur un système d'exploitation UNIX.

Les lignes qui suivent décrivent une implantation de ces fonctions pour Linux.

Comportement

Étonnement, définir le comportement des fonctions _findfirst, _findnext et _findclose n'est pas une tâche triviale. La documentation présente sur MSDN est — comme à l'habitude diront les mauvaises langues — plutôt vague. En particulier, la manière dont la spécification passée en argument à la fonction _findfirst est interprétée n'est pas documentée. Au mieux, nous savons que cette dernière peut contenir des métacaractères — wildcards.

Pour établir le comportement exacte des fonctions, l'expérimentation est donc de mise. Pour se faire, le programme suivant est employé.

#include <stdio.h>
#include <time.h>
#include <io.h>

int main(int argc, char* argv[]) {

    struct _finddata_t file_data;
    intptr_t find_handle;

    find_handle = _findfirst(argc > 1 ? argv[1]: "", &file_data);

    if (find_handle != -1L) {

        do {
            printf("%s\n", file_data.name);
        } while (_findnext(find_handle, &file_data) == 0);

        _findclose(find_handle);
    }

    return 0;
}

Les métacaractères supportés sont l'étoile et le point d'interrogation. Une étoile remplace zéro ou plusieurs caractères alors qu'un point d'interrogation remplace exactement un caractère. Les autres marques de ponctuation n'ont aucune signification particulière. Il importe également de noter que les métacaractères apparaissant dans un chemin fichier ne sont pas considérés. Autrement dit, une spécification de la forme /home/*/* ne retournera aucun résultat s'il n'existe aucune entrée nommée * dans le répertoire home.

Pour le reste, voici le comportement obtenu en fonction de différentes spécifications.

Implantation

Le traitement de la spécification constitue une partie délicate de l'implantation des fonctions _findfirst, _findnext et _findclose. Les fonctions match_spec implantent la logique nécessaire afin de déterminer si le nom d'une entrée correspond à la spécification passée en argument à _findfirst — *. étant traité en amont.

int _match_spec(const char* spec, const char* text) {

    /*
     * If the whole specification string was consumed and
     * the input text is also exhausted: it's a match.
     */
    if (spec[0] == '\0' && text[0] == '\0')
        return 1;

    /* A star matches 0 or more characters. */
    if (spec[0] == '*') {
        /*
         * Skip the star and try to find a match after it
         * by successively incrementing the text pointer.
         */
        do {
            if (_match_spec(spec + 1, text))
                return 1;
        } while (*text++ != '\0');
    }

    /*
     * An interrogation mark matches any character. Other
     * characters match themself. Also, if the input text
     * is exhausted but the specification isn't, there is
     * no match.
     */
    if (text[0] != '\0' && (spec[0] == '?' || spec[0] == text[0]))
        return _match_spec(spec + 1, text + 1);

    return 0;
}

int match_spec(const char* spec, const char* text) {

    /* On Windows, *.* matches everything. */
    if (strcmp(spec, "*.*") == 0)
        return 1;

    return _match_spec(spec, text);
}

La fonction _findfirst sépare le filespec en deux parties: une spécification et un chemin fichier. Selon la nature de la spécification et du chemin de fichier, la fonction de support appropriée est appelée afin de compléter la tâche.

intptr_t _findfirst(const char* filespec, struct _finddata_t* fileinfo) {

    char* dirpath;
    char* rmslash;      /* Rightmost forward slash in filespec. */
    const char* spec;   /* Specification string. */

    if (!fileinfo || !filespec) {
        errno = EINVAL;
        return INVALID_HANDLE;
    }

    if (filespec[0] == '\0') {
        errno = ENOENT;
        return INVALID_HANDLE;
    }

    rmslash = strrchr(filespec, '/');

    if (rmslash != NULL) {
        /*
         * At least one forward slash was found in the filespec
         * string, and rmslash points to the rightmost one. The
         * specification part, if any, begins right after it.
         */
        spec = rmslash + 1;
    } else {
        /*
         * Since no slash were found in the filespec string, its
         * entire content can be used as our spec string.
         */
        spec = filespec;
    }

    if (strcmp(spec, ".") == 0 || strcmp(spec, "..") == 0) {
        /* On Windows, . and .. must return canonicalized names. */
        return findfirst_dotdot(filespec, fileinfo);
    } else if (rmslash == filespec) {
        /*
         * Since the rightmost slash is the first character, we're
         * looking for something located at the file system's root.
         */
        return findfirst_in_directory("/", spec, fileinfo);
    } else if (rmslash != NULL) {
        /*
         * Since the rightmost slash isn't the first one, we're
         * looking for something located in a specific folder. In
         * order to open this folder, we split the folder path from
         * the specification part by overwriting the rightmost
         * forward slash.
         */
        dirpath = strdupa(filespec);
        dirpath[rmslash - filespec] = '\0';
        return findfirst_in_directory(dirpath, spec, fileinfo);
    } else {
        /*
         * Since the filespec doesn't contain any forward slash,
         * we're looking for something located in the current
         * directory.
         */
        return findfirst_in_directory(".", spec, fileinfo);
    }
}

La première fonction de support, findfirst_dotdot, gère le cas particulier où la spécification n'est formée que d'un ou deux points.

static intptr_t findfirst_dotdot(const char* filespec,
        struct _finddata_t* fileinfo) {

    char* dirname;
    char* canonicalized;
    struct stat st;

    if (stat(filespec, &st) != 0) {
        findfirst_set_errno();
        return INVALID_HANDLE;
    }

    /* Resolve filespec to an absolute path. */
    if ((canonicalized = realpath(filespec, NULL)) == NULL) {
        findfirst_set_errno();
        return INVALID_HANDLE;
    }

    /* Retrieve the basename from it. */
    dirname = basename(canonicalized);

    /* Make sure that we actually have a basename. */
    if (dirname[0] == '\0') {
        free(canonicalized);
        errno = ENOENT;
        return INVALID_HANDLE;
    }

    /* Make sure that we won't overflow finddata_t::name. */
    if (strlen(dirname) > 259) {
        free(canonicalized);
        errno = ENOMEM;
        return INVALID_HANDLE;
    }

    fill_finddata(&st, dirname, fileinfo);

    free(canonicalized);

    /*
     * Return a special handle since we can't return
     * NULL. The findnext and findclose functions know
     * about this custom handle.
     */
    return DOTDOT_HANDLE;
}

La seconde fonction de support, findfirst_in_directory gère la situation courante où les entrées du répertoire devront être filtrés une à une.

static intptr_t findfirst_in_directory(const char* dirpath,
        const char* spec, struct _finddata_t* fileinfo) {

    DIR* dstream;
    fhandle_t* ffhandle;

    if (spec[0] == '\0') {
        errno = ENOENT;
        return INVALID_HANDLE;
    }

    if ((dstream = opendir(dirpath)) == NULL) {
        findfirst_set_errno();
        return INVALID_HANDLE;
    }

    if ((ffhandle = malloc(sizeof(fhandle_t))) == NULL) {
        closedir(dstream);
        errno = ENOMEM;
        return INVALID_HANDLE;
    }

    /* On Windows, *. returns only directories. */
    ffhandle->dironly = strcmp(spec, "*.") == 0 ? 1 : 0;
    ffhandle->dstream = dstream;
    ffhandle->spec = strdup(spec);

    if (_findnext((intptr_t) ffhandle, fileinfo) != 0) {
        _findclose((intptr_t) ffhandle);
        errno = ENOENT;
        return INVALID_HANDLE;
    }

    return (intptr_t) ffhandle;
}

Jusqu'à ce que le répertoire soit épuisé, _findnext itère à la recherche d'une correspondance. Entre chaque appel à _findnext, l'état de l'itération est conservé par le directory stream contenu dans la structure fhandle_t.

int _findnext(intptr_t fhandle, struct _finddata_t* fileinfo) {

    struct dirent entry, *result;
    struct fhandle_t* handle;
    struct stat st;

    if (fhandle == DOTDOT_HANDLE) {
        errno = ENOENT;
        return -1;
    }

    if (fhandle == INVALID_HANDLE || !fileinfo) {
        errno = EINVAL;
        return -1;
    }

    handle = (struct fhandle_t*) fhandle;

    while (readdir_r(handle->dstream, &entry, &result) == 0 && result != NULL) {

        if (!handle->dironly && !match_spec(handle->spec, entry.d_name))
            continue;

        if (fstatat(dirfd(handle->dstream), entry.d_name, &st, 0) == -1)
            return -1;

        if (handle->dironly && !S_ISDIR(st.st_mode))
            continue;

        fill_finddata(&st, entry.d_name, fileinfo);

        return 0;
    }

    errno = ENOENT;
    return -1;
}

Conclusion

Le code source peut être consulté en ligne à partir des liens ci-dessous ou encore sur github: findfirst. Le fichier findfirst.c contient l'implantation des fonctions _findfirst, _findnext et _findclose. Les fonctions match_spec sont définies dans le fichier spec.c.

Le code devrait compiler autant sur Windows, Linux que FreeBSD et les tests unitaires devraient être fonctionnels sur toutes ces plateformes (Makefile, CMakeLists.txt).