mirror of
https://github.com/fadden/ciderpress.git
synced 2024-11-26 02:49:20 +00:00
aa3145856c
Focusing on the diskimg library this time, which deals with a lot of filesystem structures that have specific widths. This is still a bit lax in places, e.g. using "long" for lengths. Should either specify a bit width or use di_off_t. Also, added "override" keyword where appropriate. Also, bumped library version to 5.0.0.
2297 lines
70 KiB
C++
2297 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 = %lu, 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 = %ld (allocSize=%ld [x%lu], numAllocs=%lu)",
|
|
fTotalBlocks, fAllocationBlockSize, fAllocationBlockSize / kBlkSize,
|
|
fNumAllocationBlocks);
|
|
LOGI(" num directories=%ld, num files=%ld",
|
|
fNumDirectories, fNumFiles);
|
|
time_t when;
|
|
when = (time_t) (fCreatedDateTime - kDateTimeOffset - fLocalTimeOffset);
|
|
LOGI(" cre date=0x%08lx %.24s", fCreatedDateTime, ctime(&when));
|
|
when = (time_t) (fModifiedDateTime - kDateTimeOffset - fLocalTimeOffset);
|
|
LOGI(" mod date=0x%08lx %.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 kDIErrNone;
|
|
}
|
|
|
|
/*
|
|
* 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 kDIErrNone;
|
|
}
|
|
|
|
/*
|
|
* 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;
|
|
|
|
LOGI(" HFS reading %d bytes from '%s' (offset=%ld)",
|
|
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;
|
|
|
|
LOGI(" HFS writing %d bytes to '%s' (offset=%ld)",
|
|
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 ---------------------------------------------------
|