ciderpress/app/ACUArchive.cpp

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 an ACU file is going to be small.
*
* 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 L"";
}
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)
{
LOGW("Not an ACU archive");
return -1;
}
LOGD("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);
pNewEntry->SetPathNameMOR(pEntry->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)
{
// TODO: this is essentially copy & paste from NufxArchive::TestSelection().
// We can move the implementation to GenericArchive and just have an
// archive-specific TestEntry() function.
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();
LOGD(" Testing '%ls' (offset=%ld)", (LPCWSTR) pEntry->GetDisplayName(),
pEntry->GetOffset());
SET_PROGRESS_UPDATE2(0, pEntry->GetDisplayName(), NULL);
nerr = pEntry->TestEntry(pMsgWnd);
if (nerr != kNuErrNone) {
if (nerr == kNuErrAborted) {
CString title;
CheckedLoadString(&title, IDS_MB_APP_NAME);
errMsg = L"Cancelled.";
pMsgWnd->MessageBox(errMsg, title, MB_OK);
} else {
errMsg.Format(L"Failed while testing '%ls': %hs.",
(LPCWSTR) pEntry->GetPathNameUNI(), 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;
}