nulib2/nulib2/Filename.c

663 lines
20 KiB
C

/*
* NuLib2
* Copyright (C) 2000-2007 by Andy McFadden, All Rights Reserved.
* This is free software; you can redistribute it and/or modify it under the
* terms of the BSD License, see the file COPYING.
*
* Filename manipulation, including file type preservation.
*/
#include "NuLib2.h"
#include <ctype.h>
/*
* ===========================================================================
* Common definitions
* ===========================================================================
*/
#define kPreserveIndic '#' /* use # rather than $ for hex indication */
#define kFilenameExtDelim '.' /* separates extension from filename */
#define kResourceFlag 'r'
#define kDiskImageFlag 'i'
#define kMaxExtLen 5 /* ".1234" */
#define kResourceStr "_rsrc_"
/* must be longer then strlen(kResourceStr)... no problem there */
#define kMaxPathGrowth (sizeof("#XXXXXXXXYYYYYYYYZ")-1 + kMaxExtLen+1)
/* ProDOS file type names; must be entirely in upper case */
static const char gFileTypeNames[256][4] = {
"NON", "BAD", "PCD", "PTX", "TXT", "PDA", "BIN", "FNT",
"FOT", "BA3", "DA3", "WPF", "SOS", "$0D", "$0E", "DIR",
"RPD", "RPI", "AFD", "AFM", "AFR", "SCL", "PFS", "$17",
"$18", "ADB", "AWP", "ASP", "$1C", "$1D", "$1E", "$1F",
"TDM", "$21", "$22", "$23", "$24", "$25", "$26", "$27",
"$28", "$29", "8SC", "8OB", "8IC", "8LD", "P8C", "$2F",
"$30", "$31", "$32", "$33", "$34", "$35", "$36", "$37",
"$38", "$39", "$3A", "$3B", "$3C", "$3D", "$3E", "$3F",
"DIC", "OCR", "FTD", "$43", "$44", "$45", "$46", "$47",
"$48", "$49", "$4A", "$4B", "$4C", "$4D", "$4E", "$4F",
"GWP", "GSS", "GDB", "DRW", "GDP", "HMD", "EDU", "STN",
"HLP", "COM", "CFG", "ANM", "MUM", "ENT", "DVU", "FIN",
"$60", "$61", "$62", "$63", "$64", "$65", "$66", "$67",
"$68", "$69", "$6A", "BIO", "$6C", "TDR", "PRE", "HDV",
"$70", "$71", "$72", "$73", "$74", "$75", "$76", "$77",
"$78", "$79", "$7A", "$7B", "$7C", "$7D", "$7E", "$7F",
"$80", "$81", "$82", "$83", "$84", "$85", "$86", "$87",
"$88", "$89", "$8A", "$8B", "$8C", "$8D", "$8E", "$8F",
"$90", "$91", "$92", "$93", "$94", "$95", "$96", "$97",
"$98", "$99", "$9A", "$9B", "$9C", "$9D", "$9E", "$9F",
"WP ", "$A1", "$A2", "$A3", "$A4", "$A5", "$A6", "$A7",
"$A8", "$A9", "$AA", "GSB", "TDF", "BDF", "$AE", "$AF",
"SRC", "OBJ", "LIB", "S16", "RTL", "EXE", "PIF", "TIF",
"NDA", "CDA", "TOL", "DVR", "LDF", "FST", "$BE", "DOC",
"PNT", "PIC", "ANI", "PAL", "$C4", "OOG", "SCR", "CDV",
"FON", "FND", "ICN", "$CB", "$CC", "$CD", "$CE", "$CF",
"$D0", "$D1", "$D2", "$D3", "$D4", "MUS", "INS", "MDI",
"SND", "$D9", "$DA", "DBM", "$DC", "DDD", "$DE", "$DF",
"LBR", "$E1", "ATK", "$E3", "$E4", "$E5", "$E6", "$E7",
"$E8", "$E9", "$EA", "$EB", "$EC", "$ED", "R16", "PAS",
"CMD", "$F1", "$F2", "$F3", "$F4", "$F5", "$F6", "$F7",
"$F8", "OS ", "INT", "IVR", "BAS", "VAR", "REL", "SYS"
};
/*
* Some file extensions we recognize. When adding files with "extended"
* preservation mode, we try to assign types to files that weren't
* explicitly preserved, but nevertheless have a recognizeable type.
*
* geoff @ gwlink.net points out that this really ought to be in an external
* file rather than a hard-coded table. Ought to fix that someday.
*/
static const struct {
const char* label;
uint8_t fileType;
uint16_t auxType;
} gRecognizedExtensions[] = {
{ "ASM", 0xb0, 0x0003 }, /* APW assembly source */
{ "C", 0xb0, 0x000a }, /* APW C source */
{ "H", 0xb0, 0x000a }, /* APW C header */
{ "CPP", 0xb0, 0x0000 }, /* generic source file */
{ "BNY", 0xe0, 0x8000 }, /* Binary II lib */
{ "BQY", 0xe0, 0x8000 }, /* Binary II lib, w/ compress */
{ "BXY", 0xe0, 0x8000 }, /* Binary II wrap around SHK */
{ "BSE", 0xe0, 0x8000 }, /* Binary II wrap around SEA */
{ "SEA", 0xb3, 0xdb07 }, /* GSHK SEA */
{ "TEXT", 0x04, 0x0000 }, /* ASCII text */
{ "GIF", 0xc0, 0x8006 }, /* GIF image */
{ "JPG", 0x06, 0x0000 }, /* JPEG (nicer than 'NON') */
{ "JPEG", 0x06, 0x0000 }, /* JPEG (nicer than 'NON') */
{ "SHK", 0xe0, 0x8002 }, /* ShrinkIt archive */
};
/*
* Return a pointer to the three-letter representation of the file type name.
*/
const char* GetFileTypeString(uint32_t fileType)
{
if (fileType < NELEM(gFileTypeNames))
return gFileTypeNames[fileType];
else
return "???";
}
/*
* ===========================================================================
* File type preservation
* ===========================================================================
*/
/*
* Add a preservation string.
*
* "pathBuf" is assumed to have enough space to hold the current path
* plus kMaxPathGrowth more. It will be modified in place.
*/
static void AddPreservationString(NulibState* pState,
const NuPathnameProposal* pPathProposal, char* pathBuf)
{
char extBuf[kMaxPathGrowth +1];
const NuRecord* pRecord;
const NuThread* pThread;
NuThreadID threadID;
char* cp;
Assert(pState != NULL);
Assert(pPathProposal != NULL);
Assert(pathBuf != NULL);
Assert(NState_GetModPreserveType(pState));
pRecord = pPathProposal->pRecord;
pThread = pPathProposal->pThread;
Assert(pRecord != NULL);
Assert(pThread != NULL);
cp = extBuf;
/*
* Cons up a preservation string. On some platforms "sprintf" doesn't
* return the #of characters written, so we add it up manually.
*/
if (pRecord->recFileType < 0x100 && pRecord->recExtraType < 0x10000) {
sprintf(cp, "%c%02x%04x", kPreserveIndic, pRecord->recFileType,
pRecord->recExtraType);
cp += 7;
} else {
sprintf(cp, "%c%08x%08x", kPreserveIndic, pRecord->recFileType,
pRecord->recExtraType);
cp += 17;
}
threadID = NuMakeThreadID(pThread->thThreadClass, pThread->thThreadKind);
if (threadID == kNuThreadIDRsrcFork)
*cp++ = kResourceFlag;
else if (threadID == kNuThreadIDDiskImage)
*cp++ = kDiskImageFlag;
/*
* If they've asked for "extended" type preservation, then we need
* to retain either the existing extension or append an extension
* based on the ProDOS file type.
*/
if (NState_GetModPreserveTypeExtended(pState)) {
const char* pExt;
char* end;
/*
* Find extension. Note FindExtension guarantees there's at least
* one char after '.'.
*
* It's hard to know when this is right and when it isn't. It's
* fairly likely that a text file really ought to end in ".txt",
* and it's fairly unlikely that a BIN file should be ".bin", but
* where do the rest fall in? We might want to force TXT files
* to be ".txt", and perhaps do something clever for some others.
*/
if (pRecord->recFileType == 0x04 || threadID == kNuThreadIDDiskImage)
pExt = NULL;
else
pExt = FindExtension(pState, pathBuf);
if (pExt != NULL) {
pExt++; /* skip past the '.' */
if (strlen(pExt) >= kMaxExtLen) {
/* too long, forget it */
pExt = NULL;
} else {
/* if strictly decimal-numeric, don't use it (.1, .2, etc) */
(void) strtoul(pExt, &end, 10);
if (*end == '\0') {
pExt = NULL;
} else {
/* if '#' appears in it, don't use it -- it'll confuse us */
const char* ccp = pExt;
while (*ccp != '\0') {
if (*ccp == '#') {
pExt = NULL;
break;
}
ccp++;
}
}
}
} else {
/*
* There's no extension on the filename. Use the standard
* ProDOS type, if one exists for this entry. We don't use
* the table if it's "NON" or a hex value.
*/
if (threadID == kNuThreadIDDiskImage)
pExt = "PO"; /* indicate it's a ProDOS-ordered image */
else if (pRecord->recFileType) {
pExt = GetFileTypeString(pRecord->recFileType);
if (pExt[0] == '?' || pExt[0] == '$')
pExt = NULL;
}
}
if (pExt != NULL) {
*cp++ = kFilenameExtDelim;
strcpy(cp, pExt);
cp += strlen(pExt);
}
}
/* make sure it's terminated */
*cp = '\0';
Assert(cp - extBuf <= kMaxPathGrowth);
strcat(pathBuf, extBuf);
}
/*
* Normalize a path for the conventions on the output filesystem. This
* adds optional file type preservation.
*
* The path from the archive is in "pPathProposal". Thew new pathname
* will be placed in the "new pathname" section of "pPathProposal".
*
* The new pathname may be shorter (because characters were removed) or
* longer (if we add a "#XXYYYYZ" extension or replace chars with '%' codes).
*
* This returns the new pathname, which is held in NulibState's temporary
* pathname buffer.
*/
const char* NormalizePath(NulibState* pState, NuPathnameProposal* pPathProposal)
{
NuError err = kNuErrNone;
char* pathBuf;
const char* startp;
const char* endp;
char* dstp;
char localFssep;
int newBufLen;
Assert(pState != NULL);
Assert(pPathProposal != NULL);
Assert(pPathProposal->pathnameUNI != NULL);
localFssep = NState_GetSystemPathSeparator(pState);
/*
* Set up temporary buffer space. The maximum possible expansion
* requires converting all chars to '%' codes and adding the longest
* possible preservation string.
*/
newBufLen = strlen(pPathProposal->pathnameUNI)*3 + kMaxPathGrowth +1;
NState_SetTempPathnameLen(pState, newBufLen);
pathBuf = NState_GetTempPathnameBuf(pState);
Assert(pathBuf != NULL);
if (pathBuf == NULL)
return NULL;
startp = pPathProposal->pathnameUNI;
dstp = pathBuf;
while (*startp == pPathProposal->filenameSeparator) {
/* ignore leading path sep; always extract to current dir */
startp++;
}
/* normalize all directory components and the filename component */
while (startp != NULL) {
endp = strchr(startp, pPathProposal->filenameSeparator);
if (endp != NULL) {
/* normalize directory component */
err = NormalizeDirectoryName(pState, startp, endp - startp,
pPathProposal->filenameSeparator, &dstp,
NState_GetTempPathnameLen(pState));
if (err != kNuErrNone)
goto bail;
*dstp++ = localFssep;
startp = endp +1;
} else {
/* normalize filename */
err = NormalizeFileName(pState, startp, strlen(startp),
pPathProposal->filenameSeparator, &dstp,
NState_GetTempPathnameLen(pState));
if (err != kNuErrNone)
goto bail;
/* add/replace extension if necessary */
*dstp++ = '\0';
if (NState_GetModPreserveType(pState)) {
AddPreservationString(pState, pPathProposal, pathBuf);
} else if (NuGetThreadID(pPathProposal->pThread) == kNuThreadIDRsrcFork)
{
#ifndef HAS_RESOURCE_FORKS
/* add this in lieu of the preservation extension */
strcat(pathBuf, kResourceStr);
#endif
}
startp = NULL; /* we're done */
}
}
pPathProposal->newPathnameUNI = pathBuf;
pPathProposal->newFilenameSeparator = localFssep;
/* check for overflow */
Assert(dstp - pathBuf <= newBufLen);
/*
* If "junk paths" is set, drop everything but the last component.
*/
if (NState_GetModJunkPaths(pState)) {
char* lastFssep;
lastFssep = strrchr(pathBuf, localFssep);
if (lastFssep != NULL) {
Assert(*(lastFssep+1) != '\0'); /* should already have been caught*/
memmove(pathBuf, lastFssep+1, strlen(lastFssep+1)+1);
}
}
bail:
if (err != kNuErrNone)
return NULL;
return pathBuf;
}
/*
* ===========================================================================
* File type restoration
* ===========================================================================
*/
/*
* Try to figure out what file type is associated with a filename extension.
*
* This checks the standard list of ProDOS types (which should catch things
* like "TXT" and "BIN") and the separate list of recognized extensions.
*/
static void LookupExtension(NulibState* pState, const char* ext,
uint32_t* pFileType, uint32_t* pAuxType)
{
char uext3[4];
int i, extLen;
extLen = strlen(ext);
Assert(extLen > 0);
/*
* First step is to try to find it in the recognized types list.
*/
for (i = 0; i < NELEM(gRecognizedExtensions); i++) {
if (strcasecmp(ext, gRecognizedExtensions[i].label) == 0) {
*pFileType = gRecognizedExtensions[i].fileType;
*pAuxType = gRecognizedExtensions[i].auxType;
goto bail;
}
}
/*
* Second step is to try to find it in the ProDOS types list.
*
* The extension is converted to upper case and padded with spaces.
*
* [do we want to obstruct matching on things like '$f7' here?]
*/
if (extLen <= 3) {
for (i = 2; i >= extLen; i--)
uext3[i] = ' ';
for ( ; i >= 0; i--)
uext3[i] = toupper(ext[i]);
uext3[3] = '\0';
/*printf("### converted '%s' to '%s'\n", ext, uext3);*/
for (i = 0; i < NELEM(gFileTypeNames); i++) {
if (strcmp(uext3, gFileTypeNames[i]) == 0) {
*pFileType = i;
goto bail;
}
}
}
bail:
return;
}
/*
* Try to associate some meaning with the file extension.
*/
void InterpretExtension(NulibState* pState, const char* pathName,
uint32_t* pFileType, uint32_t* pAuxType)
{
const char* pExt;
Assert(pState != NULL);
Assert(pathName != NULL);
Assert(pFileType != NULL);
Assert(pAuxType != NULL);
pExt = FindExtension(pState, pathName);
if (pExt != NULL)
LookupExtension(pState, pExt+1, pFileType, pAuxType);
}
/*
* Check to see if there's a preservation string on the filename. If so,
* set the filetype and auxtype information, and trim the preservation
* string off.
*
* We have to be careful not to trip on false-positive occurrences of '#'
* in the filename.
*/
Boolean ExtractPreservationString(NulibState* pState, char* pathname,
uint32_t* pFileType, uint32_t* pAuxType, NuThreadID* pThreadID)
{
char numBuf[9];
uint32_t fileType, auxType;
NuThreadID threadID;
char* pPreserve;
char* cp;
int digitCount;
Assert(pState != NULL);
Assert(pathname != NULL);
Assert(pFileType != NULL);
Assert(pAuxType != NULL);
Assert(pThreadID != NULL);
pPreserve = strrchr(pathname, kPreserveIndic);
if (pPreserve == NULL)
return false;
/* count up the #of hex digits */
digitCount = 0;
for (cp = pPreserve+1; *cp != '\0' && isxdigit((int)*cp); cp++)
digitCount++;
/* extract the file and aux type */
switch (digitCount) {
case 6:
/* ProDOS 1-byte type and 2-byte aux */
memcpy(numBuf, pPreserve+1, 2);
numBuf[2] = 0;
fileType = strtoul(numBuf, &cp, 16);
Assert(cp == numBuf + 2);
auxType = strtoul(pPreserve+3, &cp, 16);
Assert(cp == pPreserve + 7);
break;
case 16:
/* HFS 4-byte type and 4-byte creator */
memcpy(numBuf, pPreserve+1, 8);
numBuf[8] = 0;
fileType = strtoul(numBuf, &cp, 16);
Assert(cp == numBuf + 8);
auxType = strtoul(pPreserve+9, &cp, 16);
Assert(cp == pPreserve + 17);
break;
default:
/* not valid */
return false;
}
/* check for a threadID specifier */
threadID = kNuThreadIDDataFork;
switch (*cp) {
case kResourceFlag:
threadID = kNuThreadIDRsrcFork;
cp++;
break;
case kDiskImageFlag:
threadID = kNuThreadIDDiskImage;
cp++;
break;
default:
/* do nothing... yet */
break;
}
/* make sure we were the very last component */
switch (*cp) {
case kFilenameExtDelim: /* redundant "-ee" extension */
case '\0': /* end of string! */
break;
default:
return false;
}
/* truncate the original string, and return what we got */
*pPreserve = '\0';
*pFileType = fileType;
*pAuxType = auxType;
*pThreadID = threadID;
return true;
}
/*
* Remove NuLib2's normalization magic (e.g. "%2f" for '/').
*
* This always results in the filename staying the same length or getting
* smaller, so we can do it in place in the buffer.
*/
void DenormalizePath(NulibState* pState, char* pathBuf)
{
const char* srcp;
char* dstp;
char ch;
srcp = pathBuf;
dstp = pathBuf;
while (*srcp != '\0') {
if (*srcp == kForeignIndic) {
srcp++;
if (*srcp == kForeignIndic) {
*dstp++ = kForeignIndic;
srcp++;
} else if (isxdigit((int)*srcp)) {
ch = HexDigit(*srcp) << 4;
srcp++;
if (isxdigit((int)*srcp)) {
/* valid, output char */
ch += HexDigit(*srcp);
if (ch != '\0') /* used by Win32 converter */
*dstp++ = ch;
srcp++;
} else {
/* bogus '%' with trailing hex digit found! */
*dstp++ = kForeignIndic;
*dstp++ = *(srcp-1);
}
} else {
/* bogus lone '%s' found! */
*dstp++ = kForeignIndic;
}
} else {
*dstp++ = *srcp++;
}
}
*dstp = '\0';
Assert(dstp <= srcp);
}
/*
* ===========================================================================
* Misc utils
* ===========================================================================
*/
/*
* Find the filename component of a local pathname. Uses the fssep defined
* in pState.
*
* Always returns a pointer to a string; never returns NULL.
*/
const char* FilenameOnly(NulibState* pState, const char* pathname)
{
const char* retstr;
const char* pSlash;
char* tmpStr = NULL;
Assert(pState != NULL);
Assert(pathname != NULL);
pSlash = strrchr(pathname, NState_GetSystemPathSeparator(pState));
if (pSlash == NULL) {
retstr = pathname; /* whole thing is the filename */
goto bail;
}
pSlash++;
if (*pSlash == '\0') {
if (strlen(pathname) < 2) {
retstr = pathname; /* the pathname is just "/"? Whatever */
goto bail;
}
/* some bonehead put an fssep on the very end; back up before it */
/* (not efficient, but this should be rare, and I'm feeling lazy) */
tmpStr = strdup(pathname);
tmpStr[strlen(pathname)-1] = '\0';
pSlash = strrchr(tmpStr, NState_GetSystemPathSeparator(pState));
if (pSlash == NULL) {
retstr = pathname; /* just a filename with a '/' after it */
goto bail;
}
pSlash++;
if (*pSlash == '\0') {
retstr = pathname; /* I give up! */
goto bail;
}
retstr = pathname + (pSlash - tmpStr);
} else {
retstr = pSlash;
}
bail:
Free(tmpStr);
return retstr;
}
/*
* Return the filename extension found in a full pathname.
*
* An extension is the stuff following the last '.' in the filename. If
* there is nothing following the last '.', then there is no extension.
*
* Returns a pointer to the '.' preceding the extension, or NULL if no
* extension was found.
*/
const char* FindExtension(NulibState* pState, const char* pathname)
{
const char* pFilename;
const char* pExt;
/*
* We have to isolate the filename so that we don't get excited
* about "/foo.bar/file".
*/
pFilename = FilenameOnly(pState, pathname);
Assert(pFilename != NULL);
pExt = strrchr(pFilename, kFilenameExtDelim);
/* also check for "/blah/foo.", which doesn't count */
if (pExt != NULL && *(pExt+1) != '\0')
return pExt;
return NULL;
}