mirror of
https://github.com/fadden/ciderpress.git
synced 2024-11-27 08:49:20 +00:00
d23a3b1ad8
The NufxLib and diskimg libraries want narrow strings for certain things, notably for the "storage name", i.e. how the name will appear on the disk image or in the file archive. We need to convert from Windows UTF-16 to an Apple II filesystem-specific 8-bit character representation. We used to just pass narrow strings all the way through, so we didn't need any intermediate storage to hold the conversion. Now we do. In some cases there's nowhere good to put it. The initial UTF-16 conversion changes just dropped in some place-holder strings. This corrects the behavior, though in a couple of cases we're adding kluges on top of code that was already badly bent from its original intent (as initially conceived, CiderPress wasn't going to handle disk images, just ShrinkIt archives). It's not pretty, but it should work for now.
2882 lines
94 KiB
C++
2882 lines
94 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
|
|
* ===========================================================================
|
|
*/
|
|
|
|
int DiskEntry::ExtractThreadToBuffer(int which, char** ppText, long* pLength,
|
|
CString* pErrMsg) const
|
|
{
|
|
DIError dierr;
|
|
A2FileDescr* pOpenFile = NULL;
|
|
char* dataBuf = NULL;
|
|
bool rsrcFork;
|
|
bool needAlloc = true;
|
|
int result = -1;
|
|
|
|
ASSERT(fpFile != NULL);
|
|
ASSERT(pErrMsg != NULL);
|
|
*pErrMsg = "";
|
|
|
|
if (*ppText != NULL)
|
|
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, NULL);
|
|
|
|
if (needAlloc) {
|
|
dataBuf = new char[(int) len];
|
|
if (dataBuf == NULL) {
|
|
pErrMsg->Format(L"ERROR: allocation of %I64d 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 != NULL)
|
|
pOpenFile->Close();
|
|
if (result == IDOK) {
|
|
SET_PROGRESS_END();
|
|
ASSERT(pErrMsg->IsEmpty());
|
|
} else {
|
|
ASSERT(result == IDCANCEL || !pErrMsg->IsEmpty());
|
|
if (needAlloc) {
|
|
delete[] dataBuf;
|
|
ASSERT(*ppText == NULL);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
int DiskEntry::ExtractThreadToFile(int which, FILE* outfp, ConvertEOL conv,
|
|
ConvertHighASCII convHA, CString* pErrMsg) const
|
|
{
|
|
A2FileDescr* pOpenFile = NULL;
|
|
bool rsrcFork;
|
|
int result = -1;
|
|
|
|
ASSERT(IDOK != -1 && IDCANCEL != -1);
|
|
ASSERT(fpFile != NULL);
|
|
|
|
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) {
|
|
LOGI("Empty fork");
|
|
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 != NULL)
|
|
pOpenFile->Close();
|
|
return result;
|
|
}
|
|
|
|
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, NULL);
|
|
|
|
/*
|
|
* 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;
|
|
}
|
|
|
|
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:
|
|
LOGI("Unexpected feature flag %d", feature);
|
|
assert(false);
|
|
return false;
|
|
}
|
|
|
|
assert(false);
|
|
return false;
|
|
}
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* DiskArchive
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*static*/ CString DiskArchive::AppInit(void)
|
|
{
|
|
CString result("");
|
|
DIError dierr;
|
|
long major, minor, bug;
|
|
|
|
LOGI("Initializing DiskImg library");
|
|
|
|
// 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;
|
|
}
|
|
|
|
/*static*/ void DiskArchive::AppCleanup(void)
|
|
{
|
|
DiskImgLib::Global::AppCleanup();
|
|
}
|
|
|
|
/*static*/ void DiskArchive::DebugMsgHandler(const char* file, int line,
|
|
const char* msg)
|
|
{
|
|
ASSERT(file != NULL);
|
|
ASSERT(msg != NULL);
|
|
|
|
LOG_BASE(DebugLog::LOG_INFO, file, line, "<diskimg> %hs", msg);
|
|
}
|
|
|
|
/*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) {
|
|
LOGI("IDCANCEL returned from Main progress updater");
|
|
return false;
|
|
}
|
|
|
|
return true; // tell DiskImgLib to continue what it's doing
|
|
}
|
|
|
|
/*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) {
|
|
LOGI("cancelled");
|
|
}
|
|
|
|
return cont;
|
|
}
|
|
|
|
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 == NULL);
|
|
ASSERT(filename != NULL);
|
|
//ASSERT(ext != NULL);
|
|
|
|
ASSERT(pPreferences != NULL);
|
|
|
|
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;
|
|
|
|
CStringA fileNameA(filename);
|
|
dierr = fDiskImg.OpenImage(fileNameA, 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
|
|
LOGI(" Retrying open with read-only set");
|
|
fIsReadOnly = readOnly = true;
|
|
dierr = fDiskImg.OpenImage(fileNameA, 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) {
|
|
LOGI("User bailed on IMF dialog");
|
|
result = kResultCancel;
|
|
goto bail;
|
|
}
|
|
|
|
if (imf.fSectorOrder != fDiskImg.GetSectorOrder() ||
|
|
imf.fFSFormat != fDiskImg.GetFSFormat())
|
|
{
|
|
LOGI("Initial values overridden, forcing img format");
|
|
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 == NULL) {
|
|
/* 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(NULL, NULL);
|
|
pMain->SetProgressCounterDialog(NULL);
|
|
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(NULL);
|
|
while (pSubVol != NULL) {
|
|
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 = NULL;
|
|
} else {
|
|
assert(result != kResultFailure);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
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 != NULL);
|
|
ASSERT(pOptions != NULL);
|
|
|
|
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;
|
|
}
|
|
|
|
LOGI("DiskArchive: new '%ls' %ld %hs in '%ls'",
|
|
(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, NULL,
|
|
DiskImg::kOuterFormatNone,
|
|
DiskImg::kFileFormatUnadorned,
|
|
DiskImg::kPhysicalFormatSectors,
|
|
NULL,
|
|
pOptions->base.sectorOrder,
|
|
DiskImg::kFormatGenericProDOSOrd, // arg must be generic
|
|
numBlocks,
|
|
canSkipFormat);
|
|
} else {
|
|
ASSERT(numTracks > 0);
|
|
dierr = fDiskImg.CreateImage(fileNameA, NULL,
|
|
DiskImg::kOuterFormatNone,
|
|
DiskImg::kFileFormatUnadorned,
|
|
DiskImg::kPhysicalFormatSectors,
|
|
NULL,
|
|
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 == NULL) {
|
|
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;
|
|
}
|
|
|
|
CString DiskArchive::Close(void)
|
|
{
|
|
if (fpPrimaryDiskFS != NULL) {
|
|
LOGI("DiskArchive shutdown closing disk image");
|
|
delete fpPrimaryDiskFS;
|
|
fpPrimaryDiskFS = NULL;
|
|
}
|
|
|
|
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);
|
|
LOGE("During close: %ls", (LPCWSTR) msg);
|
|
|
|
pMainWin->MessageBox(msg, failed, MB_OK);
|
|
}
|
|
|
|
return L"";
|
|
}
|
|
|
|
CString DiskArchive::Flush(void)
|
|
{
|
|
DIError dierr;
|
|
CWaitCursor waitc;
|
|
|
|
assert(fpPrimaryDiskFS != NULL);
|
|
|
|
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"";
|
|
}
|
|
|
|
bool DiskArchive::IsModified(void) const
|
|
{
|
|
assert(fpPrimaryDiskFS != NULL);
|
|
|
|
return fpPrimaryDiskFS->GetDiskImg()->GetDirtyFlag();
|
|
}
|
|
|
|
void DiskArchive::GetDescription(CString* pStr) const
|
|
{
|
|
if (fpPrimaryDiskFS == NULL)
|
|
return;
|
|
|
|
if (fpPrimaryDiskFS->GetVolumeID() != NULL) {
|
|
pStr->Format(L"Disk Image - %hs", fpPrimaryDiskFS->GetVolumeID());
|
|
}
|
|
}
|
|
|
|
int DiskArchive::LoadContents(void)
|
|
{
|
|
int result;
|
|
|
|
LOGI("DiskArchive LoadContents");
|
|
ASSERT(fpPrimaryDiskFS != NULL);
|
|
|
|
{
|
|
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;
|
|
}
|
|
|
|
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 "";
|
|
}
|
|
|
|
int DiskArchive::InternalReload(CWnd* pMsgWnd)
|
|
{
|
|
CString errMsg;
|
|
|
|
errMsg = Reload();
|
|
|
|
if (!errMsg.IsEmpty()) {
|
|
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
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);
|
|
|
|
LOGI("Notes for disk image '%ls':", volName);
|
|
LOGI("%hs", pDiskFS->GetDiskImg()->GetNotes());
|
|
|
|
ASSERT(pDiskFS != NULL);
|
|
pFile = pDiskFS->GetNextFile(NULL);
|
|
while (pFile != NULL) {
|
|
pNewEntry = new DiskEntry(pFile);
|
|
if (pNewEntry == NULL)
|
|
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(NULL);
|
|
while (pSubVol != NULL) {
|
|
CString concatSubVolName;
|
|
const char* subVolName;
|
|
int ret;
|
|
|
|
subVolName = pSubVol->GetDiskFS()->GetVolumeName();
|
|
if (subVolName == NULL)
|
|
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;
|
|
}
|
|
|
|
void DiskArchive::PreferencesChanged(void)
|
|
{
|
|
const Preferences* pPreferences = GET_PREFERENCES();
|
|
|
|
if (fpPrimaryDiskFS != NULL) {
|
|
fpPrimaryDiskFS->SetParameter(DiskFS::kParmProDOS_AllowLowerCase,
|
|
pPreferences->GetPrefBool(kPrProDOSAllowLower) != 0);
|
|
fpPrimaryDiskFS->SetParameter(DiskFS::kParmProDOS_AllocSparse,
|
|
pPreferences->GetPrefBool(kPrProDOSUseSparse) != 0);
|
|
}
|
|
}
|
|
|
|
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
|
|
* ===========================================================================
|
|
*/
|
|
|
|
bool DiskArchive::BulkAdd(ActionProgressDialog* pActionProgress,
|
|
const AddFilesDialog* pAddOpts)
|
|
{
|
|
NuError nerr;
|
|
CString errMsg;
|
|
WCHAR curDir[MAX_PATH] = L"";
|
|
bool retVal = false;
|
|
|
|
LOGD("Opts: '%ls' typePres=%d", (LPCWSTR) pAddOpts->fStoragePrefix,
|
|
pAddOpts->fTypePreservation);
|
|
LOGD(" sub=%d strip=%d ovwr=%d",
|
|
pAddOpts->fIncludeSubfolders, pAddOpts->fStripFolderNames,
|
|
pAddOpts->fOverwriteExisting);
|
|
|
|
ASSERT(fpAddDataHead == NULL);
|
|
|
|
/* 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 CString& directory = pAddOpts->GetDirectory();
|
|
LOGI("Selected path = '%ls'", (LPCWSTR) directory);
|
|
|
|
if (GetCurrentDirectory(NELEM(curDir), curDir) == 0) {
|
|
errMsg = L"Unable to get current directory.\n";
|
|
ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED);
|
|
goto bail;
|
|
}
|
|
if (SetCurrentDirectory(directory) == false) {
|
|
errMsg.Format(L"Unable to set current directory to '%ls'.\n",
|
|
(LPCWSTR) directory);
|
|
ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED);
|
|
goto bail;
|
|
}
|
|
|
|
const CStringArray& fileNames = pAddOpts->GetFileNames();
|
|
for (int i = 0; i < fileNames.GetCount(); i++) {
|
|
const CString& name = fileNames.GetAt(i);
|
|
LOGI(" file '%ls'", (LPCWSTR) name);
|
|
|
|
/* add the file, calling DoAddFile via the generic AddFile */
|
|
nerr = AddFile(pAddOpts, name, &errMsg);
|
|
if (nerr != kNuErrNone) {
|
|
if (errMsg.IsEmpty())
|
|
errMsg.Format(L"Failed while adding file '%ls': %hs.",
|
|
(LPCWSTR) name, NuStrError(nerr));
|
|
if (nerr != kNuErrAborted) {
|
|
ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED);
|
|
}
|
|
goto bail;
|
|
}
|
|
}
|
|
|
|
if (fpAddDataHead == NULL) {
|
|
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", curDir);
|
|
ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED);
|
|
// bummer, but don't signal failure
|
|
}
|
|
return retVal;
|
|
}
|
|
|
|
NuError DiskArchive::DoAddFile(const AddFilesDialog* pAddOpts,
|
|
FileDetails* pDetails)
|
|
{
|
|
/*
|
|
* Add a file to a disk image Unfortunately we can't just add the files
|
|
* here. We need to figure 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 FileDetails struct need to be
|
|
* copied out. It's a "hairy" struct, so we need to duplicate the strings.
|
|
*/
|
|
NuError nuerr = kNuErrNone;
|
|
DiskFS* pDiskFS = pAddOpts->fpTargetDiskFS;
|
|
|
|
DIError dierr;
|
|
int neededLen = 64; // reasonable guess
|
|
char* fsNormalBuf = NULL; // name as it will appear on disk image
|
|
|
|
LOGI(" +++ ADD file: orig='%ls' stor='%ls'",
|
|
(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 != NULL) {
|
|
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 */
|
|
LOGI(" Deleting existing file '%hs'", 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.
|
|
LOGI(" Deletion failed (err=%d)", dierr);
|
|
goto bail;
|
|
}
|
|
} else {
|
|
LOGI("GLITCH: bad return %d from HandleReplaceExisting",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 == NULL) {
|
|
nuerr = kNuErrMalloc;
|
|
goto bail;
|
|
}
|
|
|
|
LOGI("FSNormalized is '%hs'", pAddData->GetFSNormalPath());
|
|
|
|
AddToAddDataList(pAddData);
|
|
|
|
bail:
|
|
delete[] fsNormalBuf;
|
|
return nuerr;
|
|
}
|
|
|
|
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) {
|
|
LOGI("User cancelled out of add-to-diskimg replace-existing");
|
|
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;
|
|
LOGI("Trying rename to '%ls'", (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;
|
|
}
|
|
|
|
CString DiskArchive::ProcessFileAddData(DiskFS* pDiskFS, int addOptsConvEOL)
|
|
{
|
|
CString errMsg;
|
|
FileAddData* pData;
|
|
unsigned char* dataBuf = NULL;
|
|
unsigned char* rsrcBuf = NULL;
|
|
long dataLen, rsrcLen;
|
|
MainWindow* pMainWin = (MainWindow*)::AfxGetMainWnd();
|
|
|
|
LOGI("--- ProcessFileAddData");
|
|
|
|
/* 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 != NULL) {
|
|
const FileDetails* pDataDetails = NULL;
|
|
const FileDetails* pRsrcDetails = NULL;
|
|
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() != NULL) {
|
|
pDetails = pData->GetOtherFork()->GetDetails();
|
|
typeStr = "both";
|
|
|
|
switch (pDetails->entryKind) {
|
|
case FileDetails::kFileKindDataFork:
|
|
assert(pDataDetails == NULL);
|
|
pDataDetails = pDetails;
|
|
break;
|
|
case FileDetails::kFileKindRsrcFork:
|
|
assert(pRsrcDetails == NULL);
|
|
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";
|
|
}
|
|
}
|
|
|
|
LOGI("Adding file '%ls' (%hs)",
|
|
(LPCWSTR) pDetails->storageName, typeStr);
|
|
ASSERT(pDataDetails != NULL || pRsrcDetails != NULL);
|
|
|
|
/*
|
|
* 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 != NULL)
|
|
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 != NULL) {
|
|
/* 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)
|
|
{
|
|
LOGI("Enabling text conversion by type");
|
|
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 != NULL) {
|
|
/* 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 = NULL;
|
|
|
|
pData = pData->GetNext();
|
|
}
|
|
|
|
bail:
|
|
delete[] dataBuf;
|
|
delete[] rsrcBuf;
|
|
return errMsg;
|
|
}
|
|
|
|
|
|
// TODO: really ought to update the progress counter, especially when reading
|
|
// really large files.
|
|
CString DiskArchive::LoadFile(const WCHAR* pathName, uint8_t** pBuf, long* pLen,
|
|
GenericEntry::ConvertEOL conv, GenericEntry::ConvertHighASCII convHA) const
|
|
{
|
|
const char kCharLF = '\n';
|
|
const char kCharCR = '\r';
|
|
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 != NULL);
|
|
ASSERT(pBuf != NULL);
|
|
ASSERT(pLen != NULL);
|
|
|
|
fp = _wfopen(pathName, L"rb");
|
|
if (fp == NULL) {
|
|
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 = NULL;
|
|
*pLen = 0;
|
|
goto bail;
|
|
} else if (fileLen > 0x00ffffff) {
|
|
errMsg = L"Cannot add files larger than 16MB to a disk image.";
|
|
goto bail;
|
|
}
|
|
|
|
*pBuf = new uint8_t[fileLen];
|
|
if (*pBuf == NULL) {
|
|
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 = NULL;
|
|
goto bail;
|
|
}
|
|
rewind(fp);
|
|
|
|
conv = GenericEntry::DetermineConversion(*pBuf, chunkLen,
|
|
&eolType, &dummy);
|
|
LOGI("LoadFile DetermineConv returned conv=%d eolType=%d",
|
|
conv, eolType);
|
|
if (conv == GenericEntry::kConvertEOLOn &&
|
|
eolType == GenericEntry::kEOLCR)
|
|
{
|
|
LOGI(" (skipping conversion due to matching eolType)");
|
|
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 */
|
|
LOGI(" +++ NOT converting text '%ls'", pathName);
|
|
if (fread(*pBuf, fileLen, 1, fp) != 1) {
|
|
errMsg.Format(L"Unable to read '%ls': %hs.", pathName, strerror(errno));
|
|
delete[] *pBuf;
|
|
*pBuf = NULL;
|
|
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;
|
|
|
|
LOGI(" +++ Converting text '%ls', mask=0x%02x", 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;
|
|
}
|
|
|
|
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 = NULL;
|
|
A2FileDescr* pOpenFile = NULL;
|
|
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 != NULL) {
|
|
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 {
|
|
LOGI(" Ignoring subdir create req on non-hierarchic FS");
|
|
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 */
|
|
LOGI("--- nothing left to write for '%hs'",
|
|
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)
|
|
{
|
|
LOGI("+++ switching DOS file type to $f2");
|
|
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) {
|
|
LOGI(" CreateFile failed: %hs", DiskImgLib::DIStrError(dierr));
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* Note: if this was an empty directory holder, pNewFile will be set
|
|
* to NULL. 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 NULL, 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 != NULL))
|
|
{
|
|
ASSERT(pNewFile != NULL);
|
|
unsigned char dummyBuf[1] = { '\0' };
|
|
|
|
dierr = pNewFile->Open(&pOpenFile, false, false);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
pOpenFile->SetProgressUpdater(DiskArchive::ProgressCallback,
|
|
dataLen, NULL);
|
|
|
|
dierr = pOpenFile->Write(dataBuf != NULL ? dataBuf : dummyBuf, dataLen);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
dierr = pOpenFile->Close();
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
pOpenFile = NULL;
|
|
}
|
|
|
|
if (rsrcLen > 0) {
|
|
ASSERT(pNewFile != NULL);
|
|
|
|
dierr = pNewFile->Open(&pOpenFile, false, true);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
pOpenFile->SetProgressUpdater(DiskArchive::ProgressCallback,
|
|
rsrcLen, NULL);
|
|
|
|
dierr = pOpenFile->Write(rsrcBuf, rsrcLen);
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
|
|
dierr = pOpenFile->Close();
|
|
if (dierr != kDIErrNone)
|
|
goto bail;
|
|
pOpenFile = NULL;
|
|
}
|
|
|
|
bail:
|
|
if (pOpenFile != NULL)
|
|
pOpenFile->Close();
|
|
if (dierr != kDIErrNone && pNewFile != NULL) {
|
|
/*
|
|
* 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.
|
|
*/
|
|
LOGI(" Deleting newly-created file '%hs'", parmCopy.pathName);
|
|
(void) pDiskFS->DeleteFile(pNewFile);
|
|
}
|
|
return dierr;
|
|
}
|
|
|
|
// TODO: make this a member of FileDetails, and return a struct owned by
|
|
// FileDetails. This is necessary because we put const strings into
|
|
// pCreateParms that are owned by FileDetails, and need to coordinate the
|
|
// object lifetime.
|
|
void DiskArchive::ConvertFDToCP(const FileDetails* pDetails,
|
|
DiskFS::CreateParms* pCreateParms)
|
|
{
|
|
// ugly hack to get storage for narrow string
|
|
pDetails->fStorageNameA = pDetails->storageName;
|
|
pCreateParms->pathName = pDetails->fStorageNameA;
|
|
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);
|
|
}
|
|
|
|
void DiskArchive::AddToAddDataList(FileAddData* pData)
|
|
{
|
|
ASSERT(pData != NULL);
|
|
ASSERT(pData->GetNext() == NULL);
|
|
|
|
/*
|
|
* 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)
|
|
// LOGI("whee");
|
|
FileAddData* pSearch = fpAddDataHead;
|
|
FileDetails::FileKind dataKind, listKind;
|
|
|
|
dataKind = pData->GetDetails()->entryKind;
|
|
while (pSearch != NULL) {
|
|
if (pSearch->GetOtherFork() == NULL &&
|
|
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 */
|
|
LOGD("--- connecting forks of '%ls' and '%ls'",
|
|
(LPCWSTR) pData->GetDetails()->origName,
|
|
(LPCWSTR) pSearch->GetDetails()->origName);
|
|
pSearch->SetOtherFork(pData);
|
|
return;
|
|
}
|
|
}
|
|
|
|
pSearch = pSearch->GetNext();
|
|
}
|
|
|
|
if (fpAddDataHead == NULL) {
|
|
assert(fpAddDataTail == NULL);
|
|
fpAddDataHead = fpAddDataTail = pData;
|
|
} else {
|
|
fpAddDataTail->SetNext(pData);
|
|
fpAddDataTail = pData;
|
|
}
|
|
}
|
|
|
|
void DiskArchive::FreeAddDataList(void)
|
|
{
|
|
FileAddData* pData;
|
|
FileAddData* pNext;
|
|
|
|
pData = fpAddDataHead;
|
|
while (pData != NULL) {
|
|
pNext = pData->GetNext();
|
|
delete pData->GetOtherFork();
|
|
delete pData;
|
|
pData = pNext;
|
|
}
|
|
|
|
fpAddDataHead = fpAddDataTail = NULL;
|
|
}
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* DiskArchive -- create subdir
|
|
* ===========================================================================
|
|
*/
|
|
|
|
bool DiskArchive::CreateSubdir(CWnd* pMsgWnd, GenericEntry* pParentEntry,
|
|
const WCHAR* newName)
|
|
{
|
|
ASSERT(newName != NULL && wcslen(newName) > 0);
|
|
DiskEntry* pEntry = (DiskEntry*) pParentEntry;
|
|
ASSERT(pEntry != NULL);
|
|
A2File* pFile = pEntry->GetA2File();
|
|
ASSERT(pFile != NULL);
|
|
DiskFS* pDiskFS = pFile->GetDiskFS();
|
|
ASSERT(pDiskFS != NULL);
|
|
|
|
if (!pFile->IsDirectory()) {
|
|
ASSERT(false);
|
|
return false;
|
|
}
|
|
|
|
DIError dierr;
|
|
A2File* pNewFile = NULL;
|
|
DiskFS::CreateParms parms;
|
|
CStringA pathName;
|
|
time_t now = time(NULL);
|
|
|
|
/*
|
|
* Create the full path.
|
|
*/
|
|
if (pFile->IsVolumeDirectory()) {
|
|
pathName = newName;
|
|
} else {
|
|
pathName = pParentEntry->GetPathName();
|
|
pathName += pParentEntry->GetFssep();
|
|
pathName += newName;
|
|
}
|
|
ASSERT(wcschr(newName, pParentEntry->GetFssep()) == NULL);
|
|
|
|
/* 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
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*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());
|
|
}
|
|
|
|
bool DiskArchive::DeleteSelection(CWnd* pMsgWnd, SelectionSet* pSelSet)
|
|
{
|
|
/*
|
|
* 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.
|
|
*/
|
|
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 != NULL) {
|
|
pEntry = (DiskEntry*) pSelEntry->GetEntry();
|
|
ASSERT(pEntry != NULL);
|
|
|
|
entryArray[idx++] = pEntry;
|
|
LOGI("Added 0x%08lx '%ls'", (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;
|
|
}
|
|
|
|
LOGI(" Deleting '%ls' from '%hs'", (LPCWSTR) pEntry->GetPathName(),
|
|
(LPCSTR) pFile->GetDiskFS()->GetVolumeName());
|
|
SET_PROGRESS_UPDATE2(0, pEntry->GetPathName(), NULL);
|
|
|
|
/*
|
|
* 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(NULL);
|
|
}
|
|
|
|
retVal = true;
|
|
|
|
bail:
|
|
SET_PROGRESS_END();
|
|
delete[] entryArray;
|
|
if (InternalReload(pMsgWnd) != 0)
|
|
retVal = false;
|
|
|
|
return retVal;
|
|
}
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* DiskArchive -- rename files
|
|
* ===========================================================================
|
|
*/
|
|
|
|
bool DiskArchive::RenameSelection(CWnd* pMsgWnd, SelectionSet* pSelSet)
|
|
{
|
|
/*
|
|
* 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.
|
|
*/
|
|
CString errMsg;
|
|
bool retVal = false;
|
|
|
|
LOGI("Renaming %d entries", 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 != NULL) {
|
|
RenameEntryDialog renameDlg(pMsgWnd);
|
|
DiskEntry* pEntry = (DiskEntry*) pSelEntry->GetEntry();
|
|
|
|
LOGI(" Renaming '%ls'", 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(), (LPCWSTR) renameDlg.fNewName,
|
|
DiskImgLib::DIStrError(dierr));
|
|
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
|
|
goto bail;
|
|
}
|
|
LOGI("Rename of '%ls' to '%ls' succeeded",
|
|
pEntry->GetDisplayName(), (LPCWSTR) renameDlg.fNewName);
|
|
} else if (result == IDCANCEL) {
|
|
LOGI("Canceling out of remaining renames");
|
|
break;
|
|
} else {
|
|
/* 3rd possibility is IDIGNORE, i.e. skip this entry */
|
|
LOGI("Skipping rename of '%ls'", pEntry->GetDisplayName());
|
|
}
|
|
|
|
pSelEntry = pSelSet->IterNext();
|
|
}
|
|
|
|
/* reload GenericArchive from disk image */
|
|
if (InternalReload(pMsgWnd) == kNuErrNone)
|
|
retVal = true;
|
|
|
|
bail:
|
|
return retVal;
|
|
}
|
|
|
|
bool DiskArchive::SetRenameFields(CWnd* pMsgWnd, DiskEntry* pEntry,
|
|
RenameEntryDialog* pDialog)
|
|
{
|
|
DiskFS* pDiskFS;
|
|
|
|
ASSERT(pEntry != NULL);
|
|
|
|
/*
|
|
* 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;
|
|
}
|
|
|
|
CString DiskArchive::TestPathName(const GenericEntry* pGenericEntry,
|
|
const CString& basePath, const CString& newName, char newFssep) const
|
|
{
|
|
const DiskEntry* pEntry = (DiskEntry*) pGenericEntry;
|
|
DiskImg::FSFormat format;
|
|
CString errMsg, pathName;
|
|
CStringA 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 != NULL && existingFile != pEntry->GetA2File()) {
|
|
errMsg = L"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
|
|
* ===========================================================================
|
|
*/
|
|
|
|
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;
|
|
}
|
|
|
|
CString DiskArchive::TestVolumeName(const DiskFS* pDiskFS,
|
|
const WCHAR* newName) const
|
|
{
|
|
DiskImg::FSFormat format;
|
|
CString errMsg;
|
|
|
|
ASSERT(pDiskFS != NULL);
|
|
ASSERT(newName != NULL);
|
|
|
|
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
|
|
* ===========================================================================
|
|
*/
|
|
|
|
bool DiskArchive::SetProps(CWnd* pMsgWnd, GenericEntry* pGenericEntry,
|
|
const FileProps* pProps)
|
|
{
|
|
/*
|
|
* Technically we should reload the GenericArchive from the disk image,
|
|
* but the set of changes is pretty small, so we just make them here.
|
|
*/
|
|
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) {
|
|
LOGI(" (reloading additional fields after DOS SFI)");
|
|
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
|
|
* ===========================================================================
|
|
*/
|
|
|
|
GenericArchive::XferStatus DiskArchive::XferSelection(CWnd* pMsgWnd,
|
|
SelectionSet* pSelSet, ActionProgressDialog* pActionProgress,
|
|
const XferFileOptions* pXferOpts)
|
|
{
|
|
/*
|
|
* 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.
|
|
*/
|
|
LOGI("DiskArchive XferSelection!");
|
|
unsigned char* dataBuf = NULL;
|
|
unsigned char* rsrcBuf = NULL;
|
|
FileDetails fileDetails;
|
|
CString errMsg, extractErrMsg, cmpStr;
|
|
CString fixedPathName;
|
|
XferStatus retval = kXferFailed;
|
|
|
|
pXferOpts->fTarget->XferPrepare(pXferOpts);
|
|
|
|
SelectionEntry* pSelEntry = pSelSet->IterNext();
|
|
for ( ; pSelEntry != NULL; pSelEntry = pSelSet->IterNext()) {
|
|
long dataLen=-1, rsrcLen=-1;
|
|
DiskEntry* pEntry = (DiskEntry*) pSelEntry->GetEntry();
|
|
int typeOverride = -1;
|
|
int result;
|
|
|
|
ASSERT(dataBuf == NULL);
|
|
ASSERT(rsrcBuf == NULL);
|
|
|
|
if (pEntry->GetDamaged()) {
|
|
LOGI(" XFER skipping damaged entry '%ls'",
|
|
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() != NULL) {
|
|
CString tmpStr;
|
|
tmpStr = pEntry->GetSubVolName();
|
|
tmpStr += (char)PathProposal::kDefaultStoredFssep;
|
|
tmpStr += fixedPathName;
|
|
fixedPathName = tmpStr;
|
|
}
|
|
|
|
if (pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir) {
|
|
/* this is the volume dir */
|
|
LOGI(" XFER not transferring volume dir '%ls'",
|
|
(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) {
|
|
LOGI("FOUND empty dir '%ls'", (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 {
|
|
LOGI("NOT empty dir '%ls'", (LPCWSTR) fixedPathName);
|
|
}
|
|
}
|
|
|
|
LOGI(" XFER not transferring directory '%ls'",
|
|
(LPCWSTR) fixedPathName);
|
|
continue;
|
|
}
|
|
|
|
LOGI(" Xfer '%ls' (data=%d rsrc=%d)",
|
|
(LPCWSTR) fixedPathName, pEntry->GetHasDataFork(),
|
|
pEntry->GetHasRsrcFork());
|
|
|
|
dataBuf = NULL;
|
|
dataLen = 0;
|
|
result = pEntry->ExtractThreadToBuffer(GenericEntry::kDataThread,
|
|
(char**) &dataBuf, &dataLen, &extractErrMsg);
|
|
if (result == IDCANCEL) {
|
|
LOGI("Cancelled during data extract!");
|
|
goto bail; /* abort anything that was pending */
|
|
} else if (result != IDOK) {
|
|
errMsg.Format(L"Failed while extracting '%ls': %ls.",
|
|
(LPCWSTR) fixedPathName, (LPCWSTR) extractErrMsg);
|
|
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
|
|
goto bail;
|
|
}
|
|
ASSERT(dataBuf != NULL);
|
|
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;
|
|
|
|
LOGI(" Converting DOS text in '%ls'", 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)
|
|
{
|
|
LOGI("WOULD CONVERT ptx '%ls'", fixedPathName);
|
|
}
|
|
#endif
|
|
|
|
if (pEntry->GetHasRsrcFork()) {
|
|
rsrcBuf = NULL;
|
|
rsrcLen = 0;
|
|
result = pEntry->ExtractThreadToBuffer(GenericEntry::kRsrcThread,
|
|
(char**) &rsrcBuf, &rsrcLen, &extractErrMsg);
|
|
if (result == IDCANCEL) {
|
|
LOGI("Cancelled during rsrc extract!");
|
|
goto bail; /* abort anything that was pending */
|
|
} else if (result != IDOK) {
|
|
errMsg.Format(L"Failed while extracting '%ls': %ls.",
|
|
(LPCWSTR) fixedPathName, (LPCWSTR) extractErrMsg);
|
|
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
|
|
goto bail;
|
|
}
|
|
} else {
|
|
ASSERT(rsrcBuf == NULL);
|
|
}
|
|
|
|
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(NULL);
|
|
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()) {
|
|
LOGI("XferFile failed!");
|
|
errMsg.Format(L"Failed while transferring '%ls': %ls.",
|
|
(LPCWSTR) pEntry->GetDisplayName(), (LPCWSTR) errMsg);
|
|
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
|
|
goto bail;
|
|
}
|
|
ASSERT(dataBuf == NULL);
|
|
ASSERT(rsrcBuf == NULL);
|
|
|
|
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;
|
|
}
|
|
|
|
void DiskArchive::XferPrepare(const XferFileOptions* pXferOpts)
|
|
{
|
|
LOGI("DiskArchive::XferPrepare");
|
|
|
|
//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;
|
|
}
|
|
|
|
CString DiskArchive::XferFile(FileDetails* pDetails, uint8_t** pDataBuf,
|
|
long dataLen, uint8_t** pRsrcBuf, long rsrcLen)
|
|
{
|
|
//const int kFileTypeTXT = 0x04;
|
|
DiskFS::CreateParms createParms;
|
|
DiskFS* pDiskFS;
|
|
CString errMsg;
|
|
DIError dierr = kDIErrNone;
|
|
|
|
LOGI(" XFER: transfer '%ls' (dataLen=%ld rsrcLen=%ld)",
|
|
(LPCWSTR) pDetails->storageName, dataLen, rsrcLen);
|
|
|
|
ASSERT(pDataBuf != NULL);
|
|
ASSERT(pRsrcBuf != NULL);
|
|
|
|
/* fill out CreateParms from FileDetails */
|
|
ConvertFDToCP(pDetails, &createParms);
|
|
|
|
if (fpXferTargetFS == NULL)
|
|
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) {
|
|
LOGD(" Stripping high ASCII from '%ls'",
|
|
(LPCWSTR) pDetails->storageName);
|
|
|
|
while (len--)
|
|
*ucp++ &= 0x7f;
|
|
} else if (!srcIsDOS && dstIsDOS) {
|
|
LOGD(" Adding high ASCII to '%ls'", (LPCWSTR) pDetails->storageName);
|
|
|
|
while (len--) {
|
|
if (*ucp != '\0')
|
|
*ucp |= 0x80;
|
|
ucp++;
|
|
}
|
|
} else if (srcIsDOS && dstIsDOS) {
|
|
LOGD(" --- not altering DOS-to-DOS text '%ls'",
|
|
(LPCWSTR) pDetails->storageName);
|
|
} else {
|
|
LOGD(" --- non-DOS transfer '%ls'", (LPCWSTR) 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 = NULL;
|
|
delete[] *pRsrcBuf;
|
|
*pRsrcBuf = NULL;
|
|
|
|
bail:
|
|
return errMsg;
|
|
}
|
|
|
|
void DiskArchive::XferAbort(CWnd* pMsgWnd)
|
|
{
|
|
// Can't undo previous actions.
|
|
LOGI("DiskArchive::XferAbort");
|
|
InternalReload(pMsgWnd);
|
|
}
|
|
|
|
void DiskArchive::XferFinish(CWnd* pMsgWnd)
|
|
{
|
|
LOGI("DiskArchive::XferFinish");
|
|
InternalReload(pMsgWnd);
|
|
}
|