/* * 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 * =========================================================================== */ /* * Extract data from an entry. * * If "*ppText" is non-nil, the data will be read into the pointed-to buffer * so long as it's shorter than *pLength bytes. The value in "*pLength" * will be set to the actual length used. * * If "*ppText" is nil, the uncompressed data will be placed into a buffer * allocated with "new[]". * * Returns IDOK on success, IDCANCEL if the operation was cancelled by the * user, and -1 value on failure. On failure, "*pErrMsg" holds an error * message. * * "which" is an anonymous GenericArchive enum (e.g. "kDataThread"). */ int AcuEntry::ExtractThreadToBuffer(int which, char** ppText, long* pLength, CString* pErrMsg) const { NuError nerr; ExpandBuffer expBuf; char* dataBuf = nil; long len; bool needAlloc = true; int result = -1; ASSERT(fpArchive != nil); ASSERT(fpArchive->fFp != nil); if (*ppText != nil) 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 = nil; long unsqLen = 0; expBuf.SeizeBuffer(&unsqBuf, &unsqLen); WMSG2("Unsqueezed %ld bytes to %d\n", (unsigned long) GetCompressedLen(), unsqLen); if (unsqLen == 0) { // some bonehead squeezed a zero-length file delete[] unsqBuf; ASSERT(*ppText == nil); WMSG0("Handling zero-length squeezed file!\n"); 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 == nil) { 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 == nil); } } return result; } /* * Extract data from a thread to a file. Since we're not copying to memory, * we can't assume that we're able to hold the entire file all at once. * * Returns IDOK on success, IDCANCEL if the operation was cancelled by the * user, and -1 value on failure. On failure, "*pMsg" holds an * error message. */ int 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) { WMSG0("Empty fork\n"); 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); WMSG2("Unsqueezed %ld bytes to %d\n", len, uncLen); // some bonehead squeezed a zero-length file if (uncLen == 0) { ASSERT(buf == nil); WMSG0("Handling zero-length squeezed file!\n"); 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; } /* * Copy data from the seeked archive to outfp, possibly converting EOL along * the way. */ 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; } /* * Test this entry by extracting it. * * If the file isn't compressed, just make sure the file is big enough. If * it's squeezed, invoke the un-squeeze function with a "nil" buffer pointer. */ 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(), nil, 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 * =========================================================================== */ /* * Perform one-time initialization. There really isn't any for us. * * Returns 0 on success, nonzero on error. */ /*static*/ CString AcuArchive::AppInit(void) { return ""; } /* * Open an ACU archive. * * Returns an error string on failure, or "" on success. */ 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 == nil) { 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; } /* * Finish instantiating an AcuArchive object by creating a new archive. * * Returns an error string on failure, or "" on success. */ CString AcuArchive::New(const WCHAR* /*filename*/, const void* /*options*/) { return L"Sorry, AppleLink Compression Utility files can't be created."; } /* * Our capabilities. */ 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; } } /* * Load the contents of the archive. * * Returns 0 on success, < 0 if this is not an ACU archive > 0 if this appears * to be an ACU archive but it's damaged. */ int AcuArchive::LoadContents(void) { NuError nerr; int numEntries; ASSERT(fFp != nil); 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; } /* * Reload the contents of the archive. */ CString AcuArchive::Reload(void) { fReloadFlag = true; // tell everybody that cached data is invalid DeleteEntries(); if (LoadContents() != 0) { return L"Reload failed."; } return ""; } /* * Read the archive header. The archive file is left seeked to the point * at the end of the header. * * Returns 0 on success, -1 on failure. Sets *pNumEntries to the number of * entries in the archive. */ 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) { WMSG0("Not an ACU archive\n"); return -1; } WMSG1("Looks like an ACU archive with %d entries\n", header.fileCount); *pNumEntries = header.fileCount; return 0; } /* * Read and decode an AppleLink Compression Utility file entry header. * This leaves the file seeked to the point immediately past the filename. */ NuError AcuArchive::ReadFileHeader(AcuFileEntry* pEntry) { NuError err = kNuErrNone; unsigned char buf[kAcuEntryHeaderLen]; ASSERT(pEntry != nil); 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) { WMSG1("GLITCH: filename is too long (%d bytes)\n", pEntry->fileNameLen); err = kNuErrGeneric; goto bail; } if (!pEntry->fileNameLen) { WMSG0("GLITCH: filename missing\n"); err = kNuErrGeneric; goto bail; } /* don't know if this is possible or not */ if (pEntry->storageType == 5) { WMSG0("HEY: EXTENDED FILE\n"); } err = AcuRead(pEntry->fileName, pEntry->fileNameLen); if (err != kNuErrNone) goto bail; pEntry->fileName[pEntry->fileNameLen] = '\0'; //DumpFileHeader(pEntry); bail: return err; } /* * Dump the contents of an AcuFileEntry struct. */ 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); WMSG1(" Header for file '%hs':\n", pEntry->fileName); WMSG4(" dataStorageLen=%d eof=%d blockCount=%d checksum=0x%04x\n", pEntry->dataStorageLen, pEntry->dataEof, pEntry->blockCount, pEntry->dataChecksum); WMSG4(" fileType=0x%02x auxType=0x%04x storageType=0x%02x access=0x%04x\n", pEntry->fileType, pEntry->auxType, pEntry->storageType, pEntry->access); WMSG2(" created %ls, modified %ls\n", (LPCWSTR) createStr, (LPCWSTR) modStr); WMSG2(" fileNameLen=%d headerChecksum=0x%04x\n", pEntry->fileNameLen, pEntry->headerChecksum); } /* * Given an AcuFileEntry structure, add an appropriate entry to the list. */ 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 * =========================================================================== */ /* * Test if this entry is a directory. */ bool AcuArchive::IsDir(const AcuFileEntry* pEntry) { return (pEntry->storageType == 0x0d); } /* * Wrapper for fread(). Note the arguments resemble read(2) rather * than fread(3S). */ NuError AcuArchive::AcuRead(void* buf, size_t nbyte) { size_t result; ASSERT(buf != nil); ASSERT(nbyte > 0); ASSERT(fFp != nil); errno = 0; result = fread(buf, 1, nbyte, fFp); if (result != nbyte) return errno ? (NuError)errno : kNuErrFileRead; return kNuErrNone; } /* * Seek within an archive. Because we need to handle streaming archives, * and don't need to special-case anything, we only allow relative * forward seeks. */ NuError AcuArchive::AcuSeek(long offset) { ASSERT(fFp != nil); ASSERT(offset > 0); /*DBUG(("--- seeking forward %ld bytes\n", offset));*/ if (fseek(fFp, offset, SEEK_CUR) < 0) return kNuErrFileSeek; return kNuErrNone; } /* * Convert from ProDOS compact date format to the expanded DateTime format. */ void AcuArchive::AcuConvertDateTime(unsigned short prodosDate, unsigned short 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 * =========================================================================== */ /* * Test the records represented in the selection set. */ bool AcuArchive::TestSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) { NuError nerr; AcuEntry* pEntry; CString errMsg; bool retVal = false; ASSERT(fFp != nil); WMSG1("Testing %d entries\n", pSelSet->GetNumEntries()); SelectionEntry* pSelEntry = pSelSet->IterNext(); while (pSelEntry != nil) { pEntry = (AcuEntry*) pSelEntry->GetEntry(); WMSG2(" Testing '%hs' (offset=%ld)\n", pEntry->GetDisplayName(), pEntry->GetOffset()); SET_PROGRESS_UPDATE2(0, pEntry->GetDisplayName(), nil); 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 '%hs': %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; }