/* * CiderPress * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. * See the file LICENSE for distribution terms. */ /* * Implementation of the Macintosh HFS filesystem. * * Most of the stuff lives in libhfs. To avoid problems that could arise * from people ejecting floppies or trying to use a disk image while * CiderPress is still open, we call hfs_flush() to force updates to be * written. (Even with the "no caching" flag set, the master dir block and * volume bitmap aren't written until flush is called.) * * The libhfs code is licensed under the full GPL, making it awkward to * use in a commercial product. Support for libhfs can be removed with * the EXCISE_GPL_CODE ifdefs. A stub will remain that can recognize HFS * volumes, which is useful when dealing with Apple II hard drive and CFFA * images. */ #include "StdAfx.h" #include "DiskImgPriv.h" /* * =========================================================================== * DiskFSHFS * =========================================================================== */ const int kBlkSize = 512; const int kMasterDirBlock = 2; // also a copy in next-to-last block const uint16_t kSignature = 0x4244; // or 0xd2d7 for MFS const int kMaxDirectoryDepth = 128; // not sure what HFS limit is //namespace DiskImgLib { /* extent descriptor */ typedef struct ExtDescriptor { uint16_t xdrStABN; // first allocation block uint16_t xdrNumABlks; // #of allocation blocks } ExtDescriptor; /* extent data record */ typedef struct ExtDataRec { ExtDescriptor extDescriptor[3]; } ExtDataRec; /* * Contents of the HFS MDB. Information comes from "Inside Macintosh: Files", * chapter 2 ("Data Organization on Volumes"), pages 2-60 to 2-62. */ typedef struct DiskFSHFS::MasterDirBlock { uint16_t drSigWord; // volume signature uint32_t drCrDate; // date/time of volume creation uint32_t drLsMod; // date/time of last modification uint16_t drAtrb; // volume attributes uint16_t drNmPls; // #of files in root directory uint16_t drVBMSt; // first block of volume bitmap uint16_t drAllocPtr; // start of next allocation search uint16_t drNmAlBlks; // number of allocation blocks in volume uint32_t drAlBlkSiz; // size (bytes) of allocation blocks uint32_t drClpSiz; // default clump size uint16_t drAlBlSt; // first allocation block in volume uint32_t drNxtCNID; // next unused catalog node ID uint16_t drFreeBks; // number of unused allocation blocks uint8_t drVN[28]; // volume name (pascal string) uint32_t drVolBkUp; // date/time of last backup uint16_t drVSeqNum; // volume backup sequence number uint32_t drWrCnt; // volume write count uint32_t drXTClpSiz; // clump size for extents overflow file uint32_t drCTClpSiz; // clump size for catalog file uint16_t drNmRtDirs; // #of directories in root directory uint32_t drFilCnt; // #of files in volume uint32_t drDirCnt; // #of directories in volume uint32_t drFndrInfo[8]; // information used by the Finder uint16_t drVCSize; // size (blocks) of volume cache uint16_t drVBMCSize; // size (blocks) of volume bitmap cache uint16_t drCtlCSize; // size (blocks) of common volume cache uint32_t drXTFlSize; // size (bytes) of extents overflow file ExtDataRec drXTExtRec; // extent record for extents overflow file uint32_t drCTFlSize; // size (bytes) of catalog file ExtDataRec drCTExtRec; // extent record for catalog file } MasterDirBlock; //}; // namespace DiskImgLib /* * Extract fields from a Master Directory Block. */ /*static*/ void DiskFSHFS::UnpackMDB(const uint8_t* buf, MasterDirBlock* pMDB) { pMDB->drSigWord = GetShortBE(&buf[0x00]); pMDB->drCrDate = GetLongBE(&buf[0x02]); pMDB->drLsMod = GetLongBE(&buf[0x06]); pMDB->drAtrb = GetShortBE(&buf[0x0a]); pMDB->drNmPls = GetShortBE(&buf[0x0c]); pMDB->drVBMSt = GetShortBE(&buf[0x0e]); pMDB->drAllocPtr = GetShortBE(&buf[0x10]); pMDB->drNmAlBlks = GetShortBE(&buf[0x12]); pMDB->drAlBlkSiz = GetLongBE(&buf[0x14]); pMDB->drClpSiz = GetLongBE(&buf[0x18]); pMDB->drAlBlSt = GetShortBE(&buf[0x1c]); pMDB->drNxtCNID = GetLongBE(&buf[0x1e]); pMDB->drFreeBks = GetShortBE(&buf[0x22]); memcpy(pMDB->drVN, &buf[0x24], sizeof(pMDB->drVN)); pMDB->drVolBkUp = GetLongBE(&buf[0x40]); pMDB->drVSeqNum = GetShortBE(&buf[0x44]); pMDB->drWrCnt = GetLongBE(&buf[0x46]); pMDB->drXTClpSiz = GetLongBE(&buf[0x4a]); pMDB->drCTClpSiz = GetLongBE(&buf[0x4e]); pMDB->drNmRtDirs = GetShortBE(&buf[0x52]); pMDB->drFilCnt = GetLongBE(&buf[0x54]); pMDB->drDirCnt = GetLongBE(&buf[0x58]); for (int i = 0; i < (int) NELEM(pMDB->drFndrInfo); i++) pMDB->drFndrInfo[i] = GetLongBE(&buf[0x5c + i * 4]); pMDB->drVCSize = GetShortBE(&buf[0x7c]); pMDB->drVBMCSize = GetShortBE(&buf[0x7e]); pMDB->drCtlCSize = GetShortBE(&buf[0x80]); pMDB->drXTFlSize = GetLongBE(&buf[0x82]); //UnpackExtDataRec(&pMDB->drXTExtRec, &buf[0x86]); // 12 bytes pMDB->drCTFlSize = GetLongBE(&buf[0x92]); //UnpackExtDataRec(&pMDB->drXTExtRec, &buf[0x96]); // next field at 0xa2 } /* * See if this looks like an HFS volume. * * We test a few fields in the master directory block for validity. */ /*static*/ DIError DiskFSHFS::TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder) { DIError dierr = kDIErrNone; MasterDirBlock mdb; uint8_t blkBuf[kBlkSize]; dierr = pImg->ReadBlockSwapped(kMasterDirBlock, blkBuf, imageOrder, DiskImg::kSectorOrderProDOS); if (dierr != kDIErrNone) goto bail; UnpackMDB(blkBuf, &mdb); if (mdb.drSigWord != kSignature) { dierr = kDIErrFilesystemNotFound; goto bail; } if ((mdb.drAlBlkSiz & 0x1ff) != 0) { // allocation block size must be a multiple of 512 LOGI(" HFS: found allocation block size = %u, rejecting", mdb.drAlBlkSiz); dierr = kDIErrFilesystemNotFound; goto bail; } if (mdb.drVN[0] == 0 || mdb.drVN[0] > kMaxVolumeName) { LOGI(" HFS: volume name has len = %d, rejecting", mdb.drVN[0]); dierr = kDIErrFilesystemNotFound; goto bail; } long minBlocks; minBlocks = mdb.drNmAlBlks * (mdb.drAlBlkSiz / kBlkSize) + mdb.drAlBlSt + 2; if (minBlocks > pImg->GetNumBlocks()) { // We're probably trying to open a 1GB volume as if it were only // 32MB. Maybe this is a full HFS partition and we're trying to // see if it's a CFFA image. Whatever the case, we can't do this. LOGI("HFS: volume exceeds disk image size (%ld vs %ld)", minBlocks, pImg->GetNumBlocks()); dierr = kDIErrFilesystemNotFound; goto bail; } // looks good! bail: return dierr; } /* * Test to see if the image is an HFS disk. */ /*static*/ DIError DiskFSHFS::TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, DiskImg::FSFormat* pFormat, FSLeniency leniency) { //return kDIErrFilesystemNotFound; // DEBUG DEBUG DEBUG /* must be block format, should be at least 720K */ if (!pImg->GetHasBlocks() || pImg->GetNumBlocks() < kExpectedMinBlocks) return kDIErrFilesystemNotFound; DiskImg::SectorOrder ordering[DiskImg::kSectorOrderMax]; DiskImg::GetSectorOrderArray(ordering, *pOrder); for (int i = 0; i < DiskImg::kSectorOrderMax; i++) { if (ordering[i] == DiskImg::kSectorOrderUnknown) continue; if (TestImage(pImg, ordering[i]) == kDIErrNone) { *pOrder = ordering[i]; *pFormat = DiskImg::kFormatMacHFS; return kDIErrNone; } } LOGI(" HFS didn't find valid FS"); return kDIErrFilesystemNotFound; } /* * Load some stuff from the volume header. */ DIError DiskFSHFS::LoadVolHeader(void) { DIError dierr = kDIErrNone; MasterDirBlock mdb; uint8_t blkBuf[kBlkSize]; if (fLocalTimeOffset == -1) { struct tm* ptm; struct tm tmWhen; time_t when; int isDst; when = time(NULL); isDst = localtime(&when)->tm_isdst; ptm = gmtime(&when); if (ptm != NULL) { tmWhen = *ptm; // make a copy -- static buffers in time functions tmWhen.tm_isdst = isDst; fLocalTimeOffset = (long) (when - mktime(&tmWhen)); } else fLocalTimeOffset = 0; LOGI(" HFS computed local time offset = %.3f hours", fLocalTimeOffset / 3600.0); } dierr = fpImg->ReadBlock(kMasterDirBlock, blkBuf); if (dierr != kDIErrNone) goto bail; UnpackMDB(blkBuf, &mdb); /* * The minimum size of the volume is "number of allocation blocks" plus * "first allocation block" (to avoid the OS overhead) plus 2 (because * there's a backup copy of the MDB in the next-to-last block, and * nothing at all in the very last block). * * This isn't the total size, because on larger volumes there can be * some padding between the last usable block and the backup MDB. The * only way to find the MDB is to take the DiskImg's block size and * subtract 2. */ assert((mdb.drAlBlkSiz % kBlkSize) == 0); fNumAllocationBlocks = mdb.drNmAlBlks; fAllocationBlockSize = mdb.drAlBlkSiz; fTotalBlocks = fpImg->GetNumBlocks(); uint32_t minBlocks; minBlocks = mdb.drNmAlBlks * (mdb.drAlBlkSiz / kBlkSize) + mdb.drAlBlSt + 2; assert(fTotalBlocks >= minBlocks); // verified during fs tests int volNameLen; volNameLen = mdb.drVN[0]; if (volNameLen > kMaxVolumeName) { assert(false); // should've been trapped earlier volNameLen = kMaxVolumeName; } memcpy(fVolumeName, &mdb.drVN[1], volNameLen); fVolumeName[volNameLen] = '\0'; SetVolumeID(); fNumFiles = mdb.drFilCnt; fNumDirectories = mdb.drDirCnt; fCreatedDateTime = mdb.drCrDate; fModifiedDateTime = mdb.drLsMod; /* * Create a "magic" directory entry for the volume directory. This * must come first in the file list. */ A2FileHFS* pFile; pFile = new A2FileHFS(this); if (pFile == NULL) { dierr = kDIErrMalloc; goto bail; } pFile->fIsDir = true; pFile->fIsVolumeDir = true; pFile->fType = 0; pFile->fCreator = 0; strcpy(pFile->fFileName, fVolumeName); // vol names are shorter than pFile->SetPathName(":", fVolumeName); // filenames, so it fits pFile->fDataLength = 0; pFile->fRsrcLength = -1; pFile->fCreateWhen = (time_t) (fCreatedDateTime - kDateTimeOffset) - fLocalTimeOffset; pFile->fModWhen = (time_t) (fModifiedDateTime - kDateTimeOffset) - fLocalTimeOffset; pFile->fAccess = DiskFS::kFileAccessUnlocked; //LOGI("GOT *** '%s' '%s'", pFile->fFileName, pFile->fPathName); AddFileToList(pFile); bail: return dierr; } /* * Set the volume ID based on fVolumeName. */ void DiskFSHFS::SetVolumeID(void) { strcpy(fVolumeID, "HFS "); strcat(fVolumeID, fVolumeName); } /* * Blank out the volume usage map. The HFS volume bitmap is not yet supported. */ void DiskFSHFS::SetVolumeUsageMap(void) { VolumeUsage::ChunkState cstate; long block; fVolumeUsage.Create(fpImg->GetNumBlocks()); cstate.isUsed = true; cstate.isMarkedUsed = true; cstate.purpose = VolumeUsage::kChunkPurposeUnknown; for (block = fTotalBlocks-1; block >= 0; block--) fVolumeUsage.SetChunkState(block, &cstate); } /* * Print some interesting fields to the debug log. */ void DiskFSHFS::DumpVolHeader(void) { LOGI("HFS volume header read:"); LOGI(" volume name = '%s'", fVolumeName); LOGI(" total blocks = %d (allocSize=%d [x%u], numAllocs=%u)", fTotalBlocks, fAllocationBlockSize, fAllocationBlockSize / kBlkSize, fNumAllocationBlocks); LOGI(" num directories=%d, num files=%d", fNumDirectories, fNumFiles); time_t when; when = (time_t) (fCreatedDateTime - kDateTimeOffset - fLocalTimeOffset); LOGI(" cre date=0x%08x %.24s", fCreatedDateTime, ctime(&when)); when = (time_t) (fModifiedDateTime - kDateTimeOffset - fLocalTimeOffset); LOGI(" mod date=0x%08x %.24s", fModifiedDateTime, ctime(&when)); } #ifndef EXCISE_GPL_CODE /* * Get things rolling. * * Since we're assured that this is a valid disk, errors encountered from here * on out must be handled somehow, possibly by claiming that the disk is * completely full and has no files on it. */ DIError DiskFSHFS::Initialize(InitMode initMode) { DIError dierr = kDIErrNone; char msg[kMaxVolumeName + 32]; dierr = LoadVolHeader(); if (dierr != kDIErrNone) goto bail; DumpVolHeader(); if (initMode == kInitHeaderOnly) { LOGI(" HFS - headerOnly set, skipping file load"); goto bail; } sprintf(msg, "Scanning %s", fVolumeName); if (!fpImg->UpdateScanProgress(msg)) { LOGI(" HFS cancelled by user"); dierr = kDIErrCancelled; goto bail; } /* * Open the volume with libhfs. We used to set HFS_OPT_NOCACHE to avoid * consistency problems and reduce the risk of disk corruption should * CiderPress fail, but it turns out libhfs doesn't write the volume * bitmap or master dir block unless explicitly flushed anyway. Since * the caching helps us a lot when just reading -- 4 seconds vs. 9 for * a CD-ROM over gigabit Ethernet -- we leave it on, and explicitly * flush every time we make a change. */ fHfsVol = hfs_callback_open(LibHFSCB, this, /*HFS_OPT_NOCACHE |*/ (fpImg->GetReadOnly() ? HFS_MODE_RDONLY : HFS_MODE_RDWR)); if (fHfsVol == NULL) { LOGI("ERROR: hfs_opencallback failed: %s", hfs_error); return kDIErrGeneric; } /* volume dir is guaranteed to come first; if not, we need a lookup func */ A2FileHFS* pVolumeDir; pVolumeDir = (A2FileHFS*) GetNextFile(NULL); dierr = RecursiveDirAdd(pVolumeDir, ":", 0); if (dierr != kDIErrNone) goto bail; SetVolumeUsageMap(); /* * Make sure there's nothing lingering. libhfs will fiddle around with * the MDB if it looks like the volume wasn't unmounted cleanly last time. */ hfs_flush(fHfsVol); bail: return dierr; } /* * Callback function from libhfs. Can read/write/seek. * * This is a little clumsy, but it allows us to maintain a separation from * the libhfs code (which is GPLed). * * Returns -1 on failure. */ unsigned long DiskFSHFS::LibHFSCB(void* vThis, int op, unsigned long arg1, void* arg2) { DiskFSHFS* pThis = (DiskFSHFS*) vThis; unsigned long result = (unsigned long) -1; assert(pThis != NULL); switch (op) { case HFS_CB_VOLSIZE: //LOGI(" HFSCB vol size = %ld blocks", pThis->fTotalBlocks); result = pThis->fTotalBlocks; break; case HFS_CB_READ: // arg1=block, arg2=buffer //LOGI(" HFSCB read block %lu", arg1); if (arg1 < pThis->fTotalBlocks && arg2 != NULL) { DIError err = pThis->fpImg->ReadBlock(arg1, arg2); if (err == kDIErrNone) result = 0; else { LOGI(" HFSCB read %lu failed", arg1); } } break; case HFS_CB_WRITE: LOGI(" HFSCB write block %lu", arg1); if (arg1 < pThis->fTotalBlocks && arg2 != NULL) { DIError err = pThis->fpImg->WriteBlock(arg1, arg2); if (err == kDIErrNone) result = 0; else { LOGI(" HFSCB write %lu failed", arg1); } } break; case HFS_CB_SEEK: // arg1=block, arg2=unused /* just verify that the seek is legal */ //LOGI(" HFSCB seek block %lu", arg1); if (arg1 < pThis->fTotalBlocks) result = arg1; break; default: assert(false); } //LOGI("--- HFSCB returning %lu", result); return result; } /* * Determine the amount of free space on the disk. */ DIError DiskFSHFS::GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, int* pUnitSize) const { assert(fHfsVol != NULL); hfsvolent volEnt; if (hfs_vstat(fHfsVol, &volEnt) != 0) return kDIErrGeneric; *pTotalUnits = volEnt.totbytes / 512; *pFreeUnits = volEnt.freebytes / 512; *pUnitSize = 512; return kDIErrNone; } /* * Recursively traverse the filesystem. */ DIError DiskFSHFS::RecursiveDirAdd(A2File* pParent, const char* basePath, int depth) { DIError dierr = kDIErrNone; hfsdir* dir; hfsdirent dirEntry; char* pathBuf = NULL; int nameOffset; /* if we get too deep, assume it's a loop */ if (depth > kMaxDirectoryDepth) { dierr = kDIErrDirectoryLoop; goto bail; } //LOGI(" HFS RecursiveDirAdd '%s'", basePath); dir = hfs_opendir(fHfsVol, basePath); if (dir == NULL) { printf(" HFS unable to open dir '%s'\n", basePath); LOGI(" HFS unable to open dir '%s'", basePath); dierr = kDIErrGeneric; goto bail; } if (strcmp(basePath, ":") == 0) basePath = ""; nameOffset = strlen(basePath) +1; pathBuf = new char[nameOffset + A2FileHFS::kMaxFileName +1]; if (pathBuf == NULL) { dierr = kDIErrMalloc; goto bail; } strcpy(pathBuf, basePath); pathBuf[nameOffset-1] = A2FileHFS::kFssep; pathBuf[nameOffset] = '\0'; // not needed while (hfs_readdir(dir, &dirEntry) != -1) { A2FileHFS* pFile; pFile = new A2FileHFS(this); pFile->InitEntry(&dirEntry); pFile->SetPathName(basePath, pFile->fFileName); pFile->SetParent(pParent); AddFileToList(pFile); if (!fpImg->UpdateScanProgress(NULL)) { LOGI(" HFS cancelled by user"); dierr = kDIErrCancelled; goto bail; } if (dirEntry.flags & HFS_ISDIR) { strcpy(pathBuf + nameOffset, dirEntry.name); dierr = RecursiveDirAdd(pFile, pathBuf, depth+1); if (dierr != kDIErrNone) goto bail; } } bail: delete[] pathBuf; return dierr; } /* * Initialize an A2FileHFS structure from the stuff in an hfsdirent. */ void A2FileHFS::InitEntry(const hfsdirent* dirEntry) { //printf("--- File '%s' flags=0x%08x fdflags=0x%08x type='%s'\n", // dirEntry.name, dirEntry.flags, dirEntry.fdflags, // dirEntry.u.file.type); fIsVolumeDir = false; memcpy(fFileName, dirEntry->name, A2FileHFS::kMaxFileName+1); fFileName[A2FileHFS::kMaxFileName] = '\0'; // make sure if (dirEntry->flags & HFS_ISLOCKED) fAccess = DiskFS::kFileAccessLocked; else fAccess = DiskFS::kFileAccessUnlocked; if (dirEntry->fdflags & HFS_FNDR_ISINVISIBLE) fAccess |= A2FileProDOS::kAccessInvisible; if (dirEntry->flags & HFS_ISDIR) { fIsDir = true; fType = fCreator = 0; fDataLength = 0; fRsrcLength = -1; } else { uint8_t* pType; fIsDir = false; pType = (uint8_t*) dirEntry->u.file.type; fType = pType[0] << 24 | pType[1] << 16 | pType[2] << 8 | pType[3]; pType = (uint8_t*) dirEntry->u.file.creator; fCreator = pType[0] << 24 | pType[1] << 16 | pType[2] << 8 | pType[3]; fDataLength = dirEntry->u.file.dsize; fRsrcLength = dirEntry->u.file.rsize; /* * Resource fork must be at least 512 bytes for Finder, so if * it has zero length then the file must not have one. */ if (fRsrcLength == 0) fRsrcLength = -1; } /* * Create/modified dates (we ignore the "last backup" date). The * hfslib functions convert to time_t for us. */ fCreateWhen = dirEntry->crdate; fModWhen = dirEntry->mddate; } /* * Return "true" if "name" is valid for use as an HFS volume name. */ /*static*/ bool DiskFSHFS::IsValidVolumeName(const char* name) { if (name == NULL) return false; int len = strlen(name); if (len < 1 || len > kMaxVolumeName) return false; while (*name != '\0') { if (*name == A2FileHFS::kFssep) return false; name++; } return true; } /* * Return "true" if "name" is valid for use as an HFS file name. */ /*static*/ bool DiskFSHFS::IsValidFileName(const char* name) { if (name == NULL) return false; int len = strlen(name); if (len < 1 || len > A2FileHFS::kMaxFileName) return false; while (*name != '\0') { if (*name == A2FileHFS::kFssep) return false; name++; } return true; } /* * Format the current volume with HFS. */ DIError DiskFSHFS::Format(DiskImg* pDiskImg, const char* volName) { assert(strlen(volName) > 0 && strlen(volName) <= kMaxVolumeName); if (!IsValidVolumeName(volName)) return kDIErrInvalidArg; /* set fpImg so calls that rely on it will work; we un-set it later */ assert(fpImg == NULL); SetDiskImg(pDiskImg); /* need this for callback function */ fTotalBlocks = fpImg->GetNumBlocks(); // need HFS_OPT_2048 for CD-ROM? if (hfs_callback_format(LibHFSCB, this, 0, volName) != 0) { LOGI("hfs_callback_format failed (%s)", hfs_error); return kDIErrGeneric; } // no need to flush; HFS volume is closed SetDiskImg(NULL); // shouldn't really be set by us return kDIErrNone; } /* * Normalize an HFS path. Invokes DoNormalizePath and handles the buffer * management (if the normalized path doesn't fit in "*pNormalizedBufLen" * bytes, we set "*pNormalizedBufLen to the required length). * * This is invoked from the generalized "add" function in CiderPress, which * doesn't want to understand the ins and outs of pathnames. */ DIError DiskFSHFS::NormalizePath(const char* path, char fssep, char* normalizedBuf, int* pNormalizedBufLen) { DIError dierr = kDIErrNone; char* normalizedPath = NULL; int len; assert(pNormalizedBufLen != NULL); assert(normalizedBuf != NULL || *pNormalizedBufLen == 0); dierr = DoNormalizePath(path, fssep, &normalizedPath); if (dierr != kDIErrNone) goto bail; assert(normalizedPath != NULL); len = strlen(normalizedPath); if (normalizedBuf == NULL || *pNormalizedBufLen <= len) { /* too short */ dierr = kDIErrDataOverrun; } else { /* fits */ strcpy(normalizedBuf, normalizedPath); } *pNormalizedBufLen = len+1; // alloc room for the '\0' bail: delete[] normalizedPath; return dierr; } /* * Normalize an HFS path. This requires separating each path component * out, making it HFS-compliant, and then putting it back in. * The fssep could be anything, so we need to change it to kFssep. * * The caller must delete[] "*pNormalizedPath". */ DIError DiskFSHFS::DoNormalizePath(const char* path, char fssep, char** pNormalizedPath) { DIError dierr = kDIErrNone; char* workBuf = NULL; char* partBuf = NULL; char* outputBuf = NULL; char* start; char* end; char* outPtr; assert(path != NULL); workBuf = new char[strlen(path)+1]; partBuf = new char[strlen(path)+1 +1]; // need +1 for prepending letter outputBuf = new char[strlen(path) * 2]; if (workBuf == NULL || partBuf == NULL || outputBuf == NULL) { dierr = kDIErrMalloc; goto bail; } strcpy(workBuf, path); outputBuf[0] = '\0'; outPtr = outputBuf; start = workBuf; while (*start != '\0') { //char* origStart = start; // need for debug msg int partIdx; if (fssep == '\0') { end = NULL; } else { end = strchr(start, fssep); if (end != NULL) *end = '\0'; } partIdx = 0; /* * Copy, converting colons to underscores. We should strip out any * illegal characters here, but there's not much in HFS that's * considered illegal. */ while (*start != '\0') { if (*start == A2FileHFS::kFssep) partBuf[partIdx++] = '_'; else partBuf[partIdx++] = *start; start++; } /* * Truncate at 31 chars, preserving anything that looks like a * filename extension. "partIdx" represents the length of the * string at this point. "partBuf" holds the string, which we * want to null-terminate before proceeding. * * Try to keep the filename extension, if any. */ partBuf[partIdx] = '\0'; if (partIdx > A2FileHFS::kMaxFileName) { const char* pDot = strrchr(partBuf, '.'); //int DEBUGDOTLEN = pDot - partBuf; if (pDot != NULL && partIdx - (pDot-partBuf) <= kMaxExtensionLen) { int dotLen = partIdx - (pDot-partBuf); memmove(partBuf + (A2FileProDOS::kMaxFileName - dotLen), pDot, dotLen); // don't use memcpy, move might overlap } partIdx = A2FileProDOS::kMaxFileName; } partBuf[partIdx] = '\0'; //LOGI(" HFS Converted component '%s' to '%s'", // origStart, partBuf); if (outPtr != outputBuf) *outPtr++ = A2FileHFS::kFssep; strcpy(outPtr, partBuf); outPtr += partIdx; /* * Continue with next segment. */ if (end == NULL) break; start = end+1; } *outPtr = '\0'; LOGI(" HFS Converted path '%s' to '%s' (fssep='%c')", path, outputBuf, fssep); assert(*outputBuf != '\0'); *pNormalizedPath = outputBuf; outputBuf = NULL; bail: delete[] workBuf; delete[] partBuf; delete[] outputBuf; return dierr; } /* * Compare two Macintosh filename strings. * * This requires some effort because the Macintosh Roman character set * doesn't sort the same way that ASCII does. HFS is case-insensitive but * case-preserving, so we need to deal with that too. The hfs_charorder * table takes care of it. * * Returns <0, ==0, or >0 depending on whether sstr1 is lexically less than, * equal to, or greater than sstr2. */ /*static*/ int DiskFSHFS::CompareMacFileNames(const char* sstr1, const char* sstr2) { const uint8_t* str1 = (const uint8_t*) sstr1; const uint8_t* str2 = (const uint8_t*) sstr2; int diff; while (*str1 && *str2) { diff = hfs_charorder[*str1] - hfs_charorder[*str2]; if (diff != 0) return diff; str1++; str2++; } return *str1 - *str2; } /* * Keep tweaking the filename until it no longer matches an existing file. * The first time this is called we don't know if the name is unique or not, * so we need to start by checking that. * * We have our choice between the DiskFS GetFileByName(), which traverses * a linear list, and hfs_stat(), which uses more efficient data structures * but may require disk reads. We use the DiskFS interface, on the assumption * that someday we'll switch the linear list to a tree structure. */ DIError DiskFSHFS::MakeFileNameUnique(const char* pathName, char** pUniqueName) { A2File* pFile; const int kMaxExtra = 3; const int kMaxDigits = 999; char* uniqueName; char* fileName; // points inside uniqueName assert(pathName != NULL); assert(pathName[0] == A2FileHFS::kFssep); /* see if it exists */ pFile = GetFileByName(pathName+1); if (pFile == NULL) { *pUniqueName = NULL; return kDIErrNone; } /* make a copy we can chew on */ uniqueName = new char[strlen(pathName) + kMaxExtra +1]; strcpy(uniqueName, pathName); fileName = strrchr(uniqueName, A2FileHFS::kFssep); assert(fileName != NULL); fileName++; int nameLen = strlen(fileName); int dotOffset=0, dotLen=0; char dotBuf[kMaxExtensionLen+1]; /* ensure the result will be null-terminated */ memset(fileName + nameLen, 0, kMaxExtra+1); /* * If this has what looks like a filename extension, grab it. We want * to preserve ".gif", ".c", etc., since the filetypes don't necessarily * do everything we need. */ const char* cp = strrchr(fileName, '.'); if (cp != NULL) { int tmpOffset = cp - fileName; if (tmpOffset > 0 && nameLen - tmpOffset <= kMaxExtensionLen) { LOGI(" HFS (keeping extension '%s')", cp); assert(strlen(cp) <= kMaxExtensionLen); strcpy(dotBuf, cp); dotOffset = tmpOffset; dotLen = nameLen - dotOffset; } } int digits = 0; int digitLen; int copyOffset; char digitBuf[kMaxExtra+1]; do { if (digits == kMaxDigits) return kDIErrFileExists; digits++; /* not the most efficient way to do this, but it'll do */ sprintf(digitBuf, "%d", digits); digitLen = strlen(digitBuf); if (nameLen + digitLen > A2FileHFS::kMaxFileName) copyOffset = A2FileHFS::kMaxFileName - dotLen - digitLen; else copyOffset = nameLen - dotLen; memcpy(fileName + copyOffset, digitBuf, digitLen); if (dotLen != 0) memcpy(fileName + copyOffset + digitLen, dotBuf, dotLen); } while (GetFileByName(uniqueName+1) != NULL); LOGI(" HFS converted to unique name: %s", uniqueName); *pUniqueName = uniqueName; return kDIErrNone; } /* * Create a new file or directory. Automatically creates the base path * if necessary. * * NOTE: much of this was cloned out of the ProDOS code. We probably want * a stronger set of utility functions in the parent class now that we have * more than one hierarchical file system. */ DIError DiskFSHFS::CreateFile(const CreateParms* pParms, A2File** ppNewFile) { DIError dierr = kDIErrNone; char typeStr[5], creatorStr[5]; char* normalizedPath = NULL; char* basePath = NULL; char* fileName = NULL; char* fullPath = NULL; A2FileHFS* pSubdir = NULL; A2FileHFS* pNewFile = NULL; hfsfile* pHfsFile = NULL; const bool createUnique = (GetParameter(kParm_CreateUnique) != 0); assert(fHfsVol != NULL); if (fpImg->GetReadOnly()) return kDIErrAccessDenied; assert(pParms != NULL); assert(pParms->pathName != NULL); assert(pParms->storageType == A2FileProDOS::kStorageSeedling || pParms->storageType == A2FileProDOS::kStorageExtended || pParms->storageType == A2FileProDOS::kStorageDirectory); // kStorageVolumeDirHeader not allowed -- that's created by Format LOGI(" HFS ---v--- CreateFile '%s'", pParms->pathName); /* * Normalize the pathname so that all components are HFS-safe * and separated by ':'. * * This must not "sanitize" the path. We need to be working with the * original characters, not the sanitized-for-display versions. */ assert(pParms->pathName != NULL); dierr = DoNormalizePath(pParms->pathName, pParms->fssep, &normalizedPath); if (dierr != kDIErrNone) goto bail; assert(normalizedPath != NULL); /* * The normalized path lacks a leading ':', and might need to * have some digits added to make the name unique. */ fullPath = new char[strlen(normalizedPath)+2]; fullPath[0] = A2FileHFS::kFssep; strcpy(fullPath+1, normalizedPath); delete[] normalizedPath; normalizedPath = NULL; /* * Make the name unique within the current directory. This requires * appending digits until the name doesn't match any others. */ if (createUnique && pParms->storageType != A2FileProDOS::kStorageDirectory) { char* uniquePath; dierr = MakeFileNameUnique(fullPath, &uniquePath); if (dierr != kDIErrNone) goto bail; if (uniquePath != NULL) { delete[] fullPath; fullPath = uniquePath; } } else { /* can't make unique; check to see if it already exists */ hfsdirent dirEnt; if (hfs_stat(fHfsVol, fullPath, &dirEnt) == 0) { if (pParms->storageType == A2FileProDOS::kStorageDirectory) dierr = kDIErrDirectoryExists; else dierr = kDIErrFileExists; goto bail; } } /* * Split the base path and filename apart. */ char* cp; cp = strrchr(fullPath, A2FileHFS::kFssep); assert(cp != NULL); if (cp == fullPath) { assert(basePath == NULL); fileName = new char[strlen(fullPath) +1]; strcpy(fileName, fullPath); } else { int dirNameLen = cp - fullPath; fileName = new char[strlen(cp+1) +1]; strcpy(fileName, cp+1); basePath = new char[dirNameLen+1]; strncpy(basePath, fullPath, dirNameLen); basePath[dirNameLen] = '\0'; } LOGI("SPLIT: '%s' '%s'", basePath, fileName); assert(fileName != NULL); /* * Open the base path. If it doesn't exist, create it recursively. */ if (basePath != NULL) { LOGI(" HFS Creating '%s' in '%s'", fileName, basePath); /* * Open the named subdir, creating it if it doesn't exist. We need * to check basePath+1 because we're comparing against what's in our * linear file list, and that doesn't include the leading ':'. */ pSubdir = (A2FileHFS*)GetFileByName(basePath+1, CompareMacFileNames); if (pSubdir == NULL) { LOGI(" HFS Creating subdir '%s'", basePath); A2File* pNewSub; CreateParms newDirParms; newDirParms.pathName = basePath; newDirParms.fssep = A2FileHFS::kFssep; newDirParms.storageType = A2FileProDOS::kStorageDirectory; newDirParms.fileType = 0; newDirParms.auxType = 0; newDirParms.access = 0; newDirParms.createWhen = newDirParms.modWhen = time(NULL); dierr = this->CreateFile(&newDirParms, &pNewSub); if (dierr != kDIErrNone) goto bail; assert(pNewSub != NULL); pSubdir = (A2FileHFS*) pNewSub; } /* * And now the annoying part. We need to reconstruct basePath out * of the filenames actually present, rather than relying on the * argument passed in. That's because HFS is case-insensitive but * case-preserving. It's not crucial for our inner workings, but the * linear file list in the DiskFS should have accurate strings. * (It'll work just fine, but the display might show the wrong values * for parent directories until they reload the disk.) * * On the bright side, we know exactly how long the string needs * to be, so we can just stomp on it in place. Assuming, of course, * that the filename created matches up with what the filename * normalizer came up with, which we can guarantee since (a) everybody * uses the same normalizer and (b) the "uniqueify" stuff doesn't * kick in for subdirs because we wouldn't be creating a new subdir * if it didn't already exist. * * This is essentially the same as RegeneratePathName(), but that's * meant for a situation where the filename already exists. */ A2FileHFS* pBaseDir = pSubdir; int basePathLen = strlen(basePath); while (!pBaseDir->IsVolumeDirectory()) { const char* fixedName = pBaseDir->GetFileName(); int fixedLen = strlen(fixedName); if (fixedLen > basePathLen) { assert(false); break; } assert(basePathLen == fixedLen || *(basePath + (basePathLen-fixedLen-1)) == kDIFssep); memcpy(basePath + (basePathLen-fixedLen), fixedName, fixedLen); basePathLen -= fixedLen+1; pBaseDir = (A2FileHFS*) pBaseDir->GetParent(); assert(pBaseDir != NULL); } // check the math; we should be left with the leading ':' if (pSubdir->IsVolumeDirectory()) assert(basePathLen == 1); else assert(basePathLen == 0); } else { /* open the volume directory */ LOGI(" HFS Creating '%s' in volume dir", fileName); /* volume dir must be first in the list */ pSubdir = (A2FileHFS*) GetNextFile(NULL); assert(pSubdir != NULL); assert(pSubdir->IsVolumeDirectory()); } if (pSubdir == NULL) { LOGI(" HFS Unable to open subdir '%s'", basePath); dierr = kDIErrFileNotFound; goto bail; } /* * Figure out file type. */ A2FileHFS::ConvertTypeToHFS(pParms->fileType, pParms->auxType, typeStr, creatorStr); /* * Create the file or directory. Populate "dirEnt" with the details. */ hfsdirent dirEnt; if (pParms->storageType == A2FileProDOS::kStorageDirectory) { /* create the directory */ if (hfs_mkdir(fHfsVol, fullPath) != 0) { LOGI(" HFS mkdir '%s' failed: %s", fullPath, hfs_error); dierr = kDIErrGeneric; goto bail; } if (hfs_stat(fHfsVol, fullPath, &dirEnt) != 0) { LOGI(" HFS stat on new dir failed: %s", hfs_error); dierr = kDIErrGeneric; goto bail; } /* create date *might* be useful, but probably not worth adjusting */ } else { /* create, and open, the file */ pHfsFile = hfs_create(fHfsVol, fullPath, typeStr, creatorStr); if (pHfsFile == NULL) { LOGI(" HFS create failed: %s", hfs_error); dierr = kDIErrGeneric; goto bail; } if (hfs_fstat(pHfsFile, &dirEnt) != 0) { LOGI(" HFS fstat on new file failed: %s", hfs_error); dierr = kDIErrGeneric; goto bail; } /* set the attributes according to pParms, and update the file */ dirEnt.crdate = pParms->createWhen; dirEnt.mddate = pParms->modWhen; if (pParms->access & A2FileProDOS::kAccessInvisible) dirEnt.fdflags |= HFS_FNDR_ISINVISIBLE; else dirEnt.fdflags &= ~HFS_FNDR_ISINVISIBLE; if ((pParms->access & ~A2FileProDOS::kAccessInvisible) == kFileAccessLocked) dirEnt.flags |= HFS_ISLOCKED; else dirEnt.flags &= ~HFS_ISLOCKED; (void) hfs_fsetattr(pHfsFile, &dirEnt); (void) hfs_close(pHfsFile); pHfsFile = NULL; } /* * Success! * * Create a new entry and set the structure fields. */ pNewFile = new A2FileHFS(this); pNewFile->InitEntry(&dirEnt); pNewFile->SetPathName(basePath == NULL ? "" : basePath, pNewFile->fFileName); pNewFile->SetParent(pSubdir); /* * Because we're hierarchical, and we guarantee that the contents of * subdirectories are grouped together, we must insert the file into an * appropriate place in the list rather than just throwing it onto the * end. * * The proper location for the new file in the linear list is in sorted * order with the files in the current directory. We have to be careful * here because libhfs is going to use Macintosh Roman sort ordering, * which may be different from ASCII ordering. Worst case: we end up * putting it in the wrong place and it jumps around when the disk image * is reopened. * * All files in a subdir appear in the list after that subdir, but there * might be intervening entries from deeper directories. So we have to * chase through some or all of the file list to find the right place. * Not great, but we don't have enough files or do adds often enough to * make this worth optimizing. */ A2File* pLastSubdirFile; A2File* pPrevFile; A2File* pNextFile; pPrevFile = pLastSubdirFile = pSubdir; pNextFile = GetNextFile(pPrevFile); while (pNextFile != NULL) { if (pNextFile->GetParent() == pNewFile->GetParent()) { /* in same subdir, compare names */ if (CompareMacFileNames(pNextFile->GetPathName(), pNewFile->GetPathName()) > 0) { /* passed it; insert new after previous file */ pLastSubdirFile = pPrevFile; LOGI(" HFS Found '%s' > cur(%s)", pNextFile->GetPathName(), pNewFile->GetPathName()); break; } /* still too early; save in case it's last one in dir */ pLastSubdirFile = pNextFile; } pPrevFile = pNextFile; pNextFile = GetNextFile(pNextFile); } /* insert us after last file we saw that was part of the same subdir */ LOGI(" HFS inserting '%s' after '%s'", pNewFile->GetPathName(), pLastSubdirFile->GetPathName()); InsertFileInList(pNewFile, pLastSubdirFile); //LOGI("LIST NOW:"); //DumpFileList(); *ppNewFile = pNewFile; pNewFile = NULL; bail: delete pNewFile; delete[] normalizedPath; delete[] basePath; delete[] fileName; delete[] fullPath; hfs_flush(fHfsVol); LOGI(" HFS ---^--- CreateFile '%s' DONE", pParms->pathName); return dierr; } /* * Delete the named file. * * We need to use a different call for file vs. directory. */ DIError DiskFSHFS::DeleteFile(A2File* pGenericFile) { DIError dierr = kDIErrNone; char* pathName = NULL; if (fpImg->GetReadOnly()) return kDIErrAccessDenied; if (!fDiskIsGood) return kDIErrBadDiskImage; if (pGenericFile->IsFileOpen()) return kDIErrFileOpen; A2FileHFS* pFile = (A2FileHFS*) pGenericFile; pathName = pFile->GetLibHFSPathName(); LOGI(" Deleting '%s'", pathName); if (pFile->IsDirectory()) { if (hfs_rmdir(fHfsVol, pathName) != 0) { LOGI(" HFS rmdir failed '%s': '%s'", pathName, hfs_error); dierr = kDIErrGeneric; goto bail; } } else { if (hfs_delete(fHfsVol, pathName) != 0) { LOGI(" HFS delete failed '%s': '%s'", pathName, hfs_error); dierr = kDIErrGeneric; goto bail; } } /* * Remove the A2File* from the list. */ DeleteFileFromList(pFile); bail: hfs_flush(fHfsVol); delete[] pathName; return dierr; } /* * Rename a file. * * Pass in a pointer to the file and a string with the new filename (just * the filename, not a pathname -- this function doesn't move files * between directories). The new name must already be normalized. * * Renaming the magic volume directory "file" is not allowed. * * We don't try to keep AppleWorks aux type flags consistent (they're used * to determine which characters are lower case on ProDOS disks). They'll * get fixed up when we copy them to a ProDOS disk, which is the only way * 8-bit AppleWorks can get at them. */ DIError DiskFSHFS::RenameFile(A2File* pGenericFile, const char* newName) { DIError dierr = kDIErrNone; A2FileHFS* pFile = (A2FileHFS*) pGenericFile; char* colonOldName = NULL; char* colonNewName = NULL; if (pFile == NULL || newName == NULL) return kDIErrInvalidArg; if (!IsValidFileName(newName)) return kDIErrInvalidArg; if (pFile->IsVolumeDirectory()) return kDIErrInvalidArg; if (fpImg->GetReadOnly()) return kDIErrAccessDenied; if (!fDiskIsGood) return kDIErrBadDiskImage; char* lastColon; colonOldName = pFile->GetLibHFSPathName(); // adds ':' to start of string lastColon = strrchr(colonOldName, A2FileHFS::kFssep); assert(lastColon != NULL); if (lastColon == colonOldName) { /* in root dir */ colonNewName = new char[1 + strlen(newName) +1]; colonNewName[0] = A2FileHFS::kFssep; strcpy(colonNewName+1, newName); } else { /* prepend subdir */ int len = lastColon - colonOldName +1; // e.g. ":path1:path2:" colonNewName = new char[len + strlen(newName) +1]; strncpy(colonNewName, colonOldName, len); strcpy(colonNewName+len, newName); } LOGI(" HFS renaming '%s' to '%s'", colonOldName, colonNewName); if (hfs_rename(fHfsVol, colonOldName, colonNewName) != 0) { LOGI(" HFS rename('%s','%s') failed: %s", colonOldName, colonNewName, hfs_error); dierr = kDIErrGeneric; goto bail; } /* * Success! Update the file name. */ strcpy(pFile->fFileName, newName); /* * Now the fun part. If we simply renamed a file, we can just update the * one entry. If we renamed a directory, life gets interesting because * we store the full pathname in every A2FileHFS entry. (It's an * efficiency win most of the time, but it's really annoying here.) * * HFS makes this especially unpleasant because it keeps the files * arranged in sorted order. If we change a file's name, we may have to * move it to a new position in the linear file list. If we don't, the * list no longer reflects the order in which the files actually appear * on the disk, and they'll shift around when we reload. * * There are two approaches: re-sort the list (awkward, since it's stored * in a linked list -- we'd probably want to sort tags in a parallel * structure), or find the affected block of files, find the new start * position, and shift the entire range in one shot. * * This doesn't seem like something that anybody but me will ever care * about, so I'm going to skip it for now. */ A2File* pCur; if (pFile->IsDirectory()) { /* do all files that come after us */ pCur = pFile; while (pCur != NULL) { RegeneratePathName((A2FileHFS*) pCur); pCur = GetNextFile(pCur); } } else { RegeneratePathName(pFile); } bail: delete[] colonOldName; delete[] colonNewName; hfs_flush(fHfsVol); return dierr; } /* * Regenerate fPathName for the specified file. * * Has no effect on the magic volume dir entry. * * This could be implemented more efficiently, but it's only used when * renaming files, so there's not much point. * * [This was lifted straight out of the ProDOS sources. It should probably * be moved into generic DiskFS.] */ DIError DiskFSHFS::RegeneratePathName(A2FileHFS* pFile) { A2FileHFS* pParent; char* buf = NULL; int len; /* nothing to do here */ if (pFile->IsVolumeDirectory()) return kDIErrNone; /* compute the length of the path name */ len = strlen(pFile->GetFileName()); pParent = (A2FileHFS*) pFile->GetParent(); while (!pParent->IsVolumeDirectory()) { len++; // leave space for the ':' len += strlen(pParent->GetFileName()); pParent = (A2FileHFS*) pParent->GetParent(); } buf = new char[len+1]; if (buf == NULL) return kDIErrMalloc; /* generate the new path name */ int partLen; partLen = strlen(pFile->GetFileName()); strcpy(buf + len - partLen, pFile->GetFileName()); len -= partLen; pParent = (A2FileHFS*) pFile->GetParent(); while (!pParent->IsVolumeDirectory()) { assert(len > 0); buf[--len] = A2FileHFS::kFssep; partLen = strlen(pParent->GetFileName()); strncpy(buf + len - partLen, pParent->GetFileName(), partLen); len -= partLen; assert(len >= 0); pParent = (A2FileHFS*) pParent->GetParent(); } LOGI("Replacing '%s' with '%s'", pFile->GetPathName(), buf); pFile->SetPathName("", buf); delete[] buf; return kDIErrNone; } /* * Change the HFS volume name. * * This uses the same libhfs interface that we use for renaming files. The * Mac convention is to *not* start the volume name with a colon. In fact, * the libhfs convention is to *end* the volume names with a colon. */ DIError DiskFSHFS::RenameVolume(const char* newName) { DIError dierr = kDIErrNone; A2FileHFS* pFile; char* oldNameColon = NULL; char* newNameColon = NULL; if (!IsValidVolumeName(newName)) return kDIErrInvalidArg; if (fpImg->GetReadOnly()) return kDIErrAccessDenied; /* get file list entry for volume name */ pFile = (A2FileHFS*) GetNextFile(NULL); assert(strcmp(pFile->GetFileName(), fVolumeName) == 0); oldNameColon = new char[strlen(fVolumeName)+2]; strcpy(oldNameColon, fVolumeName); strcat(oldNameColon, ":"); newNameColon = new char[strlen(newName)+2]; strcpy(newNameColon, newName); strcat(newNameColon, ":"); if (hfs_rename(fHfsVol, oldNameColon, newNameColon) != 0) { LOGI(" HFS rename '%s' -> '%s' failed: %s", oldNameColon, newNameColon, hfs_error); dierr = kDIErrGeneric; goto bail; } /* update stuff */ strcpy(fVolumeName, newName); SetVolumeID(); strcpy(pFile->fFileName, newName); pFile->SetPathName("", newName); bail: delete[] oldNameColon; delete[] newNameColon; hfs_flush(fHfsVol); return dierr; } /* * Set file attributes. */ DIError DiskFSHFS::SetFileInfo(A2File* pGenericFile, uint32_t fileType, uint32_t auxType, uint32_t accessFlags) { DIError dierr = kDIErrNone; A2FileHFS* pFile = (A2FileHFS*) pGenericFile; hfsdirent dirEnt; char* colonPath; if (fpImg->GetReadOnly()) return kDIErrAccessDenied; if (pFile == NULL) return kDIErrInvalidArg; if (pFile->IsDirectory() || pFile->IsVolumeDirectory()) return kDIErrNone; // impossible; just ignore it colonPath = pFile->GetLibHFSPathName(); if (hfs_stat(fHfsVol, colonPath, &dirEnt) != 0) { LOGI(" HFS unable to stat '%s': %s", colonPath, hfs_error); dierr = kDIErrGeneric; goto bail; } A2FileHFS::ConvertTypeToHFS(fileType, auxType, dirEnt.u.file.type, dirEnt.u.file.creator); if (accessFlags & A2FileProDOS::kAccessInvisible) dirEnt.fdflags |= HFS_FNDR_ISINVISIBLE; else dirEnt.fdflags &= ~HFS_FNDR_ISINVISIBLE; if ((accessFlags & ~A2FileProDOS::kAccessInvisible) == kFileAccessLocked) dirEnt.flags |= HFS_ISLOCKED; else dirEnt.flags &= ~HFS_ISLOCKED; LOGD(" HFS setting '%s' to fdflags=0x%04x flags=0x%04x", colonPath, dirEnt.fdflags, dirEnt.flags); LOGD(" type=0x%08x creator=0x%08x", fileType, auxType); if (hfs_setattr(fHfsVol, colonPath, &dirEnt) != 0) { LOGW(" HFS setattr '%s' failed: %s", colonPath, hfs_error); dierr = kDIErrGeneric; goto bail; } /* update our local copy */ pFile->fType = fileType; pFile->fCreator = auxType; pFile->fAccess = accessFlags; // should actually base them on HFS vals bail: delete[] colonPath; hfs_flush(fHfsVol); return dierr; } #endif // !EXCISE_GPL_CODE /* * =========================================================================== * A2FileHFS * =========================================================================== */ /* * Dump the contents of the A2File structure. */ void A2FileHFS::Dump(void) const { LOGI("A2FileHFS '%s'", fFileName); } /* convert hex to decimal */ inline int FromHex(char hexVal) { if (hexVal >= '0' && hexVal <= '9') return hexVal - '0'; else if (hexVal >= 'a' && hexVal <= 'f') return hexVal -'a' + 10; else if (hexVal >= 'A' && hexVal <= 'F') return hexVal - 'A' + 10; else return -1; } /* * If this has a ProDOS filetype, convert it. * * This stuff is defined in Technical Note PT515, "Apple File Exchange Q&As". * In theory we should convert type=BINA and type=TEXT regardless of the * creator, but since those just go to generic text/binary types I don't * think we need to handle it here (and I'm more comfortable leaving them * with their Macintosh creators). * * In some respects, converting to ProDOS types is a bad idea, because we * don't have a 1:1 mapping. If we copy a pdos/p\0\0\0 file we will store it * as pdos/BINA instead. In practice, for the Apple II world they are * equivalent, and CiderPress really doesn't need the "raw" file type. If * it becomes annoying, we can add a DiskFSParameter to control it. */ uint32_t A2FileHFS::GetFileType(void) const { if (fCreator != kPdosType) return fType; if ((fType & 0xffff) == 0x2020) { // 'XY ', where XY are hex digits for ProDOS file type int digit1, digit2; digit1 = FromHex((char) (fType >> 24)); digit2 = FromHex((char) (fType >> 16)); if (digit1 < 0 || digit2 < 0) { LOGI(" Unexpected: pdos + %08x", fType); return 0x00; } return digit1 << 4 | digit2; } uint8_t flag = (uint8_t)(fType >> 24); if (flag == 0x70) { // 'p' /* type and aux embedded within */ return (fType >> 16) & 0xff; } else { /* type stored as a string */ if (fType == 0x42494e41) // 'BINA' return 0x00; // NON else if (fType == 0x54455854) // 'TEXT' return 0x04; else if (fType == 0x50535953) // 'PSYS' return 0xff; else if (fType == 0x50533136) // 'PS16' return 0xb3; else return 0x00; } } /* * If this has a ProDOS aux type, convert it. */ uint32_t A2FileHFS::GetAuxType(void) const { if (fCreator != kPdosType) return fCreator; uint8_t flag = (uint8_t)(fType >> 24); if (flag == 0x70) { // 'p' /* type and aux embedded within */ return fType & 0xffff; } else { return 0x0000; } } /* * Set the full pathname to a combination of the base path and the * current file's name. * * If we're in the volume directory, pass in "" for the base path (not NULL). */ void A2FileHFS::SetPathName(const char* basePath, const char* fileName) { assert(basePath != NULL && fileName != NULL); if (fPathName != NULL) delete[] fPathName; // strip leading ':' (but treat ":" specially for volume dir entry) if (basePath[0] == ':' && basePath[1] != '\0') basePath++; int baseLen = strlen(basePath); fPathName = new char[baseLen + 1 + strlen(fileName)+1]; strcpy(fPathName, basePath); if (baseLen != 0 && !(baseLen == 1 && basePath[0] == ':')) { *(fPathName + baseLen) = kFssep; baseLen++; } strcpy(fPathName + baseLen, fileName); } #ifndef EXCISE_GPL_CODE /* * Return a copy of the pathname that libhfs will like. * * The caller must delete[] the return value. */ char* A2FileHFS::GetLibHFSPathName(void) const { char* nameBuf; nameBuf = new char[strlen(fPathName)+2]; nameBuf[0] = kFssep; strcpy(nameBuf+1, fPathName); return nameBuf; } /* * Convert numeric file/aux type to HFS strings. "pType" and "pCreator" must * be able to hold 5 bytes each (4-byte type + nul). * * Follows the PT515 recommendations, mostly. The "PSYS" and "PS16" * conversions discard the file's aux type and therefore are unsuitable, * and the conversion of SRC throws away its identity. */ /*static*/ void A2FileHFS::ConvertTypeToHFS(uint32_t fileType, uint32_t auxType, char* pType, char* pCreator) { if (fileType == 0x00 && auxType == 0x0000) { strcpy(pCreator, "pdos"); strcpy(pType, "BINA"); } else if (fileType == 0x04 && auxType == 0x0000) { strcpy(pCreator, "pdos"); strcpy(pType, "TEXT"); } else if (fileType >= 0 && fileType <= 0xff && auxType >= 0 && auxType <= 0xffff) { pType[0] = 'p'; pType[1] = (uint8_t) fileType; pType[2] = (uint8_t) (auxType >> 8); pType[3] = (uint8_t) auxType; pType[4] = '\0'; pCreator[0] = 'p'; pCreator[1] = 'd'; pCreator[2] = 'o'; pCreator[3] = 's'; pCreator[4] = '\0'; } else { pType[0] = (uint8_t)(fileType >> 24); pType[1] = (uint8_t)(fileType >> 16); pType[2] = (uint8_t)(fileType >> 8); pType[3] = (uint8_t) fileType; pType[4] = '\0'; pCreator[0] = (uint8_t)(auxType >> 24); pCreator[1] = (uint8_t)(auxType >> 16); pCreator[2] = (uint8_t)(auxType >> 8); pCreator[3] = (uint8_t) auxType; pCreator[4] = '\0'; } } /* * Open a file through libhfs. * * libhfs wants filenames to begin with ':' unless they start with the * name of the volume. This is the opposite of the convention followed * by the rest of CiderPress (and most of the civilized world), so instead * of storing the pathname that way we just tack it on here. */ DIError A2FileHFS::Open(A2FileDescr** ppOpenFile, bool readOnly, bool rsrcFork /*=false*/) { DIError dierr = kDIErrNone; A2FDHFS* pOpenFile = NULL; hfsfile* pHfsFile; char* nameBuf = NULL; if (fpOpenFile != NULL) return kDIErrAlreadyOpen; //if (rsrcFork && fRsrcLength < 0) // return kDIErrForkNotFound; nameBuf = GetLibHFSPathName(); DiskFSHFS* pDiskFS = (DiskFSHFS*) GetDiskFS(); pHfsFile = hfs_open(pDiskFS->GetHfsVol(), nameBuf); if (pHfsFile == NULL) { LOGI(" HFS hfs_open(%s) failed: %s", nameBuf, hfs_error); dierr = kDIErrGeneric; // better value might be in errno goto bail; } hfs_setfork(pHfsFile, rsrcFork ? 1 : 0); pOpenFile = new A2FDHFS(this, pHfsFile); fpOpenFile = pOpenFile; *ppOpenFile = pOpenFile; bail: delete[] nameBuf; return dierr; } /* * =========================================================================== * A2FDHFS * =========================================================================== */ /* * Read a chunk of data from the fake file. */ DIError A2FDHFS::Read(void* buf, size_t len, size_t* pActual) { long result; LOGD(" HFS reading %lu bytes from '%s' (offset=%ld)", (unsigned long) len, fpFile->GetPathName(), hfs_seek(fHfsFile, 0, HFS_SEEK_CUR)); //A2FileHFS* pFile = (A2FileHFS*) fpFile; result = hfs_read(fHfsFile, buf, len); if (result < 0) return kDIErrReadFailed; if (pActual != NULL) { *pActual = (size_t) result; } else if (result != (long) len) { // short read, can't report it, return error return kDIErrDataUnderrun; } /* * To do this right we need to break the hfs_read() into smaller * pieces. However, it only really affects us for files that are * getting reformatted, because that's the only time we grab the * entire thing in one big piece. */ long offset = hfs_seek(fHfsFile, 0, HFS_SEEK_CUR); if (!UpdateProgress(offset)) { return kDIErrCancelled; } return kDIErrNone; } /* * Write data at the current offset. * * (In the current implementation, the entire file is always written in * one piece. This function does work correctly with multiple smaller * pieces though, because it lets libhfs do all the work.) */ DIError A2FDHFS::Write(const void* buf, size_t len, size_t* pActual) { long result; LOGD(" HFS writing %lu bytes to '%s' (offset=%ld)", (unsigned long) len, fpFile->GetPathName(), hfs_seek(fHfsFile, 0, HFS_SEEK_CUR)); fModified = true; // assume something gets changed //A2FileHFS* pFile = (A2FileHFS*) fpFile; result = hfs_write(fHfsFile, buf, len); if (result < 0) return kDIErrWriteFailed; if (pActual != NULL) { *pActual = (size_t) result; } else if (result != (long) len) { // short write, can't report it, return error return kDIErrDataUnderrun; } /* to make this work right, we need to break hfs_write into pieces */ long offset = hfs_seek(fHfsFile, 0, HFS_SEEK_CUR); if (!UpdateProgress(offset)) { return kDIErrCancelled; } /* * We don't hfs_flush here, because we don't expect the application to * hold the file open, and we flush in Close(). */ return kDIErrNone; } /* * Seek to a new offset. */ DIError A2FDHFS::Seek(di_off_t offset, DIWhence whence) { int hfsWhence; unsigned long result; switch (whence) { case kSeekSet: hfsWhence = HFS_SEEK_SET; break; case kSeekEnd: hfsWhence = HFS_SEEK_END; break; case kSeekCur: hfsWhence = HFS_SEEK_CUR; break; default: assert(false); return kDIErrInvalidArg; } result = hfs_seek(fHfsFile, (long) offset, hfsWhence); if (result == (unsigned long) -1) { DebugBreak(); return kDIErrGeneric; } return kDIErrNone; } /* * Return current offset. */ di_off_t A2FDHFS::Tell(void) { di_off_t offset; /* get current position without moving pointer */ offset = hfs_seek(fHfsFile, 0, HFS_SEEK_CUR); return offset; } /* * Release file state, and tell our parent to destroy us. */ DIError A2FDHFS::Close(void) { hfsdirent dirEnt; /* * If the file was written to, update our info. */ if (fModified) { if (hfs_fstat(fHfsFile, &dirEnt) == 0) { A2FileHFS* pFile = (A2FileHFS*) fpFile; pFile->fDataLength = dirEnt.u.file.dsize; pFile->fRsrcLength = dirEnt.u.file.rsize; if (pFile->fRsrcLength == 0) pFile->fRsrcLength = -1; LOGI(" HFS close set dataLen=%ld rsrcLen=%ld", (long) pFile->fDataLength, (long) pFile->fRsrcLength); } else { LOGI(" HFS Close fstat failed: %s", hfs_error); // close it anyway } } hfs_close(fHfsFile); fHfsFile = NULL; /* flush changes */ if (fModified) { DiskFSHFS* pDiskFS = (DiskFSHFS*) fpFile->GetDiskFS(); if (hfs_flush(pDiskFS->GetHfsVol()) != 0) { LOGI("HEY: Close flush failed!"); DebugBreak(); } } fpFile->CloseDescr(this); return kDIErrNone; } /* * Return the #of sectors/blocks in the file. Not supported, but since HFS * doesn't support "sparse" files we can fake it. */ long A2FDHFS::GetSectorCount(void) const { A2FileHFS* pFile = (A2FileHFS*) fpFile; return (long) ((pFile->fDataLength+255) / 256 + (pFile->fRsrcLength+255) / 256); } long A2FDHFS::GetBlockCount(void) const { A2FileHFS* pFile = (A2FileHFS*) fpFile; return (long) ((pFile->fDataLength+511) / 512 + (pFile->fRsrcLength+511) / 512); } /* * Return the Nth track/sector in this file. Not supported. */ DIError A2FDHFS::GetStorage(long sectorIdx, long* pTrack, long* pSector) const { return kDIErrNotSupported; } /* * Return the Nth 512-byte block in this file. Not supported. */ DIError A2FDHFS::GetStorage(long blockIdx, long* pBlock) const { return kDIErrNotSupported; } #else // EXCISE_GPL_CODE ----------------------------------------------------- /* * Get things rolling. * * Since we're assured that this is a valid disk, errors encountered from here * on out must be handled somehow, possibly by claiming that the disk is * completely full and has no files on it. */ DIError DiskFSHFS::Initialize(InitMode initMode) { DIError dierr = kDIErrNone; dierr = LoadVolHeader(); if (dierr != kDIErrNone) goto bail; DumpVolHeader(); CreateFakeFile(); SetVolumeUsageMap(); bail: return dierr; } /* * Fill a buffer with some interesting stuff, and add it to the file list. */ void DiskFSHFS::CreateFakeFile(void) { A2FileHFS* pFile; char buf[768]; // currently running about 475 static const char* kFormatMsg = "The Macintosh HFS filesystem is not supported. CiderPress knows how to\r" "recognize HFS volumes so that it can identify partitions on CFFA-formatted\r" "CompactFlash cards and Apple II CD-ROMs, but the current version does not\r" "know how to view or extract files.\r" "\r" "Some information about this HFS volume:\r" "\r" " Volume name : '%s'\r" " Storage capacity : %ld blocks (%.2fMB)\r" " Number of files : %ld\r" " Number of folders : %ld\r" " Last modified : %s\r" "\r" ; char dateBuf[32]; long capacity; const char* timeStr; capacity = (fAllocationBlockSize / kBlkSize) * fNumAllocationBlocks; /* get the mod time, format it, and remove the trailing '\n' */ time_t when = (time_t) (fModifiedDateTime - kDateTimeOffset - fLocalTimeOffset); timeStr = ctime(&when); if (timeStr == NULL) { LOGI("Invalid date %ld (orig=%ld)", when, fModifiedDateTime); strcpy(dateBuf, ""); } else strncpy(dateBuf, timeStr, sizeof(dateBuf)); int len = strlen(dateBuf); if (len > 0) dateBuf[len-1] = '\0'; memset(buf, 0, sizeof(buf)); snprintf(buf, NELEM(buf), kFormatMsg, fVolumeName, capacity, (double) capacity / 2048.0, fNumFiles, fNumDirectories, dateBuf); pFile = new A2FileHFS(this); pFile->fIsDir = false; pFile->fIsVolumeDir = false; pFile->fType = 0; pFile->fCreator = 0; pFile->SetFakeFile(buf, strlen(buf)); strcpy(pFile->fFileName, "(not supported)"); pFile->SetPathName("", pFile->fFileName); pFile->fDataLength = 0; pFile->fRsrcLength = -1; pFile->fCreateWhen = 0; pFile->fModWhen = 0; pFile->SetFakeFile(buf, strlen(buf)); AddFileToList(pFile); } /* * We could do this, but there's not much point. */ DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, int* pUnitSize) const { return kDIErrNotSupported; } /* * Not a whole lot to do. */ DIError A2FileHFS::Open(A2FileDescr** ppOpenFile, bool readOnly, bool rsrcFork /*=false*/) { A2FDHFS* pOpenFile = NULL; if (fpOpenFile != NULL) return kDIErrAlreadyOpen; if (rsrcFork && fRsrcLength < 0) return kDIErrForkNotFound; assert(readOnly == true); pOpenFile = new A2FDHFS(this, NULL); fpOpenFile = pOpenFile; *ppOpenFile = pOpenFile; return kDIErrNone; } /* * =========================================================================== * A2FDHFS * =========================================================================== */ /* * Read a chunk of data from the fake file. */ DIError A2FDHFS::Read(void* buf, size_t len, size_t* pActual) { LOGD(" HFS reading %d bytes from '%s' (offset=%ld)", len, fpFile->GetPathName(), (long) fOffset); A2FileHFS* pFile = (A2FileHFS*) fpFile; /* don't allow them to read past the end of the file */ if (fOffset + (long)len > pFile->fDataLength) { if (pActual == NULL) return kDIErrDataUnderrun; len = (size_t) (pFile->fDataLength - fOffset); } if (pActual != NULL) *pActual = len; memcpy(buf, pFile->GetFakeFileBuf(), len); fOffset += len; return kDIErrNone; } /* * Write data at the current offset. */ DIError A2FDHFS::Write(const void* buf, size_t len, size_t* pActual) { return kDIErrNotSupported; } /* * Seek to a new offset. */ DIError A2FDHFS::Seek(di_off_t offset, DIWhence whence) { di_off_t fileLen = ((A2FileHFS*) fpFile)->fDataLength; switch (whence) { case kSeekSet: if (offset < 0 || offset > fileLen) return kDIErrInvalidArg; fOffset = offset; break; case kSeekEnd: if (offset > 0 || offset < -fileLen) return kDIErrInvalidArg; fOffset = fileLen + offset; break; case kSeekCur: if (offset < -fOffset || offset >= (fileLen - fOffset)) { return kDIErrInvalidArg; } fOffset += offset; break; default: assert(false); return kDIErrInvalidArg; } assert(fOffset >= 0 && fOffset <= fileLen); return kDIErrNone; } /* * Return current offset. */ di_off_t A2FDHFS::Tell(void) { return fOffset; } /* * Release file state, and tell our parent to destroy us. */ DIError A2FDHFS::Close(void) { fpFile->CloseDescr(this); return kDIErrNone; } /* * Return the #of sectors/blocks in the file. */ long A2FDHFS::GetSectorCount(void) const { A2FileHFS* pFile = (A2FileHFS*) fpFile; return (long) ((pFile->fDataLength+255) / 256); } long A2FDHFS::GetBlockCount(void) const { A2FileHFS* pFile = (A2FileHFS*) fpFile; return (long) ((pFile->fDataLength+511) / 512); } /* * Return the Nth track/sector in this file. */ DIError A2FDHFS::GetStorage(long sectorIdx, long* pTrack, long* pSector) const { return kDIErrNotSupported; } /* * Return the Nth 512-byte block in this file. */ DIError A2FDHFS::GetStorage(long blockIdx, long* pBlock) const { return kDIErrNotSupported; } #endif // EXCISE_GPL_CODE ---------------------------------------------------