/* * 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 /* +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; }