ciderpress/diskimg/HFS.cpp
Andy McFadden 29e64011e1 Don't use %zd format
Visual Studio still doesn't support it.
2014-12-11 17:34:25 -08:00

2299 lines
70 KiB
C++

/*
* 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, long fileType, long auxType,
long 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%08lx creator=0x%08lx", 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.
*/
long 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 + %08lx", 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.
*/
long 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(long fileType, long 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, "<no date>");
} 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 ---------------------------------------------------