ciderpress/app/ACUArchive.cpp
Andy McFadden d8223dbcfd Relocate method comments
This moves method comments from the .cpp file to the .h file,
where users of the methods can find them.  This also makes it
possible for the IDE to show the comments when you mouse-hover over
the method name, though Visual Studio is a bit weak in this regard.

Also, added "override" keywords on overridden methods.  Reasonably
current versions of popular compilers seem to support this.

Also, don't have the return type on a separate line in the .cpp file.
The motivation for the practice -- quickly finding a method definition
with "^name" -- is less useful in C++ than C, and modern IDEs provide
more convenient ways to do the same thing.

Also, do some more conversion from unsigned types to uintXX_t.

This commit is primarily for the "app" directory.
2014-11-21 22:33:39 -08:00

791 lines
22 KiB
C++

/*
* CiderPress
* Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved.
* See the file LICENSE for distribution terms.
*/
/*
* AppleLink Compression Utility file support.
*
* This was adapted from the Binary II support, which has a mixed history,
* so this is a little scrambled in spots.
*/
#include "stdafx.h"
#include "ACUArchive.h"
#include "NufxArchive.h" // uses NuError
#include "Preferences.h"
#include "Main.h"
#include "Squeeze.h"
#include <errno.h>
/*
+00 2b Number of items in archive
+02 2b 0100
+04 5b "fZink"
+09 11b 0136 0000 0000 0000 0000 dd
+14
+00 1b ?? 00
+01 1b Compression type, 00=none, 03=sq
+02 2b ?? 0000 0000 0000 0000
+04 2b ?? 0a74 961f 7d85 af2c 2775 <- 0000 for dir (checksum?)
+06 2b ?? 0000 0000 0000 0000
+08 2b ?? 0000 0000 0000 0000
+0a 2b Storage size (in 512-byte blocks)
+0c 6b ?? 000000 000000 000000 000000
+12 4b Length of file in this archive (compressed or uncompressed)
+16 2b ProDOS file permissions
+18 2b ProDOS file type
+1a 4b ProDOS aux type
+1e ?? 0000
+20 1b ProDOS storage type (usually 02, 0d for dirs)
+21 ?? 00
+22 ?? 0000 0000
+26 4b Uncompressed file len
+2a 2b ProDOS date (create?)
+2c 2b ProDOS time
+2e 2b ProDOS date (mod?)
+30 2b ProDOS time
+32 2b Filename len
+34 2b ?? ac4a 2d02 for dir <- header checksum?
+36 FL Filename
+xx data start (dir has no data)
*/
/*
* ===========================================================================
* AcuEntry
* ===========================================================================
*/
int AcuEntry::ExtractThreadToBuffer(int which, char** ppText, long* pLength,
CString* pErrMsg) const
{
NuError nerr;
ExpandBuffer expBuf;
char* dataBuf = NULL;
long len;
bool needAlloc = true;
int result = -1;
ASSERT(fpArchive != NULL);
ASSERT(fpArchive->fFp != NULL);
if (*ppText != NULL)
needAlloc = false;
if (which != kDataThread) {
*pErrMsg = "No such fork";
goto bail;
}
len = (long) GetUncompressedLen();
if (len == 0) {
if (needAlloc) {
*ppText = new char[1];
**ppText = '\0';
}
*pLength = 0;
result = IDOK;
goto bail;
}
SET_PROGRESS_BEGIN();
errno = 0;
if (fseek(fpArchive->fFp, fOffset, SEEK_SET) < 0) {
pErrMsg->Format(L"Unable to seek to offset %ld: %hs",
fOffset, strerror(errno));
goto bail;
}
if (GetSqueezed()) {
nerr = UnSqueeze(fpArchive->fFp, (unsigned long) GetCompressedLen(),
&expBuf, false, 0);
if (nerr != kNuErrNone) {
pErrMsg->Format(L"File read failed: %hs", NuStrError(nerr));
goto bail;
}
char* unsqBuf = NULL;
long unsqLen = 0;
expBuf.SeizeBuffer(&unsqBuf, &unsqLen);
LOGI("Unsqueezed %ld bytes to %d",
(unsigned long) GetCompressedLen(), unsqLen);
if (unsqLen == 0) {
// some bonehead squeezed a zero-length file
delete[] unsqBuf;
ASSERT(*ppText == NULL);
LOGI("Handling zero-length squeezed file!");
if (needAlloc) {
*ppText = new char[1];
**ppText = '\0';
}
*pLength = 0;
} else {
if (needAlloc) {
/* just use the seized buffer */
*ppText = unsqBuf;
*pLength = unsqLen;
} else {
if (*pLength < unsqLen) {
pErrMsg->Format(L"buf size %ld too short (%ld)",
*pLength, unsqLen);
delete[] unsqBuf;
goto bail;
}
memcpy(*ppText, unsqBuf, unsqLen);
delete[] unsqBuf;
*pLength = unsqLen;
}
}
} else {
if (needAlloc) {
dataBuf = new char[len];
if (dataBuf == NULL) {
pErrMsg->Format(L"allocation of %ld bytes failed", len);
goto bail;
}
} else {
if (*pLength < (long) len) {
pErrMsg->Format(L"buf size %ld too short (%ld)",
*pLength, len);
goto bail;
}
dataBuf = *ppText;
}
if (fread(dataBuf, len, 1, fpArchive->fFp) != 1) {
pErrMsg->Format(L"File read failed: %hs", strerror(errno));
goto bail;
}
if (needAlloc)
*ppText = dataBuf;
*pLength = len;
}
result = IDOK;
bail:
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 AcuEntry::ExtractThreadToFile(int which, FILE* outfp, ConvertEOL conv,
ConvertHighASCII convHA, CString* pErrMsg) const
{
NuError nerr;
long len;
int result = -1;
ASSERT(IDOK != -1 && IDCANCEL != -1);
if (which != kDataThread) {
*pErrMsg = L"No such fork";
goto bail;
}
len = (long) GetUncompressedLen();
if (len == 0) {
LOGI("Empty fork");
result = IDOK;
goto bail;
}
errno = 0;
if (fseek(fpArchive->fFp, fOffset, SEEK_SET) < 0) {
pErrMsg->Format(L"Unable to seek to offset %ld: %hs",
fOffset, strerror(errno));
goto bail;
}
SET_PROGRESS_BEGIN();
/*
* Generally speaking, anything in a BNY file is going to be small. The
* major exception is a BXY file, which could be huge. However, the
* SHK embedded in a BXY is never squeezed.
*
* To make life easy, we either unsqueeze the entire thing into a buffer
* and then write that, or we do a file-to-file copy of the specified
* number of bytes.
*/
if (GetSqueezed()) {
ExpandBuffer expBuf;
bool lastCR = false;
char* buf;
long uncLen;
nerr = UnSqueeze(fpArchive->fFp, (unsigned long) GetCompressedLen(),
&expBuf, false, 0);
if (nerr != kNuErrNone) {
pErrMsg->Format(L"File read failed: %hs", NuStrError(nerr));
goto bail;
}
expBuf.SeizeBuffer(&buf, &uncLen);
LOGI("Unsqueezed %ld bytes to %d", len, uncLen);
// some bonehead squeezed a zero-length file
if (uncLen == 0) {
ASSERT(buf == NULL);
LOGI("Handling zero-length squeezed file!");
result = IDOK;
goto bail;
}
int err = GenericEntry::WriteConvert(outfp, buf, uncLen, &conv,
&convHA, &lastCR);
if (err != 0) {
pErrMsg->Format(L"File write failed: %hs", strerror(err));
delete[] buf;
goto bail;
}
delete[] buf;
} else {
nerr = CopyData(outfp, conv, convHA, pErrMsg);
if (nerr != kNuErrNone) {
if (pErrMsg->IsEmpty()) {
pErrMsg->Format(L"Failed while copying data: %hs\n",
NuStrError(nerr));
}
goto bail;
}
}
result = IDOK;
bail:
SET_PROGRESS_END();
return result;
}
NuError AcuEntry::CopyData(FILE* outfp, ConvertEOL conv, ConvertHighASCII convHA,
CString* pMsg) const
{
NuError nerr = kNuErrNone;
const int kChunkSize = 8192;
char buf[kChunkSize];
bool lastCR = false;
long srcLen, dataRem;
srcLen = (long) GetUncompressedLen();
ASSERT(srcLen > 0); // empty files should've been caught earlier
/*
* Loop until all data copied.
*/
dataRem = srcLen;
while (dataRem) {
int chunkLen;
if (dataRem > kChunkSize)
chunkLen = kChunkSize;
else
chunkLen = dataRem;
/* read a chunk from the source file */
nerr = fpArchive->AcuRead(buf, chunkLen);
if (nerr != kNuErrNone) {
pMsg->Format(L"File read failed: %hs.", NuStrError(nerr));
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));
nerr = kNuErrGeneric;
goto bail;
}
dataRem -= chunkLen;
SET_PROGRESS_UPDATE(ComputePercent(srcLen - dataRem, srcLen));
}
bail:
return nerr;
}
NuError AcuEntry::TestEntry(CWnd* pMsgWnd)
{
NuError nerr = kNuErrNone;
CString errMsg;
long len;
int result = -1;
len = (long) GetUncompressedLen();
if (len == 0)
goto bail;
errno = 0;
if (fseek(fpArchive->fFp, fOffset, SEEK_SET) < 0) {
nerr = kNuErrGeneric;
errMsg.Format(L"Unable to seek to offset %ld: %hs\n",
fOffset, strerror(errno));
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
if (GetSqueezed()) {
nerr = UnSqueeze(fpArchive->fFp, (unsigned long) GetCompressedLen(),
NULL, false, 0);
if (nerr != kNuErrNone) {
errMsg.Format(L"Unsqueeze failed: %hs.", NuStrError(nerr));
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
} else {
errno = 0;
if (fseek(fpArchive->fFp, fOffset + len, SEEK_SET) < 0) {
nerr = kNuErrGeneric;
errMsg.Format(L"Unable to seek to offset %ld (file truncated?): %hs\n",
fOffset, strerror(errno));
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
goto bail;
}
}
if (SET_PROGRESS_UPDATE(100) == IDCANCEL)
nerr = kNuErrAborted;
bail:
return nerr;
}
/*
* ===========================================================================
* AcuArchive
* ===========================================================================
*/
/*static*/ CString AcuArchive::AppInit(void)
{
return "";
}
GenericArchive::OpenResult AcuArchive::Open(const WCHAR* filename,
bool readOnly, CString* pErrMsg)
{
CString errMsg;
fIsReadOnly = true; // ignore "readOnly"
errno = 0;
fFp = _wfopen(filename, L"rb");
if (fFp == NULL) {
errMsg.Format(L"Unable to open %ls: %hs.", filename, strerror(errno));
goto bail;
}
{
CWaitCursor waitc;
int result;
result = LoadContents();
if (result < 0) {
errMsg.Format(L"The file is not an ACU archive.");
goto bail;
} else if (result > 0) {
errMsg.Format(L"Failed while reading data from ACU archive.");
goto bail;
}
}
SetPathName(filename);
bail:
*pErrMsg = errMsg;
if (!errMsg.IsEmpty())
return kResultFailure;
else
return kResultSuccess;
}
CString AcuArchive::New(const WCHAR* /*filename*/, const void* /*options*/)
{
return L"Sorry, AppleLink Compression Utility files can't be created.";
}
long AcuArchive::GetCapability(Capability cap)
{
switch (cap) {
case kCapCanTest:
return true;
break;
case kCapCanRenameFullPath:
return true;
break;
case kCapCanRecompress:
return true;
break;
case kCapCanEditComment:
return false;
break;
case kCapCanAddDisk:
return false;
break;
case kCapCanConvEOLOnAdd:
return false;
break;
case kCapCanCreateSubdir:
return false;
break;
case kCapCanRenameVolume:
return false;
break;
default:
ASSERT(false);
return -1;
break;
}
}
int AcuArchive::LoadContents(void)
{
NuError nerr;
int numEntries;
ASSERT(fFp != NULL);
rewind(fFp);
/*
* Read the master header. In an ACU file this holds the number of
* files and a bunch of stuff that doesn't seem to change.
*/
if (ReadMasterHeader(&numEntries) != 0)
return -1;
while (numEntries) {
AcuFileEntry fileEntry;
nerr = ReadFileHeader(&fileEntry);
if (nerr != kNuErrNone)
return 1;
if (CreateEntry(&fileEntry) != 0)
return 1;
/* if file isn't empty, seek past it */
if (fileEntry.dataStorageLen) {
nerr = AcuSeek(fileEntry.dataStorageLen);
if (nerr != kNuErrNone)
return 1;
}
numEntries--;
}
return 0;
}
CString AcuArchive::Reload(void)
{
fReloadFlag = true; // tell everybody that cached data is invalid
DeleteEntries();
if (LoadContents() != 0) {
return L"Reload failed.";
}
return "";
}
int AcuArchive::ReadMasterHeader(int* pNumEntries)
{
AcuMasterHeader header;
unsigned char buf[kAcuMasterHeaderLen];
NuError nerr;
nerr = AcuRead(buf, kAcuMasterHeaderLen);
if (nerr != kNuErrNone)
return -1;
header.fileCount = buf[0x00] | buf[0x01] << 8;
header.unknown1 = buf[0x02] | buf[0x03] << 8;
memcpy(header.fZink, &buf[0x04], 5);
header.fZink[5] = '\0';
memcpy(header.unknown2, &buf[0x09], 11);
if (header.fileCount == 0 ||
header.unknown1 != 1 ||
strcmp((char*) header.fZink, "fZink") != 0)
{
LOGI("Not an ACU archive");
return -1;
}
LOGI("Looks like an ACU archive with %d entries", header.fileCount);
*pNumEntries = header.fileCount;
return 0;
}
NuError AcuArchive::ReadFileHeader(AcuFileEntry* pEntry)
{
NuError err = kNuErrNone;
unsigned char buf[kAcuEntryHeaderLen];
ASSERT(pEntry != NULL);
err = AcuRead(buf, kAcuEntryHeaderLen);
if (err != kNuErrNone)
goto bail;
// unknown at 00
pEntry->compressionType = buf[0x01];
// unknown at 02-03
pEntry->dataChecksum = buf[0x04] | buf[0x05] << 8; // not sure
// unknown at 06-09
pEntry->blockCount = buf[0x0a] | buf[0x0b] << 8;
// unknown at 0c-11
pEntry->dataStorageLen = buf[0x12] | buf [0x13] << 8 | buf[0x14] << 16 |
buf[0x15] << 24;
pEntry->access = buf[0x16] | buf[0x17] << 8;
pEntry->fileType = buf[0x18] | buf[0x19] << 8;
pEntry->auxType = buf[0x1a] | buf[0x1b] << 8;
// unknown at 1e-1f
pEntry->storageType = buf[0x20];
// unknown at 21-25
pEntry->dataEof = buf[0x26] | buf[0x27] << 8 | buf[0x28] << 16 |
buf[0x29] << 24;
pEntry->prodosModDate = buf[0x2a] | buf[0x2b] << 8;
pEntry->prodosModTime = buf[0x2c] | buf[0x2d] << 8;
AcuConvertDateTime(pEntry->prodosModDate, pEntry->prodosModTime,
&pEntry->modWhen);
pEntry->prodosCreateDate = buf[0x2e] | buf[0x2f] << 8;
pEntry->prodosCreateTime = buf[0x30] | buf[0x31] << 8;
AcuConvertDateTime(pEntry->prodosCreateDate, pEntry->prodosCreateTime,
&pEntry->createWhen);
pEntry->fileNameLen = buf[0x32] | buf[0x33] << 8;
pEntry->headerChecksum = buf[0x34] | buf[0x35] << 8; // not sure
/* read the filename */
if (pEntry->fileNameLen > kAcuMaxFileName) {
LOGI("GLITCH: filename is too long (%d bytes)",
pEntry->fileNameLen);
err = kNuErrGeneric;
goto bail;
}
if (!pEntry->fileNameLen) {
LOGI("GLITCH: filename missing");
err = kNuErrGeneric;
goto bail;
}
/* don't know if this is possible or not */
if (pEntry->storageType == 5) {
LOGI("HEY: EXTENDED FILE");
}
err = AcuRead(pEntry->fileName, pEntry->fileNameLen);
if (err != kNuErrNone)
goto bail;
pEntry->fileName[pEntry->fileNameLen] = '\0';
//DumpFileHeader(pEntry);
bail:
return err;
}
void AcuArchive::DumpFileHeader(const AcuFileEntry* pEntry)
{
time_t createWhen, modWhen;
CString createStr, modStr;
createWhen = NufxArchive::DateTimeToSeconds(&pEntry->createWhen);
modWhen = NufxArchive::DateTimeToSeconds(&pEntry->modWhen);
FormatDate(createWhen, &createStr);
FormatDate(modWhen, &modStr);
LOGI(" Header for file '%hs':", pEntry->fileName);
LOGI(" dataStorageLen=%d eof=%d blockCount=%d checksum=0x%04x",
pEntry->dataStorageLen, pEntry->dataEof, pEntry->blockCount,
pEntry->dataChecksum);
LOGI(" fileType=0x%02x auxType=0x%04x storageType=0x%02x access=0x%04x",
pEntry->fileType, pEntry->auxType, pEntry->storageType, pEntry->access);
LOGI(" created %ls, modified %ls",
(LPCWSTR) createStr, (LPCWSTR) modStr);
LOGI(" fileNameLen=%d headerChecksum=0x%04x",
pEntry->fileNameLen, pEntry->headerChecksum);
}
int AcuArchive::CreateEntry(const AcuFileEntry* pEntry)
{
const int kAcuFssep = '/';
NuError err = kNuErrNone;
AcuEntry* pNewEntry;
/*
* Create the new entry.
*/
pNewEntry = new AcuEntry(this);
CString fileName(pEntry->fileName);
pNewEntry->SetPathName(fileName);
pNewEntry->SetFssep(kAcuFssep);
pNewEntry->SetFileType(pEntry->fileType);
pNewEntry->SetAuxType(pEntry->auxType);
pNewEntry->SetAccess(pEntry->access);
pNewEntry->SetCreateWhen(NufxArchive::DateTimeToSeconds(&pEntry->createWhen));
pNewEntry->SetModWhen(NufxArchive::DateTimeToSeconds(&pEntry->modWhen));
/* always ProDOS? */
pNewEntry->SetSourceFS(DiskImg::kFormatProDOS);
pNewEntry->SetHasDataFork(true);
pNewEntry->SetHasRsrcFork(false); // ?
if (IsDir(pEntry)) {
pNewEntry->SetRecordKind(GenericEntry::kRecordKindDirectory);
} else {
pNewEntry->SetRecordKind(GenericEntry::kRecordKindFile);
}
pNewEntry->SetCompressedLen(pEntry->dataStorageLen);
pNewEntry->SetDataForkLen(pEntry->dataEof);
if (pEntry->compressionType == kAcuCompNone) {
pNewEntry->SetFormatStr(L"Uncompr");
} else if (pEntry->compressionType == kAcuCompSqueeze) {
pNewEntry->SetFormatStr(L"Squeeze");
pNewEntry->SetSqueezed(true);
} else {
pNewEntry->SetFormatStr(L"(unknown)");
pNewEntry->SetSqueezed(false);
}
pNewEntry->SetOffset(ftell(fFp));
AddEntry(pNewEntry);
return err;
}
/*
* ===========================================================================
* ACU functions
* ===========================================================================
*/
bool AcuArchive::IsDir(const AcuFileEntry* pEntry)
{
return (pEntry->storageType == 0x0d);
}
NuError AcuArchive::AcuRead(void* buf, size_t nbyte)
{
size_t result;
ASSERT(buf != NULL);
ASSERT(nbyte > 0);
ASSERT(fFp != NULL);
errno = 0;
result = fread(buf, 1, nbyte, fFp);
if (result != nbyte)
return errno ? (NuError)errno : kNuErrFileRead;
return kNuErrNone;
}
NuError AcuArchive::AcuSeek(long offset)
{
ASSERT(fFp != NULL);
ASSERT(offset > 0);
/*DBUG(("--- seeking forward %ld bytes\n", offset));*/
if (fseek(fFp, offset, SEEK_CUR) < 0)
return kNuErrFileSeek;
return kNuErrNone;
}
void AcuArchive::AcuConvertDateTime(uint16_t prodosDate,
uint16_t prodosTime, NuDateTime* pWhen)
{
pWhen->second = 0;
pWhen->minute = prodosTime & 0x3f;
pWhen->hour = (prodosTime >> 8) & 0x1f;
pWhen->day = (prodosDate & 0x1f) -1;
pWhen->month = ((prodosDate >> 5) & 0x0f) -1;
pWhen->year = (prodosDate >> 9) & 0x7f;
if (pWhen->year < 40)
pWhen->year += 100; /* P8 uses 0-39 for 2000-2039 */
pWhen->extra = 0;
pWhen->weekDay = 0;
}
/*
* ===========================================================================
* AcuArchive -- test files
* ===========================================================================
*/
bool AcuArchive::TestSelection(CWnd* pMsgWnd, SelectionSet* pSelSet)
{
NuError nerr;
AcuEntry* pEntry;
CString errMsg;
bool retVal = false;
ASSERT(fFp != NULL);
LOGI("Testing %d entries", pSelSet->GetNumEntries());
SelectionEntry* pSelEntry = pSelSet->IterNext();
while (pSelEntry != NULL) {
pEntry = (AcuEntry*) pSelEntry->GetEntry();
LOGI(" Testing '%ls' (offset=%ld)", pEntry->GetDisplayName(),
pEntry->GetOffset());
SET_PROGRESS_UPDATE2(0, pEntry->GetDisplayName(), NULL);
nerr = pEntry->TestEntry(pMsgWnd);
if (nerr != kNuErrNone) {
if (nerr == kNuErrAborted) {
CString title;
title.LoadString(IDS_MB_APP_NAME);
errMsg = "Cancelled.";
pMsgWnd->MessageBox(errMsg, title, MB_OK);
} else {
errMsg.Format(L"Failed while testing '%ls': %hs.",
pEntry->GetPathName(), NuStrError(nerr));
ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED);
}
goto bail;
}
pSelEntry = pSelSet->IterNext();
}
/* show success message */
errMsg.Format(L"Tested %d file%ls, no errors found.",
pSelSet->GetNumEntries(),
pSelSet->GetNumEntries() == 1 ? L"" : L"s");
pMsgWnd->MessageBox(errMsg);
retVal = true;
bail:
SET_PROGRESS_END();
return retVal;
}