ciderpress/app/DiskArchive.cpp

3173 lines
101 KiB
C++

/*
* CiderPress
* Copyright (C) 2009 by CiderPress authors. All Rights Reserved.
* Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved.
* See the file LICENSE for distribution terms.
*/
/*
* Bridge between DiskImg and GenericArchive.
*/
#include "stdafx.h"
#include "DiskArchive.h"
#include "NufxArchive.h"
#include "Preferences.h"
#include "Main.h"
#include "ImageFormatDialog.h"
#include "RenameEntryDialog.h"
#include "ConfirmOverwriteDialog.h"
#include "../diskimg/DiskImgDetail.h"
static const char* kEmptyFolderMarker = ".$$EmptyFolder";
/*
* ===========================================================================
* DiskEntry
* ===========================================================================
*/
/*
* Extract data from a disk image.
*
* If "*ppText" is non-nil, the data will be read into the pointed-to buffer
* so long as it's shorter than *pLength bytes. The value in "*pLength"
* will be set to the actual length used.
*
* If "*ppText" is nil, the uncompressed data will be placed into a buffer
* allocated with "new[]".
*
* Returns IDOK on success, IDCANCEL if the operation was cancelled by the
* user, and -1 value on failure. On failure, "*pErrMsg" holds an error
* message.
*
* "which" is an anonymous GenericArchive enum (e.g. "kDataThread").
*/
int
DiskEntry::ExtractThreadToBuffer(int which, char** ppText, long* pLength,
CString* pErrMsg) const
{
DIError dierr;
A2FileDescr* pOpenFile = nil;
char* dataBuf = nil;
bool rsrcFork;
bool needAlloc = true;
int result = -1;
ASSERT(fpFile != nil);
ASSERT(pErrMsg != nil);
*pErrMsg = "";
if (*ppText != nil)
needAlloc = false;
if (GetDamaged()) {
*pErrMsg = "File is damaged";
goto bail;
}
if (which == kDataThread)
rsrcFork = false;
else if (which == kRsrcThread)
rsrcFork = true;
else {
*pErrMsg = "No such fork";
goto bail;
}
LONGLONG len;
if (rsrcFork)
len = fpFile->GetRsrcLength();
else
len = fpFile->GetDataLength();
if (len == 0) {
if (needAlloc) {
*ppText = new char[1];
**ppText = '\0';
}
*pLength = 0;
result = IDOK;
goto bail;
} else if (len < 0) {
assert(rsrcFork); // forked files always have a data fork
*pErrMsg = L"That fork doesn't exist";
goto bail;
}
dierr = fpFile->Open(&pOpenFile, true, rsrcFork);
if (dierr != kDIErrNone) {
*pErrMsg = L"File open failed";
goto bail;
}
SET_PROGRESS_BEGIN();
pOpenFile->SetProgressUpdater(DiskArchive::ProgressCallback, len, nil);
if (needAlloc) {
dataBuf = new char[(int) len];
if (dataBuf == nil) {
pErrMsg->Format(L"ERROR: allocation of %ld bytes failed", len);
goto bail;
}
} else {
if (*pLength < (long) len) {
pErrMsg->Format(L"ERROR: buf size %ld too short (%ld)",
*pLength, (long) len);
goto bail;
}
dataBuf = *ppText;
}
dierr = pOpenFile->Read(dataBuf, (size_t) len);
if (dierr != kDIErrNone) {
if (dierr == kDIErrCancelled) {
result = IDCANCEL;
} else {
pErrMsg->Format(L"File read failed: %hs",
DiskImgLib::DIStrError(dierr));
}
goto bail;
}
if (needAlloc)
*ppText = dataBuf;
*pLength = (long) len;
result = IDOK;
bail:
if (pOpenFile != nil)
pOpenFile->Close();
if (result == IDOK) {
SET_PROGRESS_END();
ASSERT(pErrMsg->IsEmpty());
} else {
ASSERT(result == IDCANCEL || !pErrMsg->IsEmpty());
if (needAlloc) {
delete[] dataBuf;
ASSERT(*ppText == nil);
}
}
return result;
}
/*
* Extract data from a thread to a file. Since we're not copying to memory,
* we can't assume that we're able to hold the entire file all at once.
*
* Returns IDOK on success, IDCANCEL if the operation was cancelled by the
* user, and -1 value on failure. On failure, "*pMsg" holds an
* error message.
*/
int
DiskEntry::ExtractThreadToFile(int which, FILE* outfp, ConvertEOL conv,
ConvertHighASCII convHA, CString* pErrMsg) const
{
A2FileDescr* pOpenFile = nil;
bool rsrcFork;
int result = -1;
ASSERT(IDOK != -1 && IDCANCEL != -1);
ASSERT(fpFile != nil);
if (which == kDataThread)
rsrcFork = false;
else if (which == kRsrcThread)
rsrcFork = true;
else {
/* if we handle disk images, make sure we disable "conv" */
*pErrMsg = L"No such fork";
goto bail;
}
LONGLONG len;
if (rsrcFork)
len = fpFile->GetRsrcLength();
else
len = fpFile->GetDataLength();
if (len == 0) {
WMSG0("Empty fork\n");
result = IDOK;
goto bail;
} else if (len < 0) {
assert(rsrcFork); // forked files always have a data fork
*pErrMsg = L"That fork doesn't exist";
goto bail;
}
DIError dierr;
dierr = fpFile->Open(&pOpenFile, true, rsrcFork);
if (dierr != kDIErrNone) {
*pErrMsg = L"Unable to open file on disk image";
goto bail;
}
dierr = CopyData(pOpenFile, outfp, conv, convHA, pErrMsg);
if (dierr != kDIErrNone) {
if (pErrMsg->IsEmpty()) {
pErrMsg->Format(L"Failed while copying data: %hs\n",
DiskImgLib::DIStrError(dierr));
}
goto bail;
}
result = IDOK;
bail:
if (pOpenFile != nil)
pOpenFile->Close();
return result;
}
/*
* Copy data from the open A2File to outfp, possibly converting EOL along
* the way.
*/
DIError
DiskEntry::CopyData(A2FileDescr* pOpenFile, FILE* outfp, ConvertEOL conv,
ConvertHighASCII convHA, CString* pMsg) const
{
DIError dierr = kDIErrNone;
const int kChunkSize = 16384;
char buf[kChunkSize];
//bool firstChunk = true;
//EOLType sourceType;
bool lastCR = false;
LONGLONG srcLen, dataRem;
/* get the length of the open file */
dierr = pOpenFile->Seek(0, DiskImgLib::kSeekEnd);
if (dierr != kDIErrNone)
goto bail;
srcLen = pOpenFile->Tell();
dierr = pOpenFile->Rewind();
if (dierr != kDIErrNone)
goto bail;
ASSERT(srcLen > 0); // empty files should've been caught earlier
SET_PROGRESS_BEGIN();
pOpenFile->SetProgressUpdater(DiskArchive::ProgressCallback, srcLen, nil);
/*
* Loop until all data copied.
*/
dataRem = srcLen;
while (dataRem) {
int chunkLen;
if (dataRem > kChunkSize)
chunkLen = kChunkSize;
else
chunkLen = (int) dataRem;
/* read a chunk from the source file */
dierr = pOpenFile->Read(buf, chunkLen);
if (dierr != kDIErrNone) {
pMsg->Format(L"File read failed: %hs",
DiskImgLib::DIStrError(dierr));
goto bail;
}
/* write chunk to destination file */
int err = GenericEntry::WriteConvert(outfp, buf, chunkLen, &conv,
&convHA, &lastCR);
if (err != 0) {
pMsg->Format(L"File write failed: %hs", strerror(err));
dierr = kDIErrGeneric;
goto bail;
}
dataRem -= chunkLen;
//SET_PROGRESS_UPDATE(ComputePercent(srcLen - dataRem, srcLen));
}
bail:
pOpenFile->ClearProgressUpdater();
SET_PROGRESS_END();
return dierr;
}
/*
* Figure out whether or not we're allowed to change a file's type and
* aux type.
*/
bool
DiskEntry::GetFeatureFlag(Feature feature) const
{
DiskImg::FSFormat format;
format = fpFile->GetDiskFS()->GetDiskImg()->GetFSFormat();
switch (feature) {
case kFeatureCanChangeType:
{
//if (GetRecordKind() == kRecordKindVolumeDir)
// return false;
switch (format) {
case DiskImg::kFormatProDOS:
case DiskImg::kFormatPascal:
case DiskImg::kFormatMacHFS:
case DiskImg::kFormatDOS32:
case DiskImg::kFormatDOS33:
return true;
default:
return false;
}
}
case kFeaturePascalTypes:
{
switch (format) {
case DiskImg::kFormatPascal:
return true;
default:
return false;
}
}
case kFeatureDOSTypes:
{
switch (format) {
case DiskImg::kFormatDOS32:
case DiskImg::kFormatDOS33:
return true;
default:
return false;
}
}
case kFeatureHFSTypes:
{
switch (format) {
case DiskImg::kFormatMacHFS:
return true;
default:
return false;
}
}
case kFeatureHasFullAccess:
{
switch (format) {
case DiskImg::kFormatProDOS:
return true;
default:
return false;
}
}
case kFeatureHasSimpleAccess:
{
switch (format) {
case DiskImg::kFormatDOS33:
case DiskImg::kFormatDOS32:
case DiskImg::kFormatCPM:
case DiskImg::kFormatMacHFS:
return true;
default:
return false;
}
}
case kFeatureHasInvisibleFlag:
{
switch(format) {
case DiskImg::kFormatProDOS:
case DiskImg::kFormatMacHFS:
return true;
default:
return false;
}
}
default:
WMSG1("Unexpected feature flag %d\n", feature);
assert(false);
return false;
}
assert(false);
return false;
}
/*
* ===========================================================================
* DiskArchive
* ===========================================================================
*/
/*
* Perform one-time initialization of the DiskLib library.
*/
/*static*/ CString
DiskArchive::AppInit(void)
{
CString result("");
DIError dierr;
long major, minor, bug;
WMSG0("Initializing DiskImg library\n");
// set this before initializing, so we can see init debug msgs
DiskImgLib::Global::SetDebugMsgHandler(DebugMsgHandler);
dierr = DiskImgLib::Global::AppInit();
if (dierr != kDIErrNone) {
result.Format(L"DiskImg DLL failed to initialize: %hs\n",
DiskImgLib::DIStrError(dierr));
goto bail;
}
DiskImgLib::Global::GetVersion(&major, &minor, &bug);
if (major != kDiskImgVersionMajor || minor < kDiskImgVersionMinor) {
result.Format(L"Older or incompatible version of DiskImg DLL found.\r\r"
L"Wanted v%d.%d.x, found %ld.%ld.%ld.",
kDiskImgVersionMajor, kDiskImgVersionMinor,
major, minor, bug);
goto bail;
}
bail:
return result;
}
/*
* Perform one-time cleanup of DiskImgLib at shutdown time.
*/
/*static*/ void
DiskArchive::AppCleanup(void)
{
DiskImgLib::Global::AppCleanup();
}
/*
* Handle a debug message from the DiskImg library.
*/
/*static*/ void
DiskArchive::DebugMsgHandler(const char* file, int line, const char* msg)
{
ASSERT(file != nil);
ASSERT(msg != nil);
#if defined(_DEBUG_LOG)
//fprintf(gLog, "%s(%d) : %s", file, line, msg);
fprintf(gLog, "%05u %s", gPid, msg);
#elif defined(_DEBUG)
_CrtDbgReport(_CRT_WARN, file, line, NULL, "%s", msg);
#else
/* do nothing */
#endif
}
/*
* Progress update callback, called from DiskImgLib during read/write
* operations.
*
* Returns "true" if we should continue;
*/
/*static*/ bool
DiskArchive::ProgressCallback(DiskImgLib::A2FileDescr* pFile,
DiskImgLib::di_off_t max, DiskImgLib::di_off_t current, void* state)
{
int status;
//::Sleep(10);
status = SET_PROGRESS_UPDATE(ComputePercent(current, max));
if (status == IDCANCEL) {
WMSG0("IDCANCEL returned from Main progress updater\n");
return false;
}
return true; // tell DiskImgLib to continue what it's doing
}
/*
* Progress update callback, called from DiskImgLib while scanning a volume
* during Open().
*
* "str" must not contain a '%'. (TODO: fix that)
*
* Returns "true" if we should continue.
*/
/*static*/ bool
DiskArchive::ScanProgressCallback(void* cookie, const char* str, int count)
{
CString fmt;
bool cont;
if (count == 0)
fmt = str;
else
fmt.Format(L"%hs (%%d)", str);
cont = SET_PROGRESS_COUNTER_2(fmt, count);
if (!cont) {
WMSG0("cancelled\n");
}
return cont;
}
/*
* Finish instantiating a DiskArchive object by opening an existing file.
*/
GenericArchive::OpenResult
DiskArchive::Open(const WCHAR* filename, bool readOnly, CString* pErrMsg)
{
DIError dierr;
CString errMsg;
OpenResult result = kResultUnknown;
const Preferences* pPreferences = GET_PREFERENCES();
ASSERT(fpPrimaryDiskFS == nil);
ASSERT(filename != nil);
//ASSERT(ext != nil);
ASSERT(pPreferences != nil);
fIsReadOnly = readOnly;
// special case for volume open
bool isVolume = false;
if (filename[0] >= 'A' && filename[0] <= 'Z' &&
filename[1] == ':' && filename[2] == '\\' &&
filename[3] == '\0')
{
isVolume = true;
}
/*
* Open the image. This can be very slow for compressed images,
* especially 3.5" FDI images.
*/
{
CWaitCursor waitc;
dierr = fDiskImg.OpenImage(filename, PathProposal::kLocalFssep, readOnly);
if (dierr == kDIErrAccessDenied && !readOnly && !isVolume) {
// retry file open with read-only set
// don't do that for volumes -- assume they know what they want
WMSG0(" Retrying open with read-only set\n");
fIsReadOnly = readOnly = true;
dierr = fDiskImg.OpenImage(filename, PathProposal::kLocalFssep, readOnly);
}
if (dierr != kDIErrNone) {
if (dierr == kDIErrFileArchive)
result = kResultFileArchive;
else {
result = kResultFailure;
errMsg.Format(L"Unable to open '%ls': %hs.", filename,
DiskImgLib::DIStrError(dierr));
}
goto bail;
}
}
dierr = fDiskImg.AnalyzeImage();
if (dierr != kDIErrNone) {
result = kResultFailure;
errMsg.Format(L"Analysis of '%ls' failed: %hs", filename,
DiskImgLib::DIStrError(dierr));
goto bail;
}
/* allow them to override sector order and filesystem, if requested */
if (pPreferences->GetPrefBool(kPrQueryImageFormat)) {
ImageFormatDialog imf;
imf.InitializeValues(&fDiskImg);
imf.fFileSource = filename;
imf.SetQueryDisplayFormat(false);
imf.SetAllowGenericFormats(false);
if (imf.DoModal() != IDOK) {
WMSG0("User bailed on IMF dialog\n");
result = kResultCancel;
goto bail;
}
if (imf.fSectorOrder != fDiskImg.GetSectorOrder() ||
imf.fFSFormat != fDiskImg.GetFSFormat())
{
WMSG0("Initial values overridden, forcing img format\n");
dierr = fDiskImg.OverrideFormat(fDiskImg.GetPhysicalFormat(),
imf.fFSFormat, imf.fSectorOrder);
if (dierr != kDIErrNone) {
result = kResultFailure;
errMsg.Format(L"Unable to access disk image using selected"
L" parameters. Error: %hs.",
DiskImgLib::DIStrError(dierr));
goto bail;
}
}
}
if (fDiskImg.GetFSFormat() == DiskImg::kFormatUnknown ||
fDiskImg.GetSectorOrder() == DiskImg::kSectorOrderUnknown)
{
result = kResultFailure;
errMsg.Format(L"Unable to identify filesystem on '%ls'", filename);
goto bail;
}
/* create an appropriate DiskFS object */
fpPrimaryDiskFS = fDiskImg.OpenAppropriateDiskFS();
if (fpPrimaryDiskFS == nil) {
/* unknown FS should've been caught above! */
ASSERT(false);
result = kResultFailure;
errMsg.Format(L"Format of '%ls' not recognized.", filename);
goto bail;
}
fpPrimaryDiskFS->SetScanForSubVolumes(DiskFS::kScanSubEnabled);
/*
* Scan all files and on the disk image, and recursively descend into
* sub-volumes. Can be slow on physical volumes.
*
* This is really only useful for ProDOS and HFS disks. Nothing else
* can be large enough to really get slow, and nothing else is likely
* to show up in a large multi-partition image.
*
* THOUGHT: only show the dialog if the volume is over a certain size.
*/
{
MainWindow* pMain = GET_MAIN_WINDOW();
ProgressCounterDialog* pProgress;
pProgress = new ProgressCounterDialog;
pProgress->Create(L"Examining contents, please wait...", pMain);
pProgress->SetCounterFormat(L"Scanning...");
pProgress->CenterWindow();
//pMain->PeekAndPump(); // redraw
CWaitCursor waitc;
/* set up progress dialog and scan all files */
pMain->SetProgressCounterDialog(pProgress);
fDiskImg.SetScanProgressCallback(ScanProgressCallback, this);
dierr = fpPrimaryDiskFS->Initialize(&fDiskImg, DiskFS::kInitFull);
fDiskImg.SetScanProgressCallback(nil, nil);
pMain->SetProgressCounterDialog(nil);
pProgress->DestroyWindow();
if (dierr != kDIErrNone) {
if (dierr == kDIErrCancelled) {
result = kResultCancel;
} else {
result = kResultFailure;
errMsg.Format(L"Error reading list of files from disk: %hs",
DiskImgLib::DIStrError(dierr));
}
goto bail;
}
}
if (LoadContents() != 0) {
result = kResultFailure;
errMsg = L"Failed while loading contents of disk image.";
goto bail;
}
/*
* Force read-only flag if underlying FS doesn't allow RW. We need to
* consider embedded filesystems, so we only set RO if none of the
* filesystems are writable.
*
* BUG: this only checks the first level. Should be fully recursive.
*/
if (!fpPrimaryDiskFS->GetReadWriteSupported()) {
const DiskFS::SubVolume* pSubVol;
fIsReadOnly = true;
pSubVol = fpPrimaryDiskFS->GetNextSubVolume(nil);
while (pSubVol != nil) {
if (pSubVol->GetDiskFS()->GetReadWriteSupported()) {
fIsReadOnly = false;
break;
}
pSubVol = fpPrimaryDiskFS->GetNextSubVolume(pSubVol);
}
}
/* force read-only if the primary is damaged */
if (fpPrimaryDiskFS->GetFSDamaged())
fIsReadOnly = true;
/* force read-only if the DiskImg thinks a wrapper is damaged */
if (fpPrimaryDiskFS->GetDiskImg()->GetReadOnly())
fIsReadOnly = true;
// /* force read-only on .gz/.zip unless pref allows */
// if (fDiskImg.GetOuterFormat() != DiskImg::kOuterFormatNone) {
// if (pPreferences->GetPrefBool(kPrWriteZips) == 0)
// fIsReadOnly = true;
// }
SetPathName(filename);
result = kResultSuccess;
/* set any preferences-based settings */
PreferencesChanged();
bail:
*pErrMsg = errMsg;
if (!errMsg.IsEmpty()) {
assert(result == kResultFailure);
delete fpPrimaryDiskFS;
fpPrimaryDiskFS = nil;
} else {
assert(result != kResultFailure);
}
return result;
}
/*
* Finish instantiating a DiskArchive object by creating a new archive.
*
* Returns an error string on failure, or "" on success.
*/
CString
DiskArchive::New(const WCHAR* fileName, const void* vOptions)
{
const Preferences* pPreferences = GET_PREFERENCES();
NewOptions* pOptions = (NewOptions*) vOptions;
CString volName;
CStringA volNameA, fileNameA;
long numBlocks = -1;
long numTracks = -1;
int numSectors;
CString retmsg;
DIError dierr;
bool allowLowerCase;
ASSERT(fileName != nil);
ASSERT(pOptions != nil);
allowLowerCase = pPreferences->GetPrefBool(kPrProDOSAllowLower) != 0;
switch (pOptions->base.format) {
case DiskImg::kFormatUnknown:
numBlocks = pOptions->blank.numBlocks;
break;
case DiskImg::kFormatProDOS:
volName = pOptions->prodos.volName;
numBlocks = pOptions->prodos.numBlocks;
break;
case DiskImg::kFormatPascal:
volName = pOptions->pascalfs.volName;
numBlocks = pOptions->pascalfs.numBlocks;
break;
case DiskImg::kFormatMacHFS:
volName = pOptions->hfs.volName;
numBlocks = pOptions->hfs.numBlocks;
break;
case DiskImg::kFormatDOS32:
numTracks = pOptions->dos.numTracks;
numSectors = pOptions->dos.numSectors;
if (numTracks < DiskFSDOS33::kMinTracks ||
numTracks > DiskFSDOS33::kMaxTracks)
{
retmsg = L"Invalid DOS32 track count";
goto bail;
}
if (numSectors != 13) {
retmsg = L"Invalid DOS32 sector count";
goto bail;
}
if (pOptions->dos.allocDOSTracks)
volName = L"DOS";
break;
case DiskImg::kFormatDOS33:
numTracks = pOptions->dos.numTracks;
numSectors = pOptions->dos.numSectors;
if (numTracks < DiskFSDOS33::kMinTracks ||
numTracks > DiskFSDOS33::kMaxTracks)
{
retmsg = L"Invalid DOS33 track count";
goto bail;
}
if (numSectors != 16 && numSectors != 32) { // no 13-sector (yet)
retmsg = L"Invalid DOS33 sector count";
goto bail;
}
if (pOptions->dos.allocDOSTracks)
volName = L"DOS";
break;
default:
retmsg = L"Unsupported disk format";
goto bail;
}
WMSG4("DiskArchive: new '%ls' %ld %hs in '%ls'\n",
(LPCWSTR) volName, numBlocks,
DiskImg::ToString(pOptions->base.format), fileName);
bool canSkipFormat;
if (IsWin9x())
canSkipFormat = false;
else
canSkipFormat = true;
/*
* Create an image with the appropriate characteristics. We set
* "skipFormat" because we know this will be a brand-new file, and
* we're not currently creating nibble images.
*
* GLITCH: under Win98/ME, brand-new files contain the previous contents
* of the hard drive. We need to explicitly zero them out. We don't
* want to do it under Win2K/XP because it can be slow for larger
* volumes.
*/
fileNameA = fileName;
if (numBlocks > 0) {
dierr = fDiskImg.CreateImage(fileNameA, nil,
DiskImg::kOuterFormatNone,
DiskImg::kFileFormatUnadorned,
DiskImg::kPhysicalFormatSectors,
nil,
pOptions->base.sectorOrder,
DiskImg::kFormatGenericProDOSOrd, // arg must be generic
numBlocks,
canSkipFormat);
} else {
ASSERT(numTracks > 0);
dierr = fDiskImg.CreateImage(fileNameA, nil,
DiskImg::kOuterFormatNone,
DiskImg::kFileFormatUnadorned,
DiskImg::kPhysicalFormatSectors,
nil,
pOptions->base.sectorOrder,
DiskImg::kFormatGenericProDOSOrd, // arg must be generic
numTracks, numSectors,
canSkipFormat);
}
if (dierr != kDIErrNone) {
retmsg.Format(L"Unable to create disk image: %hs.",
DiskImgLib::DIStrError(dierr));
goto bail;
}
if (pOptions->base.format == DiskImg::kFormatUnknown)
goto skip_format;
if (pOptions->base.format == DiskImg::kFormatDOS33 ||
pOptions->base.format == DiskImg::kFormatDOS32)
fDiskImg.SetDOSVolumeNum(pOptions->dos.volumeNum);
/*
* If we don't allow lower case in ProDOS filenames, don't allow them
* in volume names either. This works because we don't allow ' ' in
* volume names; otherwise we'd need to invoke a ProDOS-specific call
* to convert the ' ' to '.'. (Or we could just do it ourselves.)
*
* We can't ask the ProDOS DiskFS to force upper case for us because
* the ProDOS DiskFS object doesn't yet exist.
*/
if (pOptions->base.format == DiskImg::kFormatProDOS && !allowLowerCase)
volName.MakeUpper();
/* format it */
volNameA = volName;
dierr = fDiskImg.FormatImage(pOptions->base.format, volNameA);
if (dierr != kDIErrNone) {
retmsg.Format(L"Unable to format disk image: %hs.",
DiskImgLib::DIStrError(dierr));
goto bail;
}
fpPrimaryDiskFS = fDiskImg.OpenAppropriateDiskFS(false);
if (fpPrimaryDiskFS == nil) {
retmsg = L"Unable to create DiskFS.";
goto bail;
}
/* prep it */
dierr = fpPrimaryDiskFS->Initialize(&fDiskImg, DiskFS::kInitFull);
if (dierr != kDIErrNone) {
retmsg.Format(L"Error reading list of files from disk: %hs",
DiskImgLib::DIStrError(dierr));
goto bail;
}
/* this is pretty meaningless, but do it to ensure we're initialized */
if (LoadContents() != 0) {
retmsg = L"Failed while loading contents of disk image.";
goto bail;
}
skip_format:
SetPathName(fileName);
/* set any preferences-based settings */
PreferencesChanged();
bail:
return retmsg;
}
/*
* Close the DiskArchive ojbect.
*/
CString
DiskArchive::Close(void)
{
if (fpPrimaryDiskFS != nil) {
WMSG0("DiskArchive shutdown closing disk image\n");
delete fpPrimaryDiskFS;
fpPrimaryDiskFS = nil;
}
DIError dierr;
dierr = fDiskImg.CloseImage();
if (dierr != kDIErrNone) {
MainWindow* pMainWin = (MainWindow*)::AfxGetMainWnd();
CString msg, failed;
msg.Format(L"Failed while closing disk image: %hs.",
DiskImgLib::DIStrError(dierr));
failed.LoadString(IDS_FAILED);
WMSG1("During close: %ls\n", msg);
pMainWin->MessageBox(msg, failed, MB_OK);
}
return L"";
}
/*
* Flush the DiskArchive object.
*
* Most of the stuff we do with disk images goes straight through, but in
* the case of compressed disks we don't normally re-compress them until
* it's time to close them. This forces us to update the copy on disk.
*
* Returns an empty string on success, or an error message on failure.
*/
CString
DiskArchive::Flush(void)
{
DIError dierr;
CWaitCursor waitc;
assert(fpPrimaryDiskFS != nil);
dierr = fpPrimaryDiskFS->Flush(DiskImg::kFlushAll);
if (dierr != kDIErrNone) {
CString errMsg;
errMsg.Format(L"Attempt to flush the current archive failed: %hs.",
DiskImgLib::DIStrError(dierr));
return errMsg;
}
return L"";
}
/*
* Returns "true" if the archive has un-flushed modifications pending.
*/
bool
DiskArchive::IsModified(void) const
{
assert(fpPrimaryDiskFS != nil);
return fpPrimaryDiskFS->GetDiskImg()->GetDirtyFlag();
}
/*
* Return an description of the disk archive, suitable for display in the
* main title bar.
*/
void
DiskArchive::GetDescription(CString* pStr) const
{
if (fpPrimaryDiskFS == nil)
return;
if (fpPrimaryDiskFS->GetVolumeID() != nil) {
pStr->Format(L"Disk Image - %hs", fpPrimaryDiskFS->GetVolumeID());
}
}
/*
* Load the contents of a "disk archive".
*
* Returns 0 on success.
*/
int
DiskArchive::LoadContents(void)
{
int result;
WMSG0("DiskArchive LoadContents\n");
ASSERT(fpPrimaryDiskFS != nil);
{
MainWindow* pMain = GET_MAIN_WINDOW();
ExclusiveModelessDialog* pWaitDlg = new ExclusiveModelessDialog;
pWaitDlg->Create(IDD_LOADING, pMain);
pWaitDlg->CenterWindow();
pMain->PeekAndPump(); // redraw
CWaitCursor waitc;
result = LoadDiskFSContents(fpPrimaryDiskFS, L"");
SET_PROGRESS_COUNTER(-1);
pWaitDlg->DestroyWindow();
//pMain->PeekAndPump(); // redraw
}
return result;
}
/*
* Reload the stuff from the underlying DiskFS.
*
* This also does a "lite" flush of the disk data. For files that are
* essentially being written as we go, this does little more than clear
* the "dirty" flag. Files that need to be recompressed or have some
* other slow operation remain dirty.
*
* We don't need to do the flush as part of the reload -- we can load the
* contents with everything in a perfectly dirty state. We don't need to
* do it at all. We do it to keep the "dirty" flag clear when nothing is
* really dirty, and we do it here because almost all of our functions call
* "reload" after making changes, which makes it convenient to call from here.
*/
CString
DiskArchive::Reload()
{
fReloadFlag = true; // tell everybody that cached data is invalid
(void) fpPrimaryDiskFS->Flush(DiskImg::kFlushFastOnly);
DeleteEntries(); // a GenericArchive operation
if (LoadContents() != 0)
return "Disk image reload failed.";
return "";
}
/*
* Reload the contents of the archive, showing an error message if the
* reload fails.
*
* Returns 0 on success, -1 on failure.
*/
int
DiskArchive::InternalReload(CWnd* pMsgWnd)
{
CString errMsg;
errMsg = Reload();
if (!errMsg.IsEmpty()) {
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
return -1;
}
return 0;
}
/*
* Load the contents of a DiskFS.
*
* Recursively handle sub-volumes. "volName" holds the name of the
* sub-volume as it should appear in the list.
*/
int
DiskArchive::LoadDiskFSContents(DiskFS* pDiskFS, const WCHAR* volName)
{
static const WCHAR* kBlankFileName = L"<blank filename>";
A2File* pFile;
DiskEntry* pNewEntry;
DiskFS::SubVolume* pSubVol;
const Preferences* pPreferences = GET_PREFERENCES();
bool wantCoerceDOSFilenames = false;
CString ourSubVolName;
wantCoerceDOSFilenames = pPreferences->GetPrefBool(kPrCoerceDOSFilenames);
WMSG2("Notes for disk image '%ls':\n%hs",
volName, pDiskFS->GetDiskImg()->GetNotes());
ASSERT(pDiskFS != nil);
pFile = pDiskFS->GetNextFile(nil);
while (pFile != nil) {
pNewEntry = new DiskEntry(pFile);
if (pNewEntry == nil)
return -1;
CString path(pFile->GetPathName());
if (path.IsEmpty())
path = kBlankFileName;
if (DiskImg::UsesDOSFileStructure(pFile->GetFSFormat()) &&
wantCoerceDOSFilenames)
{
InjectLowercase(&path);
}
pNewEntry->SetPathName(path);
if (volName[0] != '\0')
pNewEntry->SetSubVolName(volName);
pNewEntry->SetFssep(pFile->GetFssep());
pNewEntry->SetFileType(pFile->GetFileType());
pNewEntry->SetAuxType(pFile->GetAuxType());
pNewEntry->SetAccess(pFile->GetAccess());
if (pFile->GetCreateWhen() == 0)
pNewEntry->SetCreateWhen(kDateNone);
else
pNewEntry->SetCreateWhen(pFile->GetCreateWhen());
if (pFile->GetModWhen() == 0)
pNewEntry->SetModWhen(kDateNone);
else
pNewEntry->SetModWhen(pFile->GetModWhen());
pNewEntry->SetSourceFS(pFile->GetFSFormat());
pNewEntry->SetHasDataFork(true);
if (pFile->IsVolumeDirectory()) {
/* volume directory entry; only on ProDOS/HFS */
ASSERT(pFile->GetRsrcLength() < 0);
pNewEntry->SetRecordKind(GenericEntry::kRecordKindVolumeDir);
//pNewEntry->SetUncompressedLen(pFile->GetDataLength());
pNewEntry->SetDataForkLen(pFile->GetDataLength());
pNewEntry->SetCompressedLen(pFile->GetDataLength());
} else if (pFile->IsDirectory()) {
/* directory entry */
ASSERT(pFile->GetRsrcLength() < 0);
pNewEntry->SetRecordKind(GenericEntry::kRecordKindDirectory);
//pNewEntry->SetUncompressedLen(pFile->GetDataLength());
pNewEntry->SetDataForkLen(pFile->GetDataLength());
pNewEntry->SetCompressedLen(pFile->GetDataLength());
} else if (pFile->GetRsrcLength() >= 0) {
/* has resource fork */
pNewEntry->SetRecordKind(GenericEntry::kRecordKindForkedFile);
pNewEntry->SetDataForkLen(pFile->GetDataLength());
pNewEntry->SetRsrcForkLen(pFile->GetRsrcLength());
//pNewEntry->SetUncompressedLen(
// pFile->GetDataLength() + pFile->GetRsrcLength() );
pNewEntry->SetCompressedLen(
pFile->GetDataSparseLength() + pFile->GetRsrcSparseLength() );
pNewEntry->SetHasRsrcFork(true);
} else {
/* just data fork */
pNewEntry->SetRecordKind(GenericEntry::kRecordKindFile);
//pNewEntry->SetUncompressedLen(pFile->GetDataLength());
pNewEntry->SetDataForkLen(pFile->GetDataLength());
pNewEntry->SetCompressedLen(pFile->GetDataSparseLength());
}
switch (pNewEntry->GetSourceFS()) {
case DiskImg::kFormatDOS33:
case DiskImg::kFormatDOS32:
case DiskImg::kFormatUNIDOS:
case DiskImg::kFormatGutenberg:
pNewEntry->SetFormatStr(L"DOS");
break;
case DiskImg::kFormatProDOS:
pNewEntry->SetFormatStr(L"ProDOS");
break;
case DiskImg::kFormatPascal:
pNewEntry->SetFormatStr(L"Pascal");
break;
case DiskImg::kFormatCPM:
pNewEntry->SetFormatStr(L"CP/M");
break;
case DiskImg::kFormatMSDOS:
pNewEntry->SetFormatStr(L"MS-DOS");
break;
case DiskImg::kFormatRDOS33:
case DiskImg::kFormatRDOS32:
case DiskImg::kFormatRDOS3:
pNewEntry->SetFormatStr(L"RDOS");
break;
case DiskImg::kFormatMacHFS:
pNewEntry->SetFormatStr(L"HFS");
break;
default:
pNewEntry->SetFormatStr(L"???");
break;
}
pNewEntry->SetDamaged(pFile->GetQuality() == A2File::kQualityDamaged);
pNewEntry->SetSuspicious(pFile->GetQuality() == A2File::kQualitySuspicious);
AddEntry(pNewEntry);
/* this is not very useful -- all the heavy lifting was done earlier */
if ((GetNumEntries() % 100) == 0)
SET_PROGRESS_COUNTER(GetNumEntries());
pFile = pDiskFS->GetNextFile(pFile);
}
/*
* Load all sub-volumes.
*
* We define the sub-volume name to use for the next layer down. We
* prepend an underscore to the unmodified name. So long as the volume
* name is a valid Windows path -- which should hold true for most disks,
* though possibly not for Pascal -- it can be extracted directly with
* its full path with no risk of conflict. (The extraction code relies
* on this, so don't put a ':' in the subvol name or Windows will choke.)
*/
pSubVol = pDiskFS->GetNextSubVolume(nil);
while (pSubVol != nil) {
CString concatSubVolName;
const char* subVolName;
int ret;
subVolName = pSubVol->GetDiskFS()->GetVolumeName();
if (subVolName == nil)
subVolName = "+++"; // call it *something*
if (volName[0] == '\0')
concatSubVolName.Format(L"_%hs", subVolName);
else
concatSubVolName.Format(L"%ls_%hs", volName, subVolName);
ret = LoadDiskFSContents(pSubVol->GetDiskFS(), concatSubVolName);
if (ret != 0)
return ret;
pSubVol = pDiskFS->GetNextSubVolume(pSubVol);
}
return 0;
}
/*
* User has updated their preferences. Take note.
*
* Setting preferences in a DiskFS causes those prefs to be pushed down
* to all sub-volumes.
*/
void
DiskArchive::PreferencesChanged(void)
{
const Preferences* pPreferences = GET_PREFERENCES();
if (fpPrimaryDiskFS != nil) {
fpPrimaryDiskFS->SetParameter(DiskFS::kParmProDOS_AllowLowerCase,
pPreferences->GetPrefBool(kPrProDOSAllowLower) != 0);
fpPrimaryDiskFS->SetParameter(DiskFS::kParmProDOS_AllocSparse,
pPreferences->GetPrefBool(kPrProDOSUseSparse) != 0);
}
}
/*
* Report on what this disk image is capable of.
*/
long
DiskArchive::GetCapability(Capability cap)
{
switch (cap) {
case kCapCanTest:
return false;
break;
case kCapCanRenameFullPath:
return false;
break;
case kCapCanRecompress:
return false;
break;
case kCapCanEditComment:
return false;
break;
case kCapCanAddDisk:
return false;
break;
case kCapCanConvEOLOnAdd:
return true;
break;
case kCapCanCreateSubdir:
return true;
break;
case kCapCanRenameVolume:
return true;
break;
default:
ASSERT(false);
return -1;
break;
}
}
/*
* ===========================================================================
* DiskArchive -- add files
* ===========================================================================
*/
/*
* Process a bulk "add" request.
*
* Returns "true" on success, "false" on failure.
*/
bool
DiskArchive::BulkAdd(ActionProgressDialog* pActionProgress,
const AddFilesDialog* pAddOpts)
{
NuError nerr;
CString errMsg;
WCHAR curDir[MAX_PATH] = L"";
bool retVal = false;
WMSG2("Opts: '%ls' typePres=%d\n", (LPCWSTR) pAddOpts->fStoragePrefix,
pAddOpts->fTypePreservation);
WMSG3(" sub=%d strip=%d ovwr=%d\n",
pAddOpts->fIncludeSubfolders, pAddOpts->fStripFolderNames,
pAddOpts->fOverwriteExisting);
ASSERT(fpAddDataHead == nil);
/* these reset on every new add */
fOverwriteExisting = false;
fOverwriteNoAsk = false;
/* we screen for clashes with existing files later; this just ensures
"batch uniqueness" */
fpPrimaryDiskFS->SetParameter(DiskFS::kParm_CreateUnique, true);
/*
* Save the current directory and change to the one from the file dialog.
*/
const WCHAR* buf = pAddOpts->GetFileNames();
WMSG2("Selected path = '%ls' (offset=%d)\n", buf,
pAddOpts->GetFileNameOffset());
if (GetCurrentDirectory(NELEM(curDir), curDir) == 0) {
errMsg = L"Unable to get current directory.\n";
ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED);
goto bail;
}
if (SetCurrentDirectory(buf) == false) {
errMsg.Format(L"Unable to set current directory to '%ls'.\n", buf);
ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED);
goto bail;
}
buf += pAddOpts->GetFileNameOffset();
while (*buf != '\0') {
WMSG1(" file '%ls'\n", buf);
/* add the file, calling DoAddFile via the generic AddFile */
nerr = AddFile(pAddOpts, buf, &errMsg);
if (nerr != kNuErrNone) {
if (errMsg.IsEmpty())
errMsg.Format(L"Failed while adding file '%ls': %hs.",
(LPCWSTR) buf, NuStrError(nerr));
if (nerr != kNuErrAborted) {
ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED);
}
goto bail;
}
buf += wcslen(buf)+1;
}
if (fpAddDataHead == nil) {
CString title;
title.LoadString(IDS_MB_APP_NAME);
errMsg = L"No files added.\n";
pActionProgress->MessageBox(errMsg, title, MB_OK | MB_ICONWARNING);
} else {
/* add all pending files */
retVal = true;
errMsg = ProcessFileAddData(pAddOpts->fpTargetDiskFS,
pAddOpts->fConvEOL);
if (!errMsg.IsEmpty()) {
CString title;
ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED);
retVal = false;
}
/* success or failure, reload the contents */
errMsg = Reload();
if (!errMsg.IsEmpty())
retVal = false;
}
bail:
FreeAddDataList();
if (SetCurrentDirectory(curDir) == false) {
errMsg.Format(L"Unable to reset current directory to '%ls'.\n", buf);
ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED);
// bummer, but don't signal failure
}
return retVal;
}
/*
* Add a file to a disk image.
*
* Unfortunately we can't just add the files here. We need to figure out
* which pairs of files should be combined into a single "extended" file.
* (Yes, the cursed forked files strike again.)
*
* The way you tell if two files should be one is by comparing their
* filenames and type info. If they match, and one is a data fork and
* one is a resource fork, we have a single split file.
*
* We have to be careful here because we don't know which will be seen
* first and whether they'll be adjacent. We have to dig through the
* list of previously-added files for a match (O(n^2) behavior currently).
*
* We also have to compare the right filename. Comparing the Windows
* filename is a bad idea, because by definition one of them has a resource
* fork tag on it. We need to compare the normalized filename before
* the ProDOS normalizer/uniqifier gets a chance to mangle it. As luck
* would have it, that's exactly what we have in "storageName".
*
* For a NuFX archive, NufxLib does all this nonsense for us, but we have
* to manage it ourselves here. The good news is that, since we have to
* wade through all the filenames, we have an opportunity to make the names
* unique. So long as we ensure that the names we have don't clash with
* anything currently on the disk, we know that anything we add that does
* clash is running into something we just added, which means we can turn
* on CreateFile's "make unique" feature and let the filesystem-specific
* code handle uniqueness.
*
* Any fields we want to keep from the NuFileDetails struct need to be
* copied out. It's a "hairy" struct, so we need to duplicate the strings.
*/
NuError
DiskArchive::DoAddFile(const AddFilesDialog* pAddOpts,
FileDetails* pDetails)
{
NuError nuerr = kNuErrNone;
DiskFS* pDiskFS = pAddOpts->fpTargetDiskFS;
DIError dierr;
int neededLen = 64; // reasonable guess
char* fsNormalBuf = nil; // name as it will appear on disk image
WMSG2(" +++ ADD file: orig='%ls' stor='%ls'\n",
(LPCWSTR) pDetails->origName, (LPCWSTR) pDetails->storageName);
retry:
/*
* Convert "storageName" to a filesystem-normalized path.
*/
delete[] fsNormalBuf;
fsNormalBuf = new char[neededLen];
CStringA storageNameA(pDetails->storageName);
dierr = pDiskFS->NormalizePath(storageNameA,
PathProposal::kDefaultStoredFssep, fsNormalBuf, &neededLen);
if (dierr == kDIErrDataOverrun) {
/* not long enough, try again *once* */
delete[] fsNormalBuf;
fsNormalBuf = new char[neededLen];
dierr = pDiskFS->NormalizePath(storageNameA,
PathProposal::kDefaultStoredFssep, fsNormalBuf, &neededLen);
}
if (dierr != kDIErrNone) {
nuerr = kNuErrInternal;
goto bail;
}
/*
* Test to see if the file already exists. If it does, give the user
* the opportunity to rename it, overwrite the original, or skip
* adding it.
*
* The FS-normalized path may not reflect the actual storage name,
* because some features (like ProDOS "allow lower case") aren't
* factored in until later. However, it should be close enough -- it
* has to be, or we'd be in trouble for saying it's going to overwrite
* the file in the archive.
*/
A2File* pExisting;
pExisting = pDiskFS->GetFileByName(fsNormalBuf);
if (pExisting != nil) {
NuResult result;
result = HandleReplaceExisting(pExisting, pDetails);
if (result == kNuAbort) {
nuerr = kNuErrAborted;
goto bail;
} else if (result == kNuSkip) {
nuerr = kNuErrSkipped;
goto bail;
} else if (result == kNuRename) {
goto retry;
} else if (result == kNuOverwrite) {
/* delete the existing file immediately */
WMSG1(" Deleting existing file '%hs'\n", fsNormalBuf);
dierr = pDiskFS->DeleteFile(pExisting);
if (dierr != kDIErrNone) {
// Would be nice to show a dialog and explain *why*, but
// I'm not sure we have a window here.
WMSG1(" Deletion failed (err=%d)\n", dierr);
goto bail;
}
} else {
WMSG1("GLITCH: bad return %d from HandleReplaceExisting\n",result);
assert(false);
nuerr = kNuErrInternal;
goto bail;
}
}
/*
* Put all the goodies into a new FileAddData object, and add it to
* the end of the list.
*/
FileAddData* pAddData;
pAddData = new FileAddData(pDetails, fsNormalBuf);
if (pAddData == nil) {
nuerr = kNuErrMalloc;
goto bail;
}
WMSG1("FSNormalized is '%hs'\n", pAddData->GetFSNormalPath());
AddToAddDataList(pAddData);
bail:
delete[] fsNormalBuf;
return nuerr;
}
/*
* A file we're adding clashes with an existing file. Decide what to do
* about it.
*
* Returns one of the following:
* kNuOverwrite - overwrite the existing file
* kNuSkip - skip adding the existing file
* kNuRename - user wants to rename the file
* kNuAbort - cancel out of the entire add process
*
* Side effects:
* Sets fOverwriteExisting and fOverwriteNoAsk if a "to all" button is hit
* Replaces pDetails->storageName if the user elects to rename
*/
NuResult
DiskArchive::HandleReplaceExisting(const A2File* pExisting,
FileDetails* pDetails)
{
NuResult result;
if (fOverwriteNoAsk) {
if (fOverwriteExisting)
return kNuOverwrite;
else
return kNuSkip;
}
ConfirmOverwriteDialog confOvwr;
confOvwr.fExistingFile = pExisting->GetPathName();
confOvwr.fExistingFileModWhen = pExisting->GetModWhen();
PathName srcPath(pDetails->origName);
confOvwr.fNewFileSource = pDetails->origName; // or storageName?
confOvwr.fNewFileModWhen = srcPath.GetModWhen();
if (confOvwr.DoModal() == IDCANCEL) {
WMSG0("User cancelled out of add-to-diskimg replace-existing\n");
return kNuAbort;
}
if (confOvwr.fResultRename) {
/*
* Replace the name in FileDetails. They were asked to modify
* the already-normalized version of the filename. We will run
* it back through the FS-specific normalizer, which will handle
* any oddities they type in.
*
* We don't want to run it through PathProposal.LocalToArchive
* because that'll strip out ':' in the pathnames.
*
* Ideally the rename dialog would have a way to validate the
* full path and reject "OK" if it's not valid. Instead, we just
* allow the FS normalizer to force the filename to be valid.
*/
pDetails->storageName = confOvwr.fExistingFile;
WMSG1("Trying rename to '%ls'\n", (LPCWSTR) pDetails->storageName);
return kNuRename;
}
if (confOvwr.fResultApplyToAll) {
fOverwriteNoAsk = true;
if (confOvwr.fResultOverwrite)
fOverwriteExisting = true;
else
fOverwriteExisting = false;
}
if (confOvwr.fResultOverwrite)
result = kNuOverwrite;
else
result = kNuSkip;
return result;
}
/*
* Process the list of pending file adds.
*
* This is where the rubber (finally!) meets the road.
*/
CString
DiskArchive::ProcessFileAddData(DiskFS* pDiskFS, int addOptsConvEOL)
{
CString errMsg;
FileAddData* pData;
unsigned char* dataBuf = nil;
unsigned char* rsrcBuf = nil;
long dataLen, rsrcLen;
MainWindow* pMainWin = (MainWindow*)::AfxGetMainWnd();
WMSG0("--- ProcessFileAddData\n");
/* map the EOL conversion to something we can use */
GenericEntry::ConvertEOL convEOL;
switch (addOptsConvEOL) {
case AddFilesDialog::kConvEOLNone:
convEOL = GenericEntry::kConvertEOLOff;
break;
case AddFilesDialog::kConvEOLType:
// will be adjusted each time through the loop
convEOL = GenericEntry::kConvertEOLOff;
break;
case AddFilesDialog::kConvEOLAuto:
convEOL = GenericEntry::kConvertEOLAuto;
break;
case AddFilesDialog::kConvEOLAll:
convEOL = GenericEntry::kConvertEOLOn;
break;
default:
assert(false);
convEOL = GenericEntry::kConvertEOLOff;
break;
}
pData = fpAddDataHead;
while (pData != nil) {
const FileDetails* pDataDetails = nil;
const FileDetails* pRsrcDetails = nil;
const FileDetails* pDetails = pData->GetDetails();
const char* typeStr = "????"; // for debug msg only
switch (pDetails->entryKind) {
case FileDetails::kFileKindDataFork:
pDataDetails = pDetails;
typeStr = "data";
break;
case FileDetails::kFileKindRsrcFork:
pRsrcDetails = pDetails;
typeStr = "rsrc";
break;
case FileDetails::kFileKindDiskImage:
pDataDetails = pDetails;
typeStr = "disk";
break;
case FileDetails::kFileKindBothForks:
case FileDetails::kFileKindDirectory:
default:
assert(false);
return L"internal error";
}
if (pData->GetOtherFork() != nil) {
pDetails = pData->GetOtherFork()->GetDetails();
typeStr = "both";
switch (pDetails->entryKind) {
case FileDetails::kFileKindDataFork:
assert(pDataDetails == nil);
pDataDetails = pDetails;
break;
case FileDetails::kFileKindRsrcFork:
assert(pRsrcDetails == nil);
pRsrcDetails = pDetails;
break;
case FileDetails::kFileKindDiskImage:
assert(false);
return L"(internal) add other disk error";
case FileDetails::kFileKindBothForks:
case FileDetails::kFileKindDirectory:
default:
assert(false);
return L"internal error";
}
}
WMSG2("Adding file '%ls' (%hs)\n",
(LPCWSTR) pDetails->storageName, typeStr);
ASSERT(pDataDetails != nil || pRsrcDetails != nil);
/*
* The current implementation of DiskImg/DiskFS requires writing each
* fork in one shot. This means loading the entire thing into
* memory. Not so bad for ProDOS, with its 16MB maximum file size,
* but it could be awkward for HFS (not to mention HFS Plus!).
*/
DiskFS::CreateParms parms;
ConvertFDToCP(pData->GetDetails(), &parms);
if (pRsrcDetails != nil)
parms.storageType = kNuStorageExtended;
else
parms.storageType = kNuStorageSeedling;
/* use the FS-normalized path here */
/* (do we have to? do we want to?) */
parms.pathName = pData->GetFSNormalPath();
dataLen = rsrcLen = -1;
if (pDataDetails != nil) {
/* figure out text conversion, including high ASCII for DOS */
/* (HA conversion only happens if text conversion happens) */
GenericEntry::ConvertHighASCII convHA;
if (addOptsConvEOL == AddFilesDialog::kConvEOLType) {
if (pDataDetails->fileType == kFileTypeTXT ||
pDataDetails->fileType == kFileTypeSRC)
{
WMSG0("Enabling text conversion by type\n");
convEOL = GenericEntry::kConvertEOLOn;
} else {
convEOL = GenericEntry::kConvertEOLOff;
}
}
if (DiskImg::UsesDOSFileStructure(pDiskFS->GetDiskImg()->GetFSFormat()))
convHA = GenericEntry::kConvertHAOn;
else
convHA = GenericEntry::kConvertHAOff;
errMsg = LoadFile(pDataDetails->origName, &dataBuf, &dataLen,
convEOL, convHA);
if (!errMsg.IsEmpty())
goto bail;
}
if (pRsrcDetails != nil) {
/* no text conversion on resource forks */
errMsg = LoadFile(pRsrcDetails->origName, &rsrcBuf, &rsrcLen,
GenericEntry::kConvertEOLOff, GenericEntry::kConvertHAOff);
if (!errMsg.IsEmpty())
goto bail;
}
/* really ought to do this separately for each thread */
SET_PROGRESS_BEGIN();
CString pathNameW(parms.pathName);
SET_PROGRESS_UPDATE2(0, pDetails->origName, pathNameW);
DIError dierr;
dierr = AddForksToDisk(pDiskFS, &parms, dataBuf, dataLen,
rsrcBuf, rsrcLen);
SET_PROGRESS_END();
if (dierr != kDIErrNone) {
errMsg.Format(L"Unable to add '%hs' to image: %hs.",
parms.pathName, DiskImgLib::DIStrError(dierr));
goto bail;
}
delete[] dataBuf;
delete[] rsrcBuf;
dataBuf = rsrcBuf = nil;
pData = pData->GetNext();
}
bail:
delete[] dataBuf;
delete[] rsrcBuf;
return errMsg;
}
#define kCharLF '\n'
#define kCharCR '\r'
/*
* Load a file into a buffer, possibly converting EOL markers and setting
* "high ASCII" along the way.
*
* Returns a pointer to a newly-allocated buffer (new[]) and the data length.
* If the file is empty, no buffer will be allocated.
*
* Returns an empty string on success, or an error message on failure.
*
* HEY: really ought to update the progress counter, especially when reading
* really large files.
*/
CString
DiskArchive::LoadFile(const WCHAR* pathName, BYTE** pBuf, long* pLen,
GenericEntry::ConvertEOL conv, GenericEntry::ConvertHighASCII convHA) const
{
CString errMsg;
FILE* fp;
long fileLen;
ASSERT(convHA == GenericEntry::kConvertHAOn ||
convHA == GenericEntry::kConvertHAOff);
ASSERT(conv == GenericEntry::kConvertEOLOn ||
conv == GenericEntry::kConvertEOLOff ||
conv == GenericEntry::kConvertEOLAuto);
ASSERT(pathName != nil);
ASSERT(pBuf != nil);
ASSERT(pLen != nil);
fp = _wfopen(pathName, L"rb");
if (fp == nil) {
errMsg.Format(L"Unable to open '%ls': %hs.", pathName,
strerror(errno));
goto bail;
}
if (fseek(fp, 0, SEEK_END) != 0) {
errMsg.Format(L"Unable to seek to end of '%ls': %hs", pathName,
strerror(errno));
goto bail;
}
fileLen = ftell(fp);
if (fileLen < 0) {
errMsg.Format(L"Unable to determine length of '%ls': %hs", pathName,
strerror(errno));
goto bail;
}
rewind(fp);
if (fileLen == 0) { // handle zero-length files
*pBuf = nil;
*pLen = 0;
goto bail;
} else if (fileLen > 0x00ffffff) {
errMsg = L"Cannot add files larger than 16MB to a disk image.";
goto bail;
}
*pBuf = new BYTE[fileLen];
if (*pBuf == nil) {
errMsg.Format(L"Unable to allocate %ld bytes for '%ls'.",
fileLen, pathName);
goto bail;
}
/*
* We're ready to load the file. We need to sort out EOL conversion.
* Since we always convert to CR, we know the file will stay the same
* size or get smaller, which means the buffer we've allocated is
* guaranteed to hold the file even if we convert it.
*
* If the text mode is "auto", we need to load a piece of the file and
* analyze it.
*/
if (conv == GenericEntry::kConvertEOLAuto) {
GenericEntry::EOLType eolType;
GenericEntry::ConvertHighASCII dummy;
int chunkLen = 16384; // nice big chunk
if (chunkLen > fileLen)
chunkLen = fileLen;
if (fread(*pBuf, chunkLen, 1, fp) != 1) {
errMsg.Format(L"Unable to read initial chunk of '%ls': %hs.",
pathName, strerror(errno));
delete[] *pBuf;
*pBuf = nil;
goto bail;
}
rewind(fp);
conv = GenericEntry::DetermineConversion(*pBuf, chunkLen,
&eolType, &dummy);
WMSG2("LoadFile DetermineConv returned conv=%d eolType=%d\n",
conv, eolType);
if (conv == GenericEntry::kConvertEOLOn &&
eolType == GenericEntry::kEOLCR)
{
WMSG0(" (skipping conversion due to matching eolType)\n");
conv = GenericEntry::kConvertEOLOff;
}
}
ASSERT(conv != GenericEntry::kConvertEOLAuto);
/*
* The "high ASCII" conversion is either on or off. In this context,
* "on" means "convert all text files", and "off" means "don't convert
* text files". We never convert non-text files. Conversion should
* always be "on" for DOS 3.2/3.3, and "off" for everything else (except
* RDOS, should we choose to make that writeable).
*/
if (conv == GenericEntry::kConvertEOLOff) {
/* fast path */
WMSG1(" +++ NOT converting text '%ls'\n", pathName);
if (fread(*pBuf, fileLen, 1, fp) != 1) {
errMsg.Format(L"Unable to read '%ls': %hs.", pathName, strerror(errno));
delete[] *pBuf;
*pBuf = nil;
goto bail;
}
} else {
/*
* Convert as we go.
*
* Observation: if we copy a binary file to a DOS disk, and force
* the text conversion, we will convert 0x0a to 0x0d, and thence
* to 0x8d. However, we may still have some 0x8a bytes lying around,
* because we don't convert 0x8a in the original file to anything.
* This means that a CR->CRLF or LF->CRLF conversion can't be
* "undone" on a DOS disk. (Not that anyone cares.)
*/
long count = fileLen;
int mask, ic;
bool lastCR = false;
unsigned char* buf = *pBuf;
if (convHA == GenericEntry::kConvertHAOn)
mask = 0x80;
else
mask = 0x00;
WMSG2(" +++ Converting text '%ls', mask=0x%02x\n", pathName, mask);
while (count--) {
ic = getc(fp);
if (ic == kCharCR) {
*buf++ = (unsigned char) (kCharCR | mask);
lastCR = true;
} else if (ic == kCharLF) {
if (!lastCR)
*buf++ = (unsigned char) (kCharCR | mask);
lastCR = false;
} else {
if (ic == '\0')
*buf++ = (unsigned char) ic; // don't conv 0x00
else
*buf++ = (unsigned char) (ic | mask);
lastCR = false;
}
}
fileLen = buf - *pBuf;
}
(void) fclose(fp);
*pLen = fileLen;
bail:
return errMsg;
}
/*
* Add a file with the supplied data to the disk image.
*
* Forks that exist but are empty have a length of zero. Forks that don't
* exist have a length of -1.
*
* Called by XferFile and ProcessFileAddData.
*/
DIError
DiskArchive::AddForksToDisk(DiskFS* pDiskFS, const DiskFS::CreateParms* pParms,
const unsigned char* dataBuf, long dataLen,
const unsigned char* rsrcBuf, long rsrcLen) const
{
DIError dierr = kDIErrNone;
const int kFileTypeBIN = 0x06;
const int kFileTypeINT = 0xfa;
const int kFileTypeBAS = 0xfc;
A2File* pNewFile = nil;
A2FileDescr* pOpenFile = nil;
DiskFS::CreateParms parmCopy;
/*
* Make a temporary copy, pointers and all, so we can rewrite some of
* the fields. This is sort of bad, because we're making copies of a
* const char* filename pointer whose underlying storage we're not
* really familiar with. However, so long as we don't try to retain
* it after this function returns we should be fine.
*
* Might be better to make CreateParms a class instead of a struct,
* make the pathName field new[] storage, and write a copy constructor
* for the operation below. This will be fine for now.
*/
memcpy(&parmCopy, pParms, sizeof(parmCopy));
if (rsrcLen >= 0) {
ASSERT(parmCopy.storageType == kNuStorageExtended);
}
/*
* Look for "empty directory holders" that we put into NuFX archives
* when doing disk-to-archive conversions. These make no sense if
* there's no fssep (because it's coming from DOS), or if there's no
* base path, so we can ignore those cases. We can also ignore it if
* the file is forked or is already a directory.
*/
if (parmCopy.fssep != '\0' && parmCopy.storageType == kNuStorageSeedling) {
const char* cp;
cp = strrchr(parmCopy.pathName, parmCopy.fssep);
if (cp != nil) {
if (strcmp(cp+1, kEmptyFolderMarker) == 0 && dataLen == 0) {
/* drop the junk on the end */
parmCopy.storageType = kNuStorageDirectory;
CStringA replacementFileName(parmCopy.pathName);;
replacementFileName =
replacementFileName.Left(cp - parmCopy.pathName -1);
parmCopy.pathName = replacementFileName;
parmCopy.fileType = 0x0f; // DIR
parmCopy.access &= ~(A2FileProDOS::kAccessInvisible);
dataLen = -1;
}
}
}
/*
* If this is a subdir create request (from the clipboard or an "empty
* directory placeholder" in a NuFX archive), handle it here. If we're
* on a filesystem that doesn't have subdirectories, just skip it.
*/
if (parmCopy.storageType == kNuStorageDirectory) {
A2File* pDummyFile;
ASSERT(dataLen < 0 && rsrcLen < 0);
if (DiskImg::IsHierarchical(pDiskFS->GetDiskImg()->GetFSFormat())) {
dierr = pDiskFS->CreateFile(&parmCopy, &pDummyFile);
if (dierr == kDIErrDirectoryExists)
dierr = kDIErrNone; // dirs are not made unique
goto bail;
} else {
WMSG0(" Ignoring subdir create req on non-hierarchic FS\n");
goto bail;
}
}
/* don't try to put resource forks onto a DOS disk */
if (!DiskImg::HasResourceForks(pDiskFS->GetDiskImg()->GetFSFormat())) {
if (rsrcLen >= 0) {
rsrcLen = -1;
parmCopy.storageType = kNuStorageSeedling;
if (dataLen < 0) {
/* this was a resource-fork-only file */
WMSG1("--- nothing left to write for '%hs'\n",
parmCopy.pathName);
goto bail;
}
} else {
ASSERT(parmCopy.storageType == kNuStorageSeedling);
}
}
/* quick kluge to get the right file type on large DOS files */
if (DiskImg::UsesDOSFileStructure(pDiskFS->GetDiskImg()->GetFSFormat()) &&
dataLen >= 65536)
{
if (parmCopy.fileType == kFileTypeBIN ||
parmCopy.fileType == kFileTypeINT ||
parmCopy.fileType == kFileTypeBAS)
{
WMSG0("+++ switching DOS file type to $f2\n");
parmCopy.fileType = 0xf2; // DOS 'S' file
}
}
/*
* Create the file on the disk. The storage type determines whether
* it has data+rsrc forks or just data (there's no such thing in
* ProDOS as "just a resource fork"). There's no need to open the
* fork if we're not going to write to it.
*
* This holds for resource forks as well, because the storage type
* determines whether or not the file is forked, and we've asserted
* that a file with a non-(-1) rsrcLen is forked.
*/
dierr = pDiskFS->CreateFile(&parmCopy, &pNewFile);
if (dierr != kDIErrNone) {
WMSG1(" CreateFile failed: %hs\n", DiskImgLib::DIStrError(dierr));
goto bail;
}
/*
* Note: if this was an empty directory holder, pNewFile will be set
* to nil. We used to avoid handling this by just not opening the file
* if it had a length of zero. However, DOS 3.3 needs to write some
* kinds of zero-length files, because e.g. a zero-length 'B' file
* actually has 4 bytes of data in it.
*
* Of course, if dataLen is zero then dataBuf is nil, so we need to
* supply a dummy write buffer. None of this is an issue for resource
* forks, because DOS 3.3 doesn't have those.
*/
if (dataLen > 0 ||
(dataLen == 0 && pNewFile != nil))
{
ASSERT(pNewFile != nil);
unsigned char dummyBuf[1] = { '\0' };
dierr = pNewFile->Open(&pOpenFile, false, false);
if (dierr != kDIErrNone)
goto bail;
pOpenFile->SetProgressUpdater(DiskArchive::ProgressCallback,
dataLen, nil);
dierr = pOpenFile->Write(dataBuf != nil ? dataBuf : dummyBuf, dataLen);
if (dierr != kDIErrNone)
goto bail;
dierr = pOpenFile->Close();
if (dierr != kDIErrNone)
goto bail;
pOpenFile = nil;
}
if (rsrcLen > 0) {
ASSERT(pNewFile != nil);
dierr = pNewFile->Open(&pOpenFile, false, true);
if (dierr != kDIErrNone)
goto bail;
pOpenFile->SetProgressUpdater(DiskArchive::ProgressCallback,
rsrcLen, nil);
dierr = pOpenFile->Write(rsrcBuf, rsrcLen);
if (dierr != kDIErrNone)
goto bail;
dierr = pOpenFile->Close();
if (dierr != kDIErrNone)
goto bail;
pOpenFile = nil;
}
bail:
if (pOpenFile != nil)
pOpenFile->Close();
if (dierr != kDIErrNone && pNewFile != nil) {
/*
* Clean up the partially-written file. This does not, of course,
* erase any subdirectories that were created to contain this file.
* Not worth worrying about.
*/
WMSG1(" Deleting newly-created file '%hs'\n", parmCopy.pathName);
(void) pDiskFS->DeleteFile(pNewFile);
}
return dierr;
}
/*
* Fill out a CreateParms structure from a FileDetails structure.
*
* The NuStorageType values correspond exactly to ProDOS storage types, so
* there's no need to convert them.
*/
void
DiskArchive::ConvertFDToCP(const FileDetails* pDetails,
DiskFS::CreateParms* pCreateParms)
{
// TODO(xyzzy): need to store 8-bit form
pCreateParms->pathName = "XYZZY-DiskArchive"; // pDetails->storageName;
pCreateParms->fssep = (char) pDetails->fileSysInfo;
pCreateParms->storageType = pDetails->storageType;
pCreateParms->fileType = pDetails->fileType;
pCreateParms->auxType = pDetails->extraType;
pCreateParms->access = pDetails->access;
pCreateParms->createWhen = NufxArchive::DateTimeToSeconds(&pDetails->createWhen);
pCreateParms->modWhen = NufxArchive::DateTimeToSeconds(&pDetails->modWhen);
}
/*
* Add an entry to the end of the FileAddData list.
*
* If "storageName" (the Windows filename with type goodies stripped, but
* without filesystem normalization) matches an entry already in the list,
* we check to see if these are forks of the same file. If they are
* different forks and we don't already have both forks, we put the
* pointer into the "fork pointer" of the existing file rather than adding
* it to the end of the list.
*/
void
DiskArchive::AddToAddDataList(FileAddData* pData)
{
ASSERT(pData != nil);
ASSERT(pData->GetNext() == nil);
/*
* Run through the entire existing list, looking for a match. This is
* O(n^2) behavior, but I'm expecting N to be relatively small (under
* 1000 in almost all cases).
*/
//if (strcasecmp(pData->GetDetails()->storageName, "system\\finder") == 0)
// WMSG0("whee\n");
FileAddData* pSearch = fpAddDataHead;
FileDetails::FileKind dataKind, listKind;
dataKind = pData->GetDetails()->entryKind;
while (pSearch != nil) {
if (pSearch->GetOtherFork() == nil &&
wcscmp(pSearch->GetDetails()->storageName,
pData->GetDetails()->storageName) == 0)
{
//NuThreadID dataID = pData->GetDetails()->threadID;
//NuThreadID listID = pSearch->GetDetails()->threadID;
listKind = pSearch->GetDetails()->entryKind;
/* got a name match */
if (dataKind != listKind &&
(dataKind == FileDetails::kFileKindDataFork || dataKind == FileDetails::kFileKindRsrcFork) &&
(listKind == FileDetails::kFileKindDataFork || listKind == FileDetails::kFileKindRsrcFork))
{
/* looks good, hook it in here instead of the list */
WMSG2("--- connecting forks of '%ls' and '%ls'\n",
pData->GetDetails()->origName,
pSearch->GetDetails()->origName);
pSearch->SetOtherFork(pData);
return;
}
}
pSearch = pSearch->GetNext();
}
if (fpAddDataHead == nil) {
assert(fpAddDataTail == nil);
fpAddDataHead = fpAddDataTail = pData;
} else {
fpAddDataTail->SetNext(pData);
fpAddDataTail = pData;
}
}
/*
* Free all entries in the FileAddData list.
*/
void
DiskArchive::FreeAddDataList(void)
{
FileAddData* pData;
FileAddData* pNext;
pData = fpAddDataHead;
while (pData != nil) {
pNext = pData->GetNext();
delete pData->GetOtherFork();
delete pData;
pData = pNext;
}
fpAddDataHead = fpAddDataTail = nil;
}
/*
* ===========================================================================
* DiskArchive -- create subdir
* ===========================================================================
*/
/*
* Create a subdirectory named "newName" in "pParentEntry".
*/
bool
DiskArchive::CreateSubdir(CWnd* pMsgWnd, GenericEntry* pParentEntry,
const WCHAR* newName)
{
ASSERT(newName != nil && wcslen(newName) > 0);
DiskEntry* pEntry = (DiskEntry*) pParentEntry;
ASSERT(pEntry != nil);
A2File* pFile = pEntry->GetA2File();
ASSERT(pFile != nil);
DiskFS* pDiskFS = pFile->GetDiskFS();
ASSERT(pDiskFS != nil);
if (!pFile->IsDirectory()) {
ASSERT(false);
return false;
}
DIError dierr;
A2File* pNewFile = nil;
DiskFS::CreateParms parms;
CStringA pathName;
time_t now = time(nil);
/*
* Create the full path.
*/
if (pFile->IsVolumeDirectory()) {
pathName = newName;
} else {
pathName = pParentEntry->GetPathName();
pathName += pParentEntry->GetFssep();
pathName += newName;
}
ASSERT(wcschr(newName, pParentEntry->GetFssep()) == nil);
/* using NufxLib constants; they match with ProDOS */
memset(&parms, 0, sizeof(parms));
parms.pathName = pathName;
parms.fssep = pParentEntry->GetFssep();
parms.storageType = kNuStorageDirectory;
parms.fileType = 0x0f; // ProDOS DIR
parms.auxType = 0;
parms.access = kNuAccessUnlocked;
parms.createWhen = now;
parms.modWhen = now;
dierr = pDiskFS->CreateFile(&parms, &pNewFile);
if (dierr != kDIErrNone) {
CString errMsg;
errMsg.Format(L"Unable to create subdirectory: %hs.\n",
DiskImgLib::DIStrError(dierr));
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
return false;
}
if (InternalReload(pMsgWnd) != 0)
return false;
return true;
}
/*
* ===========================================================================
* DiskArchive -- delete selection
* ===========================================================================
*/
/*
* Compare DiskEntry display names in descending order (Z-A).
*/
/*static*/ int
DiskArchive::CompareDisplayNamesDesc(const void* ventry1, const void* ventry2)
{
const DiskEntry* pEntry1 = *((const DiskEntry**) ventry1);
const DiskEntry* pEntry2 = *((const DiskEntry**) ventry2);
return wcsicmp(pEntry2->GetDisplayName(), pEntry1->GetDisplayName());
}
/*
* Delete the records listed in the selection set.
*
* The DiskFS DeleteFile() function will not delete a subdirectory unless
* it is empty. This complicates matters somewhat for us, because the
* selection set isn't in any particular order. We need to sort on the
* pathname and then delete bottom-up.
*
* CiderPress does work to ensure that, if a subdir is selected, everything
* in that subdir is also selected. So if we just delete everything in the
* right order, we should be okay.
*/
bool
DiskArchive::DeleteSelection(CWnd* pMsgWnd, SelectionSet* pSelSet)
{
CString errMsg;
SelectionEntry* pSelEntry;
DiskEntry* pEntry;
DIError dierr;
bool retVal = false;
SET_PROGRESS_BEGIN();
/*
* Start by copying the DiskEntry pointers out of the selection set and
* into an array. The selection set was created such that there is one
* entry in the set for each file. (The file viewer likes to have one
* entry for each thread.)
*/
int numEntries = pSelSet->GetNumEntries();
ASSERT(numEntries > 0);
DiskEntry** entryArray = new DiskEntry*[numEntries];
int idx = 0;
pSelEntry = pSelSet->IterNext();
while (pSelEntry != nil) {
pEntry = (DiskEntry*) pSelEntry->GetEntry();
ASSERT(pEntry != nil);
entryArray[idx++] = pEntry;
WMSG2("Added 0x%08lx '%ls'\n", (long) entryArray[idx-1],
(LPCWSTR) entryArray[idx-1]->GetDisplayName());
pSelEntry = pSelSet->IterNext();
}
ASSERT(idx == numEntries);
/*
* Sort the file array by descending filename.
*/
::qsort(entryArray, numEntries, sizeof(DiskEntry*), CompareDisplayNamesDesc);
/*
* Run through the sorted list, deleting each entry.
*/
for (idx = 0; idx < numEntries; idx++) {
A2File* pFile;
pEntry = entryArray[idx];
pFile = pEntry->GetA2File();
/*
* We shouldn't be here at all if the main volume were opened
* read-only. However, it's possible that the main is read-write
* and our sub-volumes are read-only (probably because we don't
* support write access to the filesystem).
*/
if (!pFile->GetDiskFS()->GetReadWriteSupported()) {
errMsg.Format(L"Unable to delete '%ls' on '%hs': operation not supported.",
(LPCWSTR) pEntry->GetDisplayName(),
(LPCSTR) pFile->GetDiskFS()->GetVolumeName());
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
WMSG2(" Deleting '%ls' from '%hs'\n", (LPCWSTR) pEntry->GetPathName(),
(LPCSTR) pFile->GetDiskFS()->GetVolumeName());
SET_PROGRESS_UPDATE2(0, pEntry->GetPathName(), nil);
/*
* Ask the DiskFS to delete the file. As soon as this completes,
* "pFile" is invalid and must not be dereferenced.
*/
dierr = pFile->GetDiskFS()->DeleteFile(pFile);
if (dierr != kDIErrNone) {
errMsg.Format(L"Unable to delete '%ls' on '%hs': %hs.",
(LPCWSTR) pEntry->GetDisplayName(),
(LPCSTR) pFile->GetDiskFS()->GetVolumeName(),
DiskImgLib::DIStrError(dierr));
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
SET_PROGRESS_UPDATE(100);
/*
* Be paranoid and zap the pointer, on the off chance somebody
* tries to redraw the content list from the deleted data.
*
* In practice we don't work this way -- the stuff that gets drawn
* on the screen comes out of GenericEntry, not A2File. If this
* changes we'll need to raise the "reload" flag here, before the
* reload, to prevent the ContentList from chasing a bad pointer.
*/
pEntry->SetA2File(nil);
}
retVal = true;
bail:
SET_PROGRESS_END();
delete[] entryArray;
if (InternalReload(pMsgWnd) != 0)
retVal = false;
return retVal;
}
/*
* ===========================================================================
* DiskArchive -- rename files
* ===========================================================================
*/
/*
* Rename a set of files, one at a time.
*
* If we rename a subdirectory, it could affect the next thing we try to
* rename (because we show the full path). We have to reload our file
* list from the DiskFS after each renamed subdir. The trouble is that
* this invalidates the data displayed in the ContentList, and we won't
* redraw the screen correctly. We can work around the problem by getting
* the pathname directly from the DiskFS instead of from DiskEntry, though
* it's not immediately obvious which is less confusing.
*/
bool
DiskArchive::RenameSelection(CWnd* pMsgWnd, SelectionSet* pSelSet)
{
CString errMsg;
bool retVal = false;
WMSG1("Renaming %d entries\n", pSelSet->GetNumEntries());
/*
* For each item in the selection set, bring up the "rename" dialog,
* and ask the GenericEntry to process it.
*
* If they hit "cancel" or there's an error, we still flush the
* previous changes. This is so that we don't have to create the
* same sort of deferred-write feature when renaming things in other
* sorts of archives (e.g. disk archives).
*/
SelectionEntry* pSelEntry = pSelSet->IterNext();
while (pSelEntry != nil) {
RenameEntryDialog renameDlg(pMsgWnd);
DiskEntry* pEntry = (DiskEntry*) pSelEntry->GetEntry();
WMSG1(" Renaming '%ls'\n", pEntry->GetPathName());
if (!SetRenameFields(pMsgWnd, pEntry, &renameDlg))
break;
int result;
if (pEntry->GetA2File()->IsVolumeDirectory())
result = IDIGNORE; // don't allow rename of volume dir
else
result = renameDlg.DoModal();
if (result == IDOK) {
DIError dierr;
DiskFS* pDiskFS;
A2File* pFile;
pFile = pEntry->GetA2File();
pDiskFS = pFile->GetDiskFS();
CStringA newNameA(renameDlg.fNewName);
dierr = pDiskFS->RenameFile(pFile, newNameA);
if (dierr != kDIErrNone) {
errMsg.Format(L"Unable to rename '%ls' to '%ls': %hs.",
pEntry->GetPathName(), renameDlg.fNewName,
DiskImgLib::DIStrError(dierr));
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
WMSG2("Rename of '%ls' to '%ls' succeeded\n",
pEntry->GetDisplayName(), renameDlg.fNewName);
} else if (result == IDCANCEL) {
WMSG0("Canceling out of remaining renames\n");
break;
} else {
/* 3rd possibility is IDIGNORE, i.e. skip this entry */
WMSG1("Skipping rename of '%ls'\n", pEntry->GetDisplayName());
}
pSelEntry = pSelSet->IterNext();
}
/* reload GenericArchive from disk image */
if (InternalReload(pMsgWnd) == kNuErrNone)
retVal = true;
bail:
return retVal;
}
/*
* Set up a RenameEntryDialog for the entry in "*pEntry".
*
* Returns "true" on success, "false" on failure.
*/
bool
DiskArchive::SetRenameFields(CWnd* pMsgWnd, DiskEntry* pEntry,
RenameEntryDialog* pDialog)
{
DiskFS* pDiskFS;
ASSERT(pEntry != nil);
/*
* Figure out if we're allowed to change the entire path. (This is
* doing it the hard way, but what the hell.)
*/
long cap = GetCapability(GenericArchive::kCapCanRenameFullPath);
bool renameFullPath = (cap != 0);
// a bit round-about, but it works
pDiskFS = pEntry->GetA2File()->GetDiskFS();
/*
* Make sure rename is allowed. It's nice to do these *before* putting
* up the rename dialog, so that the user doesn't do a bunch of typing
* before being told that it's pointless.
*/
if (!pDiskFS->GetReadWriteSupported()) {
CString errMsg;
errMsg.Format(L"Unable to rename '%ls': operation not supported.",
pEntry->GetPathName());
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
return false;
}
if (pDiskFS->GetFSDamaged()) {
CString errMsg;
errMsg.Format(L"Unable to rename '%ls': the disk it's on appears to be damaged.",
pEntry->GetPathName());
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
return false;
}
pDialog->SetCanRenameFullPath(renameFullPath);
pDialog->fOldName = pEntry->GetPathName();
pDialog->fFssep = pEntry->GetFssep();
pDialog->fpArchive = this;
pDialog->fpEntry = pEntry;
return true;
}
/*
* Verify that the a name is suitable. Called by RenameEntryDialog and
* CreateSubdirDialog.
*
* Tests for context-specific syntax and checks for duplicates.
*
* Returns an empty string on success, or an error message on failure.
*/
CString
DiskArchive::TestPathName(const GenericEntry* pGenericEntry,
const CString& basePath, const CString& newName, char newFssep) const
{
const DiskEntry* pEntry = (DiskEntry*) pGenericEntry;
DiskImg::FSFormat format;
CStringA pathName, errMsg, newNameA;
DiskFS* pDiskFS;
if (basePath.IsEmpty()) {
pathName = newName;
} else {
pathName = basePath;
pathName += newFssep;
pathName += newName;
}
pDiskFS = pEntry->GetA2File()->GetDiskFS();
format = pDiskFS->GetDiskImg()->GetFSFormat();
/* look for an existing file, but don't compare against self */
A2File* existingFile;
CStringA pathNameA(pathName);
existingFile = pDiskFS->GetFileByName(pathNameA);
if (existingFile != nil && existingFile != pEntry->GetA2File()) {
errMsg = "A file with that name already exists.";
goto bail;
}
newNameA = newName;
switch (format) {
case DiskImg::kFormatProDOS:
if (!DiskFSProDOS::IsValidFileName(newNameA))
errMsg.LoadString(IDS_VALID_FILENAME_PRODOS);
break;
case DiskImg::kFormatDOS33:
case DiskImg::kFormatDOS32:
if (!DiskFSDOS33::IsValidFileName(newNameA))
errMsg.LoadString(IDS_VALID_FILENAME_DOS);
break;
case DiskImg::kFormatPascal:
if (!DiskFSPascal::IsValidFileName(newNameA))
errMsg.LoadString(IDS_VALID_FILENAME_PASCAL);
break;
case DiskImg::kFormatMacHFS:
if (!DiskFSHFS::IsValidFileName(newNameA))
errMsg.LoadString(IDS_VALID_FILENAME_HFS);
break;
default:
errMsg = L"Not supported by TestPathName!";
}
bail:
return errMsg;
}
/*
* ===========================================================================
* DiskArchive -- rename a volume
* ===========================================================================
*/
/*
* Ask a DiskFS to change its volume name.
*
* Returns "true" on success, "false" on failure.
*/
bool
DiskArchive::RenameVolume(CWnd* pMsgWnd, DiskFS* pDiskFS,
const WCHAR* newName)
{
DIError dierr;
CString errMsg;
bool retVal = true;
CStringA newNameA(newName);
dierr = pDiskFS->RenameVolume(newNameA);
if (dierr != kDIErrNone) {
errMsg.Format(L"Unable to rename volume: %hs.\n",
DiskImgLib::DIStrError(dierr));
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
retVal = false;
/* fall through to reload anyway */
}
/* reload GenericArchive from disk image */
if (InternalReload(pMsgWnd) != 0)
retVal = false;
return retVal;
}
/*
* Test a volume name for validity.
*/
CString
DiskArchive::TestVolumeName(const DiskFS* pDiskFS,
const WCHAR* newName) const
{
DiskImg::FSFormat format;
CString errMsg;
ASSERT(pDiskFS != nil);
ASSERT(newName != nil);
format = pDiskFS->GetDiskImg()->GetFSFormat();
CStringA newNameA(newName);
switch (format) {
case DiskImg::kFormatProDOS:
if (!DiskFSProDOS::IsValidVolumeName(newNameA))
errMsg.LoadString(IDS_VALID_VOLNAME_PRODOS);
break;
case DiskImg::kFormatDOS33:
case DiskImg::kFormatDOS32:
if (!DiskFSDOS33::IsValidVolumeName(newNameA))
errMsg.LoadString(IDS_VALID_VOLNAME_DOS);
break;
case DiskImg::kFormatPascal:
if (!DiskFSPascal::IsValidVolumeName(newNameA))
errMsg.LoadString(IDS_VALID_VOLNAME_PASCAL);
break;
case DiskImg::kFormatMacHFS:
if (!DiskFSHFS::IsValidVolumeName(newNameA))
errMsg.LoadString(IDS_VALID_VOLNAME_HFS);
break;
default:
errMsg = L"Not supported by TestVolumeName!";
}
return errMsg;
}
/*
* ===========================================================================
* DiskArchive -- set file properties
* ===========================================================================
*/
/*
* Set the properties of "pEntry" to what's in "pProps".
*
* [currently only supports file type, aux type, and access flags]
*
* Technically we should reload the GenericArchive from the NufxArchive,
* but the set of changes is pretty small, so we just make them here.
*/
bool
DiskArchive::SetProps(CWnd* pMsgWnd, GenericEntry* pGenericEntry,
const FileProps* pProps)
{
DIError dierr;
DiskEntry* pEntry = (DiskEntry*) pGenericEntry;
A2File* pFile = pEntry->GetA2File();
dierr = pFile->GetDiskFS()->SetFileInfo(pFile, pProps->fileType,
pProps->auxType, pProps->access);
if (dierr != kDIErrNone) {
CString errMsg;
errMsg.Format(L"Unable to set file info: %hs.\n",
DiskImgLib::DIStrError(dierr));
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
return false;
}
/* do this in lieu of reloading GenericArchive */
pEntry->SetFileType(pFile->GetFileType());
pEntry->SetAuxType(pFile->GetAuxType());
pEntry->SetAccess(pFile->GetAccess());
/* DOS 3.2/3.3 may change these as well */
DiskImg::FSFormat fsFormat;
fsFormat = pFile->GetDiskFS()->GetDiskImg()->GetFSFormat();
if (fsFormat == DiskImg::kFormatDOS32 || fsFormat == DiskImg::kFormatDOS33) {
WMSG0(" (reloading additional fields after DOS SFI)\n");
pEntry->SetDataForkLen(pFile->GetDataLength());
pEntry->SetCompressedLen(pFile->GetDataSparseLength());
pEntry->SetSuspicious(pFile->GetQuality() == A2File::kQualitySuspicious);
}
/* clear the dirty flag in trivial cases */
(void) fpPrimaryDiskFS->Flush(DiskImg::kFlushFastOnly);
return true;
}
/*
* ===========================================================================
* DiskArchive -- transfer files to another archive
* ===========================================================================
*/
/*
* Transfer the selected files out of this archive and into another.
*
* In this case, it's files on a disk (with unspecified filesystem) to a NuFX
* archive. We get the open archive pointer and some options from "pXferOpts".
*
* The selection set was created with the "any" selection criteria, which
* means there's only one entry for each file regardless of whether it's
* forked or not.
*/
GenericArchive::XferStatus
DiskArchive::XferSelection(CWnd* pMsgWnd, SelectionSet* pSelSet,
ActionProgressDialog* pActionProgress, const XferFileOptions* pXferOpts)
{
WMSG0("DiskArchive XferSelection!\n");
unsigned char* dataBuf = nil;
unsigned char* rsrcBuf = nil;
FileDetails fileDetails;
CString errMsg, extractErrMsg, cmpStr;
CString fixedPathName;
XferStatus retval = kXferFailed;
pXferOpts->fTarget->XferPrepare(pXferOpts);
SelectionEntry* pSelEntry = pSelSet->IterNext();
for ( ; pSelEntry != nil; pSelEntry = pSelSet->IterNext()) {
long dataLen=-1, rsrcLen=-1;
DiskEntry* pEntry = (DiskEntry*) pSelEntry->GetEntry();
int typeOverride = -1;
int result;
ASSERT(dataBuf == nil);
ASSERT(rsrcBuf == nil);
if (pEntry->GetDamaged()) {
WMSG1(" XFER skipping damaged entry '%ls'\n",
pEntry->GetDisplayName());
continue;
}
/*
* Do a quick de-colonizing pass for non-ProDOS volumes, then prepend
* the subvolume name (if any).
*/
fixedPathName = pEntry->GetPathName();
if (fixedPathName.IsEmpty())
fixedPathName = _T("(no filename)");
if (pEntry->GetFSFormat() != DiskImg::kFormatProDOS)
fixedPathName.Replace(PathProposal::kDefaultStoredFssep, '.');
if (pEntry->GetSubVolName() != nil) {
CString tmpStr;
tmpStr = pEntry->GetSubVolName();
tmpStr += (char)PathProposal::kDefaultStoredFssep;
tmpStr += fixedPathName;
fixedPathName = tmpStr;
}
if (pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir) {
/* this is the volume dir */
WMSG1(" XFER not transferring volume dir '%ls'\n",
(LPCWSTR) fixedPathName);
continue;
} else if (pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory) {
if (pXferOpts->fPreserveEmptyFolders) {
/* if this is an empty directory, create a fake entry */
cmpStr = fixedPathName;
cmpStr += (char)PathProposal::kDefaultStoredFssep;
if (pSelSet->CountMatchingPrefix(cmpStr) == 0) {
WMSG1("FOUND empty dir '%ls'\n", (LPCWSTR) fixedPathName);
cmpStr += kEmptyFolderMarker;
dataBuf = new unsigned char[1];
dataLen = 0;
fileDetails.entryKind = FileDetails::kFileKindDataFork;
fileDetails.storageName = cmpStr;
fileDetails.fileType = 0; // NON
fileDetails.access =
pEntry->GetAccess() | GenericEntry::kAccessInvisible;
goto have_stuff2;
} else {
WMSG1("NOT empty dir '%ls'\n", (LPCWSTR) fixedPathName);
}
}
WMSG1(" XFER not transferring directory '%ls'\n",
(LPCWSTR) fixedPathName);
continue;
}
WMSG3(" Xfer '%ls' (data=%d rsrc=%d)\n",
(LPCWSTR) fixedPathName, pEntry->GetHasDataFork(),
pEntry->GetHasRsrcFork());
dataBuf = nil;
dataLen = 0;
result = pEntry->ExtractThreadToBuffer(GenericEntry::kDataThread,
(char**) &dataBuf, &dataLen, &extractErrMsg);
if (result == IDCANCEL) {
WMSG0("Cancelled during data extract!\n");
goto bail; /* abort anything that was pending */
} else if (result != IDOK) {
errMsg.Format(L"Failed while extracting '%ls': %ls.",
fixedPathName, extractErrMsg);
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
ASSERT(dataBuf != nil);
ASSERT(dataLen >= 0);
#if 0
if (pXferOpts->fConvDOSText &&
DiskImg::UsesDOSFileStructure(pEntry->GetFSFormat()) &&
pEntry->GetFileType() == kFileTypeTXT)
{
/* don't need to convert EOL, so just strip in place */
long len;
unsigned char* ucp;
WMSG1(" Converting DOS text in '%ls'\n", fixedPathName);
for (ucp = dataBuf, len = dataLen; len > 0; len--, ucp++)
*ucp = *ucp & 0x7f;
}
#endif
#if 0 // annoying to invoke PTX reformatter from here... ReformatHolder, etc.
if (pXferOpts->fConvPascalText &&
pEntry->GetFSFormat() == DiskImg::kFormatPascal &&
pEntry->GetFileType() == kFileTypePTX)
{
WMSG1("WOULD CONVERT ptx '%ls'\n", fixedPathName);
}
#endif
if (pEntry->GetHasRsrcFork()) {
rsrcBuf = nil;
rsrcLen = 0;
result = pEntry->ExtractThreadToBuffer(GenericEntry::kRsrcThread,
(char**) &rsrcBuf, &rsrcLen, &extractErrMsg);
if (result == IDCANCEL) {
WMSG0("Cancelled during rsrc extract!\n");
goto bail; /* abort anything that was pending */
} else if (result != IDOK) {
errMsg.Format(L"Failed while extracting '%ls': %ls.",
fixedPathName, extractErrMsg);
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
} else {
ASSERT(rsrcBuf == nil);
}
if (pEntry->GetHasDataFork() && pEntry->GetHasRsrcFork())
fileDetails.entryKind = FileDetails::kFileKindBothForks;
else if (pEntry->GetHasDataFork())
fileDetails.entryKind = FileDetails::kFileKindDataFork;
else if (pEntry->GetHasRsrcFork())
fileDetails.entryKind = FileDetails::kFileKindRsrcFork;
else {
ASSERT(false);
fileDetails.entryKind = FileDetails::kFileKindUnknown;
}
/*
* Set up the FileDetails.
*/
fileDetails.storageName = fixedPathName;
fileDetails.fileType = pEntry->GetFileType();
fileDetails.access = pEntry->GetAccess();
have_stuff2:
fileDetails.fileSysFmt = pEntry->GetSourceFS();
fileDetails.fileSysInfo = PathProposal::kDefaultStoredFssep;
fileDetails.extraType = pEntry->GetAuxType();
fileDetails.storageType = kNuStorageUnknown; /* let NufxLib deal */
time_t when;
when = time(nil);
UNIXTimeToDateTime(&when, &fileDetails.archiveWhen);
when = pEntry->GetModWhen();
UNIXTimeToDateTime(&when, &fileDetails.modWhen);
when = pEntry->GetCreateWhen();
UNIXTimeToDateTime(&when, &fileDetails.createWhen);
pActionProgress->SetArcName(fileDetails.storageName);
if (pActionProgress->SetProgress(0) == IDCANCEL) {
retval = kXferCancelled;
goto bail;
}
errMsg = pXferOpts->fTarget->XferFile(&fileDetails, &dataBuf, dataLen,
&rsrcBuf, rsrcLen);
if (!errMsg.IsEmpty()) {
WMSG0("XferFile failed!\n");
errMsg.Format(L"Failed while transferring '%ls': %ls.",
(LPCWSTR) pEntry->GetDisplayName(), (LPCWSTR) errMsg);
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
ASSERT(dataBuf == nil);
ASSERT(rsrcBuf == nil);
if (pActionProgress->SetProgress(100) == IDCANCEL) {
retval = kXferCancelled;
goto bail;
}
}
//MainWindow* pMainWin;
//pMainWin = (MainWindow*)::AfxGetMainWnd();
//pMainWin->EventPause(1000);
retval = kXferOK;
bail:
if (retval != kXferOK)
pXferOpts->fTarget->XferAbort(pMsgWnd);
else
pXferOpts->fTarget->XferFinish(pMsgWnd);
delete[] dataBuf;
delete[] rsrcBuf;
return retval;
}
/*
* Prepare for file transfers.
*/
void
DiskArchive::XferPrepare(const XferFileOptions* pXferOpts)
{
WMSG0("DiskArchive::XferPrepare\n");
//fpPrimaryDiskFS->SetParameter(DiskFS::kParmProDOS_AllowLowerCase,
// pXferOpts->fAllowLowerCase);
//fpPrimaryDiskFS->SetParameter(DiskFS::kParmProDOS_AllocSparse,
// pXferOpts->fUseSparseBlocks);
fpPrimaryDiskFS->SetParameter(DiskFS::kParm_CreateUnique, true);
//fXferStoragePrefix = pXferOpts->fStoragePrefix;
fpXferTargetFS = pXferOpts->fpTargetFS;
}
/*
* Transfer a file to the disk image. Called from NufxArchive's XferSelection
* and clipboard "paste".
*
* "dataLen" and "rsrcLen" will be -1 if the corresponding fork doesn't
* exist.
*
* Returns 0 on success, nonzero on failure.
*
* On success, *pDataBuf and *pRsrcBuf are freed and set to nil. (It's
* necessary for the interface to work this way because the NufxArchive
* version just tucks the pointers into NufxLib structures.)
*/
CString
DiskArchive::XferFile(FileDetails* pDetails, BYTE** pDataBuf,
long dataLen, BYTE** pRsrcBuf, long rsrcLen)
{
//const int kFileTypeTXT = 0x04;
DiskFS::CreateParms createParms;
DiskFS* pDiskFS;
CString errMsg;
DIError dierr = kDIErrNone;
WMSG3(" XFER: transfer '%ls' (dataLen=%ld rsrcLen=%ld)\n",
pDetails->storageName, dataLen, rsrcLen);
ASSERT(pDataBuf != nil);
ASSERT(pRsrcBuf != nil);
/* fill out CreateParms from FileDetails */
ConvertFDToCP(pDetails, &createParms);
if (fpXferTargetFS == nil)
pDiskFS = fpPrimaryDiskFS;
else
pDiskFS = fpXferTargetFS;
/*
* Strip the high ASCII from DOS and RDOS text files, unless we're adding
* them to a DOS disk. Likewise, if we're adding non-DOS text files to
* a DOS disk, we need to add the high bit.
*
* DOS converts both TXT and SRC to 'T', so we have to handle both here.
* Ideally we'd just ask DOS, "do you think this is a text file?", but it's
* not worth adding a new interface just for that.
*/
bool srcIsDOS, dstIsDOS;
srcIsDOS = DiskImg::UsesDOSFileStructure(pDetails->fileSysFmt);
dstIsDOS = DiskImg::UsesDOSFileStructure(pDiskFS->GetDiskImg()->GetFSFormat());
if (dataLen > 0 &&
(pDetails->fileType == kFileTypeTXT || pDetails->fileType == kFileTypeSRC))
{
unsigned char* ucp = *pDataBuf;
long len = dataLen;
if (srcIsDOS && !dstIsDOS) {
WMSG1(" Stripping high ASCII from '%ls'\n", pDetails->storageName);
while (len--)
*ucp++ &= 0x7f;
} else if (!srcIsDOS && dstIsDOS) {
WMSG1(" Adding high ASCII to '%ls'\n", pDetails->storageName);
while (len--) {
if (*ucp != '\0')
*ucp |= 0x80;
ucp++;
}
} else if (srcIsDOS && dstIsDOS) {
WMSG1(" --- not altering DOS-to-DOS text '%ls'\n",
pDetails->storageName);
} else {
WMSG1(" --- non-DOS transfer '%ls'\n", pDetails->storageName);
}
}
/* add a file with one or two forks */
if (createParms.storageType == kNuStorageDirectory) {
ASSERT(dataLen < 0 && rsrcLen < 0);
} else {
ASSERT(dataLen >= 0 || rsrcLen >= 0); // at least one fork
}
/* if we still have something to write, write it */
dierr = AddForksToDisk(pDiskFS, &createParms, *pDataBuf, dataLen,
*pRsrcBuf, rsrcLen);
if (dierr != kDIErrNone) {
errMsg.Format(L"%hs", DiskImgLib::DIStrError(dierr));
goto bail;
}
/* clean up */
delete[] *pDataBuf;
*pDataBuf = nil;
delete[] *pRsrcBuf;
*pRsrcBuf = nil;
bail:
return errMsg;
}
/*
* Abort our progress. Not really possible, except by throwing the disk
* image away.
*/
void
DiskArchive::XferAbort(CWnd* pMsgWnd)
{
WMSG0("DiskArchive::XferAbort\n");
InternalReload(pMsgWnd);
}
/*
* Transfer is finished.
*/
void
DiskArchive::XferFinish(CWnd* pMsgWnd)
{
WMSG0("DiskArchive::XferFinish\n");
InternalReload(pMsgWnd);
}