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.
- §3. The routine
- §3.1. Looking for case-insensitive matches instead
- §3.3. Allocation and deallocation
- §3.6. Pathname hacking
- §4. strrchr
- §5. Counting matches
- §6. Non-POSIX tail
§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:
- (a) If no suitable file was found, errno is set to ENOENT.
- (b) If more than one possibility was found, but none of them exactly match the supplied case, errno is set to EBADF.
- (c) Note that if multiple directories which match case-insensitively are found, but none is an exact match, EBADF will be set regardless of the contents of the directories.
- (d) If CIFilingSystem::fopen() fails during its allocation of space to hold its intermediate strings for comparison, or for its various data structures, errno is set to ENOMEM.
- (e) If an unambiguous filename is found but the fopen() fails, errno is left at whatever value the underlying fopen() set it to.
§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; }
- This paragraph is used only if PLATFORM_POSIX is defined.
- The function CIFilingSystem::fopen is used in §6, Filenames (§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 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; }
- 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 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; }
- 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 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; }
- This code is used in §3.
§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; }
- 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:
- (a) topdirpath is /Users/bobama/Library/Inform/Extensions, and its casing is correct.
- (b) ciextdirpath is Hillary Clinton, but its casing may not be correct.
- (c) 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 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;
- This code is used in §3.
§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); }
- This paragraph is used only if PLATFORM_POSIX is undefined.