On some of the Unix-derived file systems on which Inform runs, filenames are case-sensitive, so that FISH and fish might be different files. This makes extension files, installed by the user, prone to being missed. The code in this section provides a routine to carry out file opening as if filenames are case-insensitive, and is used only for extensions.


§1. This section contains a single utility routine, contributed by Adam Thornton: a specialised, case-insensitive form of fopen(). It is specialised in that it is designed for opening extensions, where the file path will be case-correct up to the last two components of the path (the leafname and the immediately containing directory), but where the casing may be wrong in those last two components.

§2. If the exact filename or extension directory (case-correct) exists, CIFilingSystem::fopen() will choose it to open. If not, it will use strcasecmp() to find a file or directory with the same name but differing in case and use it instead. If it finds exactly one candidate file, it will then attempt to fopen() it and return the result.

If CIFilingSystem::fopen() succeeds, it returns a FILE * (passed back to it from the underlying fopen()). If CIFilingSystem::fopen() fails, it returns NULL, and errno is set accordingly:

§3. The routine. The routine is available only on POSIX platforms where PLATFORM_POSIX is defined (see "Platform-Specific Definitions"). In practice this means everywhere except Windows, but all Windows file systems are case-preserving and case-insensitive in any case.

Briefly, we try to get the extension directory name right first, by looking for the given casing, then if that fails, for a unique alternative with different casing; and then repeat within that directory for the extension file itself.

    FILE *CIFilingSystem::fopen(const char *path, const char *mode) {
        char *topdirpath = NULL, *ciextdirpath = NULL, *cistring = NULL, *ciextname = NULL;
        char *workstring = NULL, *workstring2 = NULL;
        DIR *topdir = NULL, *extdir = NULL; FILE *handle;
        size_t length;

        for efficiency's sake, though it's logically equivalent, we try...
        handle = fopen(path, mode); if (handle) <Happy ending to ci-fopen 3.4>;

        <Find the length of the path, giving an error if it is empty or NULL 3.6>;
        <Allocate memory for strings large enough to hold any subpath of the path 3.3>;
        <Parse the path to break it into topdir path, extension directory and leafname 3.7>;

        topdir = opendir(topdirpath);     whose pathname is assumed case-correct...
        if (topdir == NULL) <Sad ending to ci-fopen 3.5>;     ...so that failure is fatal; errno is set by opendir

        sprintf(workstring, "%s%c%s", topdirpath, FOLDER_SEPARATOR, ciextdirpath);
        extdir = opendir(workstring);     try with supplied extension directory name
        if (extdir == NULL) <Try to find a unique insensitively matching directory name in topdir 3.1>
        else strcpy(cistring, workstring);

        sprintf(workstring, "%s%c%s", cistring, FOLDER_SEPARATOR, ciextname);
        handle = fopen(workstring, mode);     try with supplied name
        if (handle) <Happy ending to ci-fopen 3.4>;

        <Try to find a unique insensitively matching entry in extdir 3.2>;
    }

This paragraph is used only if PLATFORM_POSIX is defined.

The function CIFilingSystem::fopen is used in §5, 3/fln (§10).

§3.1. Looking for case-insensitive matches instead. We emerge from the following only in the happy case where a unique matching directory name can be found.

<Try to find a unique insensitively matching directory name in topdir 3.1> =

        int rc = CIFilingSystem::match_in_directory(topdir, ciextdirpath, workstring);
        switch (rc) {
            case 0:
                errno = ENOENT; <Sad ending to ci-fopen 3.5>;
            case 1:
                sprintf(cistring, "%s%c%s", topdirpath, FOLDER_SEPARATOR, workstring);
                extdir = opendir(cistring);
                if (extdir == NULL) {
                    errno = ENOENT; <Sad ending to ci-fopen 3.5>;
                }
                break;
            default:
                errno = EBADF; <Sad ending to ci-fopen 3.5>;
        }

This code is used in §3.

§3.2. More or less the same, but we never emerge at all: all cases of the switch return from the function.

<Try to find a unique insensitively matching entry in extdir 3.2> =

        int rc = CIFilingSystem::match_in_directory(extdir, ciextname, workstring);

        switch (rc) {
            case 0:
                errno = ENOENT; <Sad ending to ci-fopen 3.5>;
            case 1:
                sprintf(workstring2, "%s%c%s", cistring, FOLDER_SEPARATOR, workstring);
                workstring2[length] = 0;
                handle = fopen(workstring2, mode);
                if (handle) <Happy ending to ci-fopen 3.4>;
                errno = ENOENT; <Sad ending to ci-fopen 3.5>;
            default:
                errno = EBADF; <Sad ending to ci-fopen 3.5>;
        }

This code is used in §3.

§3.3. Allocation and deallocation. We use six strings to hold full or partial pathnames.

<Allocate memory for strings large enough to hold any subpath of the path 3.3> =

        workstring = calloc(length+1, sizeof(char));
        if (workstring == NULL) { errno = ENOMEM; <Sad ending to ci-fopen 3.5>; }
        workstring2 = calloc(length+1, sizeof(char));
        if (workstring2 == NULL) { errno = ENOMEM; <Sad ending to ci-fopen 3.5>; }
        topdirpath = calloc(length+1, sizeof(char));
        if (topdirpath == NULL) { errno = ENOMEM; <Sad ending to ci-fopen 3.5>; }
        ciextdirpath = calloc(length+1, sizeof(char));
        if (ciextdirpath == NULL) { errno = ENOMEM; <Sad ending to ci-fopen 3.5>; }
        cistring = calloc(length+1, sizeof(char));
        if (cistring == NULL) { errno = ENOMEM; <Sad ending to ci-fopen 3.5>; }
        ciextname = calloc(length+1, sizeof(char));
        if (ciextname == NULL) { errno = ENOMEM; <Sad ending to ci-fopen 3.5>; }

This code is used in §3.

§3.4. If we are successful, we return a valid file handle...

<Happy ending to ci-fopen 3.4> =

        <Prepare to exit ci-fopen cleanly 3.4.1>;
        return handle;

This code is used in §3 (twice), §3.2.

§3.5. ...and otherwise NULL, having already set errno with the reason why.

<Sad ending to ci-fopen 3.5> =

        <Prepare to exit ci-fopen cleanly 3.4.1>;
        return NULL;

This code is used in §3, §3.1 (three times), §3.2 (three times), §3.3 (6 times).

§3.4.1. <Prepare to exit ci-fopen cleanly 3.4.1> =

        if (workstring) free(workstring);
        if (workstring2) free(workstring2);
        if (topdirpath) free(topdirpath);
        if (ciextdirpath) free(ciextdirpath);
        if (cistring) free(cistring);
        if (ciextname) free(ciextname);
        if (topdir) closedir(topdir);
        if (extdir) closedir(extdir);

This code is used in §3.4, §3.5.

§3.6. Pathname hacking.

<Find the length of the path, giving an error if it is empty or NULL 3.6> =

        length = 0;
        if (path) length = (size_t) strlen(path);
        if (length < 1) { errno = ENOENT; return NULL; }

This code is used in §3.

§3.7. And here we break up a pathname like

        /Users/bobama/Library/Inform/Extensions/Hillary Clinton/Health Care.i7x

into three components:

        topdirpath| is |/Users/bobama/Library/Inform/Extensions
    , and its casing is correct    ciextdirpath| is |Hillary Clinton
    , but its casing may not be correct    ciextname| is |Health Care.i7x
    , but its casing may not be correct

The contents of workstring are not significant afterwards.

<Parse the path to break it into topdir path, extension directory and leafname 3.7> =

        char *p;
        size_t extdirindex = 0, extindex = 0, namelen = 0, dirlen = 0;

        p = strrchr(path, FOLDER_SEPARATOR);
        if (p) {
            extindex = (size_t) (p - path);
            namelen = length - extindex - 1;
            strncpy(ciextname, path + extindex + 1, namelen);
        }
        ciextname[namelen] = 0;

        strncpy(workstring, path, extindex);
        workstring[extindex] = 0;
        p = strrchr(workstring, FOLDER_SEPARATOR);
        if (p) {
            extdirindex = (size_t) (p - workstring);
            strncpy(topdirpath, path, extdirindex);
        }
        topdirpath[extdirindex] = 0;

        dirlen = extindex - extdirindex;
        if (dirlen > 0) dirlen -= 1;
        strncpy(ciextdirpath, path + extdirindex + 1, dirlen);
        ciextdirpath[dirlen] = 0;

This code is used in §3.

§4. Counting matches. We count the number of names within the directory which case-insensitively match against name, and copy the last which matches into last_match. This must be at least as long as name. (We ought to be just a little careful in case of improbable cases where the matched name contains a different number of characters from name, for instance because on a strict reading of Unicode "SS" is casing-equivalent to the eszet, but it's unlikely that many contemporary implementations of strcasecmp are aware of this, and in any case the code above contains much larger buffers than needed.)

    int CIFilingSystem::match_in_directory(void *vd,
        char *name, char *last_match) {
        DIR *d = (DIR *) vd;
        struct dirent *dirp;
        int rc = 0;

        last_match[0] = 0;
        while ((dirp = readdir(d)) != NULL) {
            if (strcasecmp(name, dirp->d_name) == 0) {
                rc++;
                strcpy(last_match, dirp->d_name);
            }
        }
        return rc;
    }

The function CIFilingSystem::match_in_directory is used in §3.1, §3.2.

§5. Non-POSIX tail. On platforms without POSIX directory handling, we revert to regular fopen.

    FILE *CIFilingSystem::fopen(const char *path, const char *mode) {
        return fopen(path, mode);
    }

This paragraph is used only if PLATFORM_POSIX is undefined.

The function CIFilingSystem::fopen is used in §3, 3/fln (§10).