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-fopen3.4;

    Find the length of the path, giving an error if it is empty or NULL3.6;
    Allocate memory for strings large enough to hold any subpath of the path3.3;
    Parse the path to break it into topdir path, extension directory and leafname3.7;

    topdir = opendir(topdirpath);  whose pathname is assumed case-correct...
    if (topdir == NULL) Sad ending to ci-fopen3.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 topdir3.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-fopen3.4;

    Try to find a unique insensitively matching entry in extdir3.2;
}

§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 topdir3.1 =

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

§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 extdir3.2 =

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

    switch (rc) {
        case 0:
            errno = ENOENT; Sad ending to ci-fopen3.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-fopen3.4;
            errno = ENOENT; Sad ending to ci-fopen3.5;
        default:
            errno = EBADF; Sad ending to ci-fopen3.5;
    }

§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 path3.3 =

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

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

Happy ending to ci-fopen3.4 =

    Prepare to exit ci-fopen cleanly3.4.1;
    return handle;

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

Sad ending to ci-fopen3.5 =

    Prepare to exit ci-fopen cleanly3.4.1;
    return NULL;

§3.4.1. Prepare to exit ci-fopen cleanly3.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);

§3.6. Pathname hacking. Find the length of the path, giving an error if it is empty or NULL3.6 =

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

§3.7. And here we break up a pathname like

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

into three components:

The contents of workstring are not significant afterwards.

Parse the path to break it into topdir path, extension directory and leafname3.7 =

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

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

    if (extindex > 0) strncpy(workstring, path, extindex);
    workstring[extindex] = 0;
    p = CIFilingSystem::strrchr(workstring);
    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;

§4. strrchr. This is an elderly C library function, really, but rewritten so that it can recognise any folder separator character.

char *CIFilingSystem::strrchr(const char *p) {
    const char *q = NULL;
    while (*p) {
        if (Platform::is_folder_separator((inchar32_t) (*p))) q = p;
        p++;
    }
    return (char *) q;
}

§5. 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;
}

§6. 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);
}