/* * 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 /* * =========================================================================== * 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; }