inweb-bootstrap/foundation-module/Chapter 3/Case-Insensitive Filenames.w

247 lines
9.3 KiB
OpenEdge ABL

[CIFilingSystem::] Case-Insensitive Filenames.
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.
@ 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.
@ 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.
@h The routine. ^"ifdef-PLATFORM_POSIX"
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@>;
@<Find the length of the path, giving an error if it is empty or NULL@>;
@<Allocate memory for strings large enough to hold any subpath of the path@>;
@<Parse the path to break it into topdir path, extension directory and leafname@>;
topdir = opendir(topdirpath); /* whose pathname is assumed case-correct... */
if (topdir == NULL) @<Sad ending to ci-fopen@>; /* ...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@>
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@>;
@<Try to find a unique insensitively matching entry in extdir@>;
}
@h 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@> =
int rc = CIFilingSystem::match_in_directory(topdir, ciextdirpath, workstring);
switch (rc) {
case 0:
errno = ENOENT; @<Sad ending to ci-fopen@>;
case 1:
sprintf(cistring, "%s%c%s", topdirpath, FOLDER_SEPARATOR, workstring);
extdir = opendir(cistring);
if (extdir == NULL) {
errno = ENOENT; @<Sad ending to ci-fopen@>;
}
break;
default:
errno = EBADF; @<Sad ending to ci-fopen@>;
}
@ 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@> =
int rc = CIFilingSystem::match_in_directory(extdir, ciextname, workstring);
switch (rc) {
case 0:
errno = ENOENT; @<Sad ending to ci-fopen@>;
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@>;
errno = ENOENT; @<Sad ending to ci-fopen@>;
default:
errno = EBADF; @<Sad ending to ci-fopen@>;
}
@h 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@> =
workstring = calloc(length+1, sizeof(char));
if (workstring == NULL) { errno = ENOMEM; @<Sad ending to ci-fopen@>; }
workstring2 = calloc(length+1, sizeof(char));
if (workstring2 == NULL) { errno = ENOMEM; @<Sad ending to ci-fopen@>; }
topdirpath = calloc(length+1, sizeof(char));
if (topdirpath == NULL) { errno = ENOMEM; @<Sad ending to ci-fopen@>; }
ciextdirpath = calloc(length+1, sizeof(char));
if (ciextdirpath == NULL) { errno = ENOMEM; @<Sad ending to ci-fopen@>; }
cistring = calloc(length+1, sizeof(char));
if (cistring == NULL) { errno = ENOMEM; @<Sad ending to ci-fopen@>; }
ciextname = calloc(length+1, sizeof(char));
if (ciextname == NULL) { errno = ENOMEM; @<Sad ending to ci-fopen@>; }
@ If we are successful, we return a valid file handle...
@<Happy ending to ci-fopen@> =
@<Prepare to exit ci-fopen cleanly@>;
return handle;
@ ...and otherwise |NULL|, having already set |errno| with the reason why.
@<Sad ending to ci-fopen@> =
@<Prepare to exit ci-fopen cleanly@>;
return NULL;
@<Prepare to exit ci-fopen cleanly@> =
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);
@h Pathname hacking.
@<Find the length of the path, giving an error if it is empty or NULL@> =
length = 0;
if (path) length = (size_t) strlen(path);
if (length < 1) { errno = ENOENT; return NULL; }
@ And here we break up a pathname like
= (text)
/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 leafname@> =
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;
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;
@h 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((wchar_t) (*p))) q = p;
p++;
}
return (char *) q;
}
@h 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;
}
@h Non-POSIX tail. ^"ifndef-PLATFORM_POSIX"
On platforms without POSIX directory handling, we revert to regular |fopen|.
=
FILE *CIFilingSystem::fopen(const char *path, const char *mode) {
return fopen(path, mode);
}