From 5946481b4e38e07de1f38955a08fce7092d2a4f1 Mon Sep 17 00:00:00 2001 From: Andy McFadden Date: Sun, 11 Jan 2015 23:02:35 -0800 Subject: [PATCH] Add AppleSingle support This handles version 1 and 2, and copes with the broken files created by the Mac OS X "applesingle" command-line tool (which is unable to decode the broken files it creates). I get the sense that many AppleSingle files don't end with ".AS", so the filespec includes "*.*" as well. Some AppleSingle files don't include a filename. In that case, we use the file's name as the entry name, minus any ".as" extension. The current implementation doesn't convert from Unicode to Mac OS Roman, so non-ASCII characters are mishandled unless the file was generated by GS/ShrinkIt. (We assume version 1 AppleSingle files use MOR name strings.) Also, version bump to 4.0.0d3. --- DIST/with-mdc.deploy | 8 +- app/ACUArchive.h | 18 +- app/AppleSingleArchive.cpp | 735 +++++++++++++++++++++++++++++++++++++ app/AppleSingleArchive.h | 314 ++++++++++++++++ app/ArchiveInfoDialog.cpp | 20 + app/ArchiveInfoDialog.h | 18 + app/CiderPress.rc | 21 ++ app/GenericArchive.h | 33 ++ app/Main.cpp | 86 ++++- app/Main.h | 11 +- app/MyApp.h | 2 +- app/NufxArchive.cpp | 6 +- app/app.vcxproj | 2 + app/app.vcxproj.filters | 6 + app/resource.h | 3 +- 15 files changed, 1251 insertions(+), 32 deletions(-) create mode 100644 app/AppleSingleArchive.cpp create mode 100644 app/AppleSingleArchive.h diff --git a/DIST/with-mdc.deploy b/DIST/with-mdc.deploy index ff9aeff..4fc8859 100644 --- a/DIST/with-mdc.deploy +++ b/DIST/with-mdc.deploy @@ -4,10 +4,10 @@ faddenSoft http://www.faddensoft.com/ CiderPress http://a2ciderpress.com/ -4.0.0d2 -41989 +4.0.0d3 +42015 C:\DATA\faddenSoft\fs.ico -Copyright © 2014 CiderPress project authors. All rights reserved. +Copyright © 2015 CiderPress project authors. All rights reserved. C:\Src\CiderPress\DIST\ReadMe.txt C:\Src\CiderPress\DIST\License.txt @@ -355,7 +355,7 @@ FALSE 4095 -Setup400d2.exe +Setup400d3.exe FALSE diff --git a/app/ACUArchive.h b/app/ACUArchive.h index 3aca48d..4dec130 100644 --- a/app/ACUArchive.h +++ b/app/ACUArchive.h @@ -81,7 +81,7 @@ public: /* * Perform one-time initialization. There really isn't any for us. * - * Returns 0 on success, nonzero on error. + * Returns an error string on failure. */ static CString AppInit(void); @@ -96,17 +96,17 @@ public: /* * Finish instantiating an AcuArchive object by creating a new archive. * - * Returns an error string on failure, or "" on success. + * This isn't implemented, and will always return an error. */ virtual CString New(const WCHAR* filename, const void* options) override; - virtual CString Flush(void) override { return ""; } + virtual CString Flush(void) override { return L""; } virtual CString Reload(void) override; virtual bool IsReadOnly(void) const override { return true; }; virtual bool IsModified(void) const override { return false; } virtual void GetDescription(CString* pStr) const override - { *pStr = "AppleLink ACU"; } + { *pStr = L"AppleLink ACU"; } virtual bool BulkAdd(ActionProgressDialog* pActionProgress, const AddFilesDialog* pAddOpts) override { ASSERT(false); return false; } @@ -126,10 +126,10 @@ public: { ASSERT(false); return false; } virtual CString TestVolumeName(const DiskFS* pDiskFS, const WCHAR* newName) const override - { ASSERT(false); return "!"; } + { ASSERT(false); return L"!"; } virtual CString TestPathName(const GenericEntry* pGenericEntry, const CString& basePath, const CString& newName, char newFssep) const override - { ASSERT(false); return "!"; } + { ASSERT(false); return L"!"; } virtual bool RecompressSelection(CWnd* pMsgWnd, SelectionSet* pSelSet, const RecompressOptionsDialog* pRecompOpts) override { ASSERT(false); return false; } @@ -165,7 +165,7 @@ private: { ASSERT(false); } virtual CString XferFile(LocalFileDetails* pDetails, uint8_t** pDataBuf, long dataLen, uint8_t** pRsrcBuf, long rsrcLen) override - { ASSERT(false); return "!"; } + { ASSERT(false); return L"!"; } virtual void XferAbort(CWnd* pMsgWnd) override { ASSERT(false); } virtual void XferFinish(CWnd* pMsgWnd) override @@ -237,8 +237,8 @@ private: /* * 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. + * Returns 0 on success, < 0 if this is not an ACU archive, or > 0 if + * this appears to be an ACU archive but it's damaged. */ int LoadContents(void); diff --git a/app/AppleSingleArchive.cpp b/app/AppleSingleArchive.cpp new file mode 100644 index 0000000..830e381 --- /dev/null +++ b/app/AppleSingleArchive.cpp @@ -0,0 +1,735 @@ +/* + * CiderPress + * Copyright (C) 2015 by faddenSoft. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +#include "stdafx.h" +#include "AppleSingleArchive.h" +#include "NufxArchive.h" // using date/time function +#include "Preferences.h" +#include "Main.h" +#include + + +/* + * =========================================================================== + * AppleSingleEntry + * =========================================================================== + */ + +int AppleSingleEntry::ExtractThreadToBuffer(int which, char** ppText, + long* pLength, CString* pErrMsg) const +{ + ExpandBuffer expBuf; + char* dataBuf = NULL; + bool needAlloc = true; + int result = -1; + + ASSERT(fpArchive != NULL); + ASSERT(fpArchive->fFp != NULL); + + if (*ppText != NULL) + needAlloc = false; + + long offset, length; + if (which == kDataThread && fDataOffset >= 0) { + offset = fDataOffset; + length = (long) GetDataForkLen(); + } else if (which == kRsrcThread && fRsrcOffset >= 0) { + offset = fRsrcOffset; + length = (long) GetRsrcForkLen(); + } else { + *pErrMsg = "No such fork"; + goto bail; + } + + SET_PROGRESS_BEGIN(); + + errno = 0; + if (fseek(fpArchive->fFp, offset, SEEK_SET) < 0) { + pErrMsg->Format(L"Unable to seek to offset %ld: %hs", + fDataOffset, strerror(errno)); + goto bail; + } + + if (needAlloc) { + dataBuf = new char[length]; + if (dataBuf == NULL) { + pErrMsg->Format(L"allocation of %ld bytes failed", length); + goto bail; + } + } else { + if (*pLength < length) { + pErrMsg->Format(L"buf size %ld too short (%ld)", + *pLength, length); + goto bail; + } + dataBuf = *ppText; + } + if (length > 0) { + if (fread(dataBuf, length, 1, fpArchive->fFp) != 1) { + pErrMsg->Format(L"File read failed: %hs", strerror(errno)); + goto bail; + } + } + + if (needAlloc) + *ppText = dataBuf; + *pLength = length; + + 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 AppleSingleEntry::ExtractThreadToFile(int which, FILE* outfp, + ConvertEOL conv, ConvertHighASCII convHA, CString* pErrMsg) const +{ + int result = -1; + + ASSERT(IDOK != -1 && IDCANCEL != -1); + long offset, length; + if (which == kDataThread && fDataOffset >= 0) { + offset = fDataOffset; + length = (long) GetDataForkLen(); + } else if (which == kRsrcThread && fRsrcOffset >= 0) { + offset = fRsrcOffset; + length = (long) GetRsrcForkLen(); + } else { + *pErrMsg = "No such fork"; + goto bail; + } + + if (length == 0) { + LOGD("Empty fork"); + result = IDOK; + goto bail; + } + + errno = 0; + if (fseek(fpArchive->fFp, offset, SEEK_SET) < 0) { + pErrMsg->Format(L"Unable to seek to offset %ld: %hs", + fDataOffset, strerror(errno)); + goto bail; + } + + SET_PROGRESS_BEGIN(); + + if (CopyData(length, outfp, conv, convHA, pErrMsg) != 0) { + if (pErrMsg->IsEmpty()) { + *pErrMsg = L"Failed while copying data."; + } + goto bail; + } + + result = IDOK; + +bail: + SET_PROGRESS_END(); + return result; +} + +int AppleSingleEntry::CopyData(long srcLen, FILE* outfp, ConvertEOL conv, + ConvertHighASCII convHA, CString* pMsg) const +{ + int err = 0; + const int kChunkSize = 65536; + char* buf = new char[kChunkSize]; + bool lastCR = false; + long dataRem; + + 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 */ + size_t result = fread(buf, 1, chunkLen, fpArchive->fFp); + if (result != chunkLen) { + pMsg->Format(L"File read failed: %hs.", strerror(errno)); + err = -1; + 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)); + err = -1; + goto bail; + } + + dataRem -= chunkLen; + SET_PROGRESS_UPDATE(ComputePercent(srcLen - dataRem, srcLen)); + } + +bail: + delete[] buf; + return err; +} + + +/* + * =========================================================================== + * AppleSingleArchive + * =========================================================================== + */ + +/*static*/ CString AppleSingleArchive::AppInit(void) +{ + return L""; +} + +GenericArchive::OpenResult AppleSingleArchive::Open(const WCHAR* filename, + bool readOnly, CString* pErrMsg) +{ + CString errMsg; + + errno = 0; + fFp = _wfopen(filename, L"rb"); + if (fFp == NULL) { + errMsg.Format(L"Unable to open %ls: %hs.", filename, strerror(errno)); + goto bail; + } + + // Set this before calling LoadContents() -- we may need to use it as + // the name of the archived file. + SetPathName(filename); + + { + CWaitCursor waitc; + int result; + + result = LoadContents(); + if (result < 0) { + errMsg.Format(L"The file is not an AppleSingle archive."); + goto bail; + } else if (result > 0) { + errMsg.Format(L"Failed while reading data from AppleSingle file."); + goto bail; + } + } + +bail: + *pErrMsg = errMsg; + if (!errMsg.IsEmpty()) + return kResultFailure; + else + return kResultSuccess; +} + +CString AppleSingleArchive::New(const WCHAR* /*filename*/, const void* /*options*/) +{ + return L"Sorry, AppleSingle files can't be created."; +} + +long AppleSingleArchive::GetCapability(Capability cap) +{ + switch (cap) { + case kCapCanTest: return false; break; + case kCapCanRenameFullPath: return false; break; + case kCapCanRecompress: return false; break; + case kCapCanEditComment: return true; 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 AppleSingleArchive::LoadContents(void) +{ + ASSERT(fFp != NULL); + rewind(fFp); + + /* + * Read the file header. + */ + uint8_t headerBuf[kHeaderLen]; + if (fread(headerBuf, 1, kHeaderLen, fFp) != kHeaderLen) { + return -1; // probably not AppleSingle + } + if (headerBuf[1] == 0x05) { + // big-endian (spec-compliant) + fIsBigEndian = true; + fHeader.magic = Get32BE(&headerBuf[0]); + fHeader.version = Get32BE(&headerBuf[4]); + fHeader.numEntries = Get16BE(&headerBuf[8 + kHomeFileSystemLen]); + } else { + // little-endian (Mac OS X generated) + fIsBigEndian = false; + fHeader.magic = Get32LE(&headerBuf[0]); + fHeader.version = Get32LE(&headerBuf[4]); + fHeader.numEntries = Get16LE(&headerBuf[8 + kHomeFileSystemLen]); + } + memcpy(fHeader.homeFileSystem, &headerBuf[8], kHomeFileSystemLen); + fHeader.homeFileSystem[kHomeFileSystemLen] = '\0'; + + if (fHeader.magic != kMagicNumber) { + LOGD("File does not have AppleSingle magic number"); + return -1; + } + if (fHeader.version != kVersion1 && fHeader.version != kVersion2) { + LOGI("AS file has unrecognized version number 0x%08x", fHeader.version); + return -1; + } + + /* + * Read the entries (a table of contents). There are at most 65535 + * entries, so we don't need to worry about capping it at a "reasonable" + * size. + */ + size_t totalEntryLen = fHeader.numEntries * kEntryLen; + uint8_t* entryBuf = new uint8_t[totalEntryLen]; + if (fread(entryBuf, 1, totalEntryLen, fFp) != totalEntryLen) { + LOGW("Unable to read entry list from AS file (err=%d)", errno); + delete[] entryBuf; + return 1; + } + fEntries = new TOCEntry[fHeader.numEntries]; + const uint8_t* ptr = entryBuf; + for (size_t i = 0; i < fHeader.numEntries; i++, ptr += kEntryLen) { + if (fIsBigEndian) { + fEntries[i].entryId = Get32BE(ptr); + fEntries[i].offset = Get32BE(ptr + 4); + fEntries[i].length = Get32BE(ptr + 8); + } else { + fEntries[i].entryId = Get32LE(ptr); + fEntries[i].offset = Get32LE(ptr + 4); + fEntries[i].length = Get32LE(ptr + 8); + } + } + + delete[] entryBuf; + + /* + * Make sure the file actually has everything. + */ + if (!CheckFileLength()) { + return 1; + } + + /* + * Walk through the TOC entries, using them to fill out the fields in an + * AppleSingleEntry class. + */ + if (!CreateEntry()) { + return 1; + } + + DumpArchive(); + + return 0; +} + +bool AppleSingleArchive::CheckFileLength() +{ + // Find the biggest offset+length. + uint64_t maxPosn = 0; + + for (size_t i = 0; i < fHeader.numEntries; i++) { + uint64_t end = (uint64_t) fEntries[i].offset + fEntries[i].length; + if (maxPosn < end) { + maxPosn = end; + } + } + + fseek(fFp, 0, SEEK_END); + long fileLen = ftell(fFp); + if (fileLen < 0) { + LOGW("Unable to determine file length"); + return false; + } + if (maxPosn > (uint64_t) fileLen) { + LOGW("AS max=%llu, file len is only %ld", maxPosn, fileLen); + return false; + } + + return true; +} + +bool AppleSingleArchive::CreateEntry() +{ + AppleSingleEntry* pNewEntry = new AppleSingleEntry(this); + uint32_t dataLen = 0, rsrcLen = 0; + bool haveInfo = false; + bool hasFileName = false; + + for (size_t i = 0; i < fHeader.numEntries; i++) { + const TOCEntry* pToc = &fEntries[i]; + switch (pToc->entryId) { + case kIdDataFork: + if (pNewEntry->GetHasDataFork()) { + LOGW("Found two data forks in AppleSingle"); + return false; + } + dataLen = pToc->length; + pNewEntry->SetHasDataFork(true); + pNewEntry->SetDataOffset(pToc->offset); + pNewEntry->SetDataForkLen(pToc->length); + break; + case kIdResourceFork: + if (pNewEntry->GetHasRsrcFork()) { + LOGW("Found two rsrc forks in AppleSingle"); + return false; + } + rsrcLen = pToc->length; + pNewEntry->SetHasRsrcFork(true); + pNewEntry->SetRsrcOffset(pToc->offset); + pNewEntry->SetRsrcForkLen(pToc->length); + break; + case kIdRealName: + hasFileName = HandleRealName(pToc, pNewEntry); + break; + case kIdComment: + // We could handle this, but I don't think this is widely used. + break; + case kIdFileInfo: + HandleFileInfo(pToc, pNewEntry); + break; + case kIdFileDatesInfo: + HandleFileDatesInfo(pToc, pNewEntry); + break; + case kIdFinderInfo: + if (!haveInfo) { + HandleFinderInfo(pToc, pNewEntry); + } + break; + case kIdProDOSFileInfo: + // this take precedence over Finder info + haveInfo = HandleProDOSFileInfo(pToc, pNewEntry); + break; + case kIdBWIcon: + case kIdColorIcon: + case kIdMacintoshFileInfo: + case kIdMSDOSFileInfo: + case kIdShortName: + case kIdAFPFileInfo: + case kIdDirectoryId: + // We're not interested in these. + break; + default: + LOGD("Ignoring entry with type=%u", pToc->entryId); + break; + } + } + + pNewEntry->SetCompressedLen(dataLen + rsrcLen); + if (rsrcLen > 0) { + pNewEntry->SetRecordKind(GenericEntry::kRecordKindForkedFile); + } else { + pNewEntry->SetRecordKind(GenericEntry::kRecordKindFile); + } + pNewEntry->SetFormatStr(L"Uncompr"); + + // If there wasn't a file name, use the AppleSingle file's name, minus + // any ".as" extension. + if (!hasFileName) { + CString fileName(PathName::FilenameOnly(GetPathName(), '\\')); + if (fileName.GetLength() > 3 && + fileName.Right(3).CompareNoCase(L".as") == 0) { + fileName = fileName.Left(fileName.GetLength() - 3); + } + // TODO: convert UTF-16 Unicode to MOR + CStringA fileNameA(fileName); + pNewEntry->SetPathNameMOR(fileNameA); + } + + AddEntry(pNewEntry); + return true; +} + +bool AppleSingleArchive::HandleRealName(const TOCEntry* tocEntry, + AppleSingleEntry* pEntry) +{ + if (tocEntry->length > 1024) { + // this is a single file name, not a full path + LOGW("Ignoring excessively long filename (%u)", tocEntry->length); + return false; + } + + (void) fseek(fFp, tocEntry->offset, SEEK_SET); + + char* buf = new char[tocEntry->length + 1]; + if (fread(buf, 1, tocEntry->length, fFp) != tocEntry->length) { + LOGW("failed reading file name"); + delete[] buf; + return false; + } + buf[tocEntry->length] = '\0'; + + if (fHeader.version == kVersion1) { + // filename is in Mac OS Roman format already + pEntry->SetPathNameMOR(buf); + } else { + // filename is in UTF-8-encoded Unicode + // TODO: convert UTF-8 to MOR, dropping invalid characters + pEntry->SetPathNameMOR(buf); + } + + delete[] buf; + return true; +} + +bool AppleSingleArchive::HandleFileInfo(const TOCEntry* tocEntry, + AppleSingleEntry* pEntry) +{ + if (strcmp(fHeader.homeFileSystem, "ProDOS ") != 0) { + LOGD("Ignoring file info for filesystem '%s'", fHeader.homeFileSystem); + return false; + } + + const int kEntrySize = 16; + + if (tocEntry->length != kEntrySize) { + LOGW("Bad length on ProDOS File Info (%d)", tocEntry->length); + return false; + } + (void) fseek(fFp, tocEntry->offset, SEEK_SET); + + uint8_t buf[kEntrySize]; + if (fread(buf, 1, kEntrySize, fFp) != kEntrySize) { + LOGW("failed reading ProDOS File Info"); + return false; + } + + uint16_t createDate, createTime, modDate, modTime, access, fileType; + uint32_t auxType; + + if (fIsBigEndian) { + createDate = Get16BE(buf); + createTime = Get16BE(buf + 2); + modDate = Get16BE(buf + 4); + modTime = Get16BE(buf + 6); + access = Get16BE(buf + 8); + fileType = Get16BE(buf + 10); + auxType = Get32BE(buf + 12); + } else { + createDate = Get16LE(buf); + createTime = Get16LE(buf + 2); + modDate = Get16LE(buf + 4); + modTime = Get16LE(buf + 6); + access = Get16LE(buf + 8); + fileType = Get16LE(buf + 10); + auxType = Get32LE(buf + 12); + } + + pEntry->SetAccess(access); + pEntry->SetFileType(fileType); + pEntry->SetAuxType(auxType); + pEntry->SetCreateWhen(ConvertProDOSDateTime(createDate, createTime)); + pEntry->SetModWhen(ConvertProDOSDateTime(modDate, modTime)); + return true; +} + +bool AppleSingleArchive::HandleFileDatesInfo(const TOCEntry* tocEntry, + AppleSingleEntry* pEntry) +{ + const int kEntrySize = 16; + + if (tocEntry->length != kEntrySize) { + LOGW("Bad length on File Dates info (%d)", tocEntry->length); + return false; + } + (void) fseek(fFp, tocEntry->offset, SEEK_SET); + + uint8_t buf[kEntrySize]; + if (fread(buf, 1, kEntrySize, fFp) != kEntrySize) { + LOGW("failed reading File Dates info"); + return false; + } + + int32_t createDate, modDate; + if (fIsBigEndian) { + createDate = Get32BE(buf); + modDate = Get32BE(buf + 4); + // ignore backup date and access date + } else { + createDate = Get32LE(buf); + modDate = Get32LE(buf + 4); + } + + // Number of seconds between Jan 1 1970 and Jan 1 2000, computed with + // Linux mktime(). Does not include leap-seconds. + // + const int32_t kTimeOffset = 946684800; + + // The Mac OS X applesingle tool is creating entries with some pretty + // wild values, so we have to range-check them here or the Windows + // time conversion method gets bent out of shape. + // + // TODO: these are screwy enough that I'm just going to ignore them. + // If it turns out I'm holding it wrong we can re-enable it. + time_t tmpTime = (time_t) createDate + kTimeOffset; + if (tmpTime >= 0 && tmpTime <= 0xffffffffLL) { + //pEntry->SetCreateWhen(tmpTime); + } + tmpTime = (time_t) modDate + kTimeOffset; + if (tmpTime >= 0 && tmpTime <= 0xffffffffLL) { + //pEntry->SetModWhen(tmpTime); + } + + return false; +} + +bool AppleSingleArchive::HandleProDOSFileInfo(const TOCEntry* tocEntry, + AppleSingleEntry* pEntry) +{ + const int kEntrySize = 8; + uint16_t access, fileType; + uint32_t auxType; + + if (tocEntry->length != kEntrySize) { + LOGW("Bad length on ProDOS file info (%d)", tocEntry->length); + return false; + } + (void) fseek(fFp, tocEntry->offset, SEEK_SET); + + uint8_t buf[kEntrySize]; + if (fread(buf, 1, kEntrySize, fFp) != kEntrySize) { + LOGW("failed reading ProDOS info"); + return false; + } + + if (fIsBigEndian) { + access = Get16BE(buf); + fileType = Get16BE(buf + 2); + auxType = Get32BE(buf + 4); + } else { + access = Get16LE(buf); + fileType = Get16LE(buf + 2); + auxType = Get32LE(buf + 4); + } + + pEntry->SetAccess(access); + pEntry->SetFileType(fileType); + pEntry->SetAuxType(auxType); + return true; +} + +bool AppleSingleArchive::HandleFinderInfo(const TOCEntry* tocEntry, + AppleSingleEntry* pEntry) +{ + const int kEntrySize = 32; + const int kPdosType = 0x70646f73; // 'pdos' + uint32_t creator, macType; + + if (tocEntry->length != kEntrySize) { + LOGW("Bad length on Finder info (%d)", tocEntry->length); + return false; + } + (void) fseek(fFp, tocEntry->offset, SEEK_SET); + + uint8_t buf[kEntrySize]; + if (fread(buf, 1, kEntrySize, fFp) != kEntrySize) { + LOGW("failed reading Finder info"); + return false; + } + + // These values are stored big-endian even on Mac OS X. + macType = Get32BE(buf); + creator = Get32BE(buf + 4); + + if (creator == kPdosType && (macType >> 24) == 'p') { + pEntry->SetFileType((macType >> 16) & 0xff); + pEntry->SetAuxType(macType & 0xffff); + } else { + pEntry->SetFileType(macType); + pEntry->SetAuxType(creator); + } + return true; +} + +CString AppleSingleArchive::Reload(void) +{ + fReloadFlag = true; // tell everybody that cached data is invalid + + DeleteEntries(); + if (LoadContents() != 0) { + return L"Reload failed."; + } + + return ""; +} + +CString AppleSingleArchive::GetInfoString() +{ + CString str; + + if (fHeader.version == kVersion1) { + str += "Version 1, "; + } else { + str += "Version 2, "; + } + if (fIsBigEndian) { + str += "big endian"; + } else { + str += "little endian"; + } + + return str; +} + + +/* + * =========================================================================== + * Utility functions + * =========================================================================== + */ + +time_t AppleSingleArchive::ConvertProDOSDateTime(uint16_t prodosDate, + uint16_t prodosTime) +{ + NuDateTime ndt; + + ndt.second = 0; + ndt.minute = prodosTime & 0x3f; + ndt.hour = (prodosTime >> 8) & 0x1f; + ndt.day = (prodosDate & 0x1f) -1; + ndt.month = ((prodosDate >> 5) & 0x0f) -1; + ndt.year = (prodosDate >> 9) & 0x7f; + if (ndt.year < 40) + ndt.year += 100; /* P8 uses 0-39 for 2000-2039 */ + ndt.extra = 0; + ndt.weekDay = 0; + + return NufxArchive::DateTimeToSeconds(&ndt); +} + +void AppleSingleArchive::DumpArchive() +{ + LOGI("AppleSingleArchive: %hs magic=0x%08x, version=%08x, entries=%u", + fIsBigEndian ? "BE" : "LE", fHeader.magic, fHeader.version, + fHeader.numEntries); + LOGI(" homeFileSystem='%hs'", fHeader.homeFileSystem); + for (size_t i = 0; i < fHeader.numEntries; i++) { + LOGI(" %2u: id=%u off=%u len=%u", i, + fEntries[i].entryId, fEntries[i].offset, fEntries[i].length); + } +} \ No newline at end of file diff --git a/app/AppleSingleArchive.h b/app/AppleSingleArchive.h new file mode 100644 index 0000000..172868a --- /dev/null +++ b/app/AppleSingleArchive.h @@ -0,0 +1,314 @@ +/* + * CiderPress + * Copyright (C) 2015 by faddenSoft. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * AppleSingle support. This format provides a way to package a single + * forked file into an ordinary file. + * + * To create a test file from Mac OS X using NuLib2 v3.0 or later: + * - extract a forked file with "nulib2 xe " + * - rename the type-preservation header off of 's data fork + * - combine the forks with "cat #nnnnr > /..namedfork/rsrc" + * - use "xattr -l " to confirm that the file has a resource fork + * and the FinderInfo with the ProDOS file type + * - use "applesingle encode " to create .as + * + * The tool does not create a spec-compliant AppleSingle file. The v2 + * spec is mildly ambiguous, but the Apple II file type note says, + * "...which is stored reverse as $00 $05 $16 $00". It appears that + * someone decided to generate little-endian AppleSingle files, and you + * have to use the magic number to figure out which end is which. + * FWIW, the Linux "file" command only recognizes the big-endian form. + * + * Perhaps unsurprisingly, the "applesingle" tool is not able to decode the + * files it creates -- but it can handle files GS/ShrinkIt creates. + * + * The GS/ShrinkIt "create AppleSingle" function creates a version 1 file + * with Mac OS Roman filenames. The Mac OS X tool creates a version 2 file + * with UTF-8-encoded Unicode filenames. We will treat the name + * accordingly, though it's possible there are v2 files with MOR strings. + */ +#ifndef APP_APPLESINGLEARCHIVE_H +#define APP_APPLESINGLEARCHIVE_H + +#include "GenericArchive.h" + + +class AppleSingleArchive; + +/* + * AppleSingle files only have one entry, so making this a separate class + * is just in keeping with the overall structure. + */ +class AppleSingleEntry : public GenericEntry { +public: + AppleSingleEntry(AppleSingleArchive* pArchive) : + fpArchive(pArchive), fDataOffset(-1), fRsrcOffset(-1) {} + virtual ~AppleSingleEntry(void) {} + + virtual int ExtractThreadToBuffer(int which, char** ppText, long* pLength, + CString* pErrMsg) const override; + virtual int ExtractThreadToFile(int which, FILE* outfp, ConvertEOL conv, + ConvertHighASCII convHA, CString* pErrMsg) const override; + + // doesn't matter + virtual long GetSelectionSerial(void) const override { return -1; } + + virtual bool GetFeatureFlag(Feature feature) const override { + if (feature == kFeatureHasFullAccess || + feature == kFeatureHFSTypes) + { + return true; + } else { + return false; + } + } + + void SetDataOffset(long offset) { fDataOffset = offset; } + void SetRsrcOffset(long offset) { fRsrcOffset = offset; } + +private: + /* + * Copy data from the seeked archive to outfp, possibly converting EOL along + * the way. + */ + int CopyData(long srcLen, FILE* outfp, ConvertEOL conv, + ConvertHighASCII convHA, CString* pMsg) const; + + AppleSingleArchive* fpArchive; // holds FILE* for archive + long fDataOffset; + long fRsrcOffset; +}; + + +/* + * AppleSingle archive definition. + */ +class AppleSingleArchive : public GenericArchive { +public: + AppleSingleArchive(void) : fFp(NULL), fEntries(NULL), fIsBigEndian(false) {} + virtual ~AppleSingleArchive(void) { + (void) Close(); + delete[] fEntries; + } + + /* + * Perform one-time initialization. There really isn't any for us. + * + * Returns an error string on failure. + */ + static CString AppInit(void); + + /* + * Open an AppleSingle archive. + * + * Returns an error string on failure, or "" on success. + */ + virtual OpenResult Open(const WCHAR* filename, bool readOnly, + CString* pErrMsg) override; + + /* + * Create a new AppleSingleArchive instance. + * + * This isn't implemented, and will always return an error. + */ + virtual CString New(const WCHAR* filename, const void* options) override; + + virtual CString Flush(void) override { return L""; } + + virtual CString Reload(void) override; + virtual bool IsReadOnly(void) const override { return true; }; + virtual bool IsModified(void) const override { return false; } + virtual void GetDescription(CString* pStr) const override + { *pStr = L"AppleSingle"; } + virtual bool BulkAdd(ActionProgressDialog* pActionProgress, + const AddFilesDialog* pAddOpts) override + { ASSERT(false); return false; } + virtual bool AddDisk(ActionProgressDialog* pActionProgress, + const AddFilesDialog* pAddOpts) override + { ASSERT(false); return false; } + virtual bool CreateSubdir(CWnd* pMsgWnd, GenericEntry* pParentEntry, + const WCHAR* newName) override + { ASSERT(false); return false; } + virtual bool TestSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) override + { ASSERT(false); return false; } + virtual bool DeleteSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) override + { ASSERT(false); return false; } + virtual bool RenameSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) override + { ASSERT(false); return false; } + virtual bool RenameVolume(CWnd* pMsgWnd, DiskFS* pDiskFS, + const WCHAR* newName) override + { ASSERT(false); return false; } + virtual CString TestVolumeName(const DiskFS* pDiskFS, + const WCHAR* newName) const override + { ASSERT(false); return L"!"; } + virtual CString TestPathName(const GenericEntry* pGenericEntry, + const CString& basePath, const CString& newName, char newFssep) const override + { ASSERT(false); return L"!"; } + virtual bool RecompressSelection(CWnd* pMsgWnd, SelectionSet* pSelSet, + const RecompressOptionsDialog* pRecompOpts) override + { ASSERT(false); return false; } + virtual XferStatus XferSelection(CWnd* pMsgWnd, SelectionSet* pSelSet, + ActionProgressDialog* pActionProgress, + const XferFileOptions* pXferOpts) override + { ASSERT(false); return kXferFailed; } + virtual bool GetComment(CWnd* pMsgWnd, const GenericEntry* pEntry, + CString* pStr) override + { ASSERT(false); return false; } + virtual bool SetComment(CWnd* pMsgWnd, GenericEntry* pEntry, + const CString& str) override + { ASSERT(false); return false; } + virtual bool DeleteComment(CWnd* pMsgWnd, GenericEntry* pEntry) override + { ASSERT(false); return false; } + virtual bool SetProps(CWnd* pMsgWnd, GenericEntry* pEntry, + const FileProps* pProps) override + { ASSERT(false); return false; } + virtual void PreferencesChanged(void) override {} + virtual long GetCapability(Capability cap) override; + + // Generate a string for the "archive info" dialog. + CString GetInfoString(); + + friend class AppleSingleEntry; + +private: + // File header. "homeFileSystem" became all-zero "filler" in v2. + static const int kHomeFileSystemLen = 16; + static const int kMagicNumber = 0x00051600; + static const int kVersion1 = 0x00010000; + static const int kVersion2 = 0x00020000; + struct FileHeader { + uint32_t magic; + uint32_t version; + char homeFileSystem[kHomeFileSystemLen + 1]; + uint16_t numEntries; + }; + static const size_t kHeaderLen = 4 + 4 + kHomeFileSystemLen + 2; + + // Array of these, just past the file header. + struct TOCEntry { + uint32_t entryId; + uint32_t offset; + uint32_t length; + }; + static const size_t kEntryLen = 4 + 4 + 4; + + // predefined values for entryId + enum { + kIdDataFork = 1, + kIdResourceFork = 2, + kIdRealName = 3, + kIdComment = 4, + kIdBWIcon = 5, + kIdColorIcon = 6, + kIdFileInfo = 7, // version 1 only + kIdFileDatesInfo = 8, // version 2 only + kIdFinderInfo = 9, + kIdMacintoshFileInfo = 10, // here and below are version 2 only + kIdProDOSFileInfo = 11, + kIdMSDOSFileInfo = 12, + kIdShortName = 13, + kIdAFPFileInfo = 14, + kIdDirectoryId = 15 + }; + + virtual CString Close(void) { + if (fFp != NULL) { + fclose(fFp); + fFp = NULL; + } + return L""; + } + virtual void XferPrepare(const XferFileOptions* pXferOpts) override + { ASSERT(false); } + virtual CString XferFile(LocalFileDetails* pDetails, uint8_t** pDataBuf, + long dataLen, uint8_t** pRsrcBuf, long rsrcLen) override + { ASSERT(false); return L"!"; } + virtual void XferAbort(CWnd* pMsgWnd) override + { ASSERT(false); } + virtual void XferFinish(CWnd* pMsgWnd) override + { ASSERT(false); } + + virtual ArchiveKind GetArchiveKind(void) override { return kArchiveAppleSingle; } + virtual NuError DoAddFile(const AddFilesDialog* pAddOpts, + LocalFileDetails* pDetails) override + { ASSERT(false); return kNuErrGeneric; } + + + /* + * Loads the contents of the archive. + * + * Returns 0 on success, < 0 if this is not an AppleSingle file, or + * > 0 if this appears to be an AppleSingle file but it's damaged. + */ + int LoadContents(); + + /* + * Confirms that the file is big enough to hold all of the entries + * listed in the table of contents. + */ + bool CheckFileLength(); + + /* + * Creates our one and only AppleSingleEntry instance by walking through + * the various bits of info. + */ + bool CreateEntry(); + + /* + * Reads the "real name" chunk, converting the character set to + * Mac OS Roman if necessary. (If we wanted to be a general AppleSingle + * tool we wouldn't do that... but we're not.) + */ + bool HandleRealName(const TOCEntry* tocEntry, AppleSingleEntry* pEntry); + + /* + * Reads the version 1 File Info chunk, which is OS-specific. The data + * layout is determined by the "home file system" string in the header. + * + * We only really want to find a ProDOS chunk. The Macintosh chunk doesn't + * have the file type in it. + * + * This will set the access, file type, aux type, create date/time, and + * modification date/time. + */ + bool HandleFileInfo(const TOCEntry* tocEntry, AppleSingleEntry* pEntry); + + /* + * Reads the version 2 File Dates Info chunk, which provides various + * dates as 32-bit seconds since Jan 1 2000 UTC. Nothing else uses + * this, making it equally inconvenient on all systems. + */ + bool HandleFileDatesInfo(const TOCEntry* tocEntry, + AppleSingleEntry* pEntry); + + /* + * Reads a ProDOS file info block, using the values to set the access, + * file type, and aux type fields. + */ + bool HandleProDOSFileInfo(const TOCEntry* tocEntry, + AppleSingleEntry* pEntry); + + /* + * Reads a Finder info block, using the values to set the file type and + * aux type. + */ + bool HandleFinderInfo(const TOCEntry* tocEntry, AppleSingleEntry* pEntry); + + /* + * Convert from ProDOS compact date format to time_t (time in seconds + * since Jan 1 1970 UTC). + */ + time_t ConvertProDOSDateTime(uint16_t prodosDate, uint16_t prodosTime); + + void DumpArchive(); + + FILE* fFp; + bool fIsBigEndian; + FileHeader fHeader; + TOCEntry* fEntries; +}; + +#endif /*APP_APPLESINGLEARCHIVE_H*/ diff --git a/app/ArchiveInfoDialog.cpp b/app/ArchiveInfoDialog.cpp index d18afe1..d437c0f 100644 --- a/app/ArchiveInfoDialog.cpp +++ b/app/ArchiveInfoDialog.cpp @@ -395,3 +395,23 @@ BOOL AcuArchiveInfoDialog::OnInitDialog(void) return ArchiveInfoDialog::OnInitDialog(); } + +/* + * =========================================================================== + * AppleSingleArchiveInfoDialog + * =========================================================================== + */ + +BOOL AppleSingleArchiveInfoDialog::OnInitDialog(void) +{ + CWnd* pWnd; + + ASSERT(fpArchive != NULL); + + pWnd = GetDlgItem(IDC_AI_FILENAME); + pWnd->SetWindowText(fpArchive->GetPathName()); + pWnd = GetDlgItem(IDC_AIBNY_RECORDS); + pWnd->SetWindowText(fpArchive->GetInfoString()); + + return ArchiveInfoDialog::OnInitDialog(); +} diff --git a/app/ArchiveInfoDialog.h b/app/ArchiveInfoDialog.h index e6ed139..0b48334 100644 --- a/app/ArchiveInfoDialog.h +++ b/app/ArchiveInfoDialog.h @@ -15,6 +15,7 @@ #include "DiskArchive.h" #include "BnyArchive.h" #include "AcuArchive.h" +#include "AppleSingleArchive.h" /* * This is an abstract base class for the archive info dialogs. There is @@ -130,4 +131,21 @@ private: AcuArchive* fpArchive; }; +/* + * AppleSingle archive info. + */ +class AppleSingleArchiveInfoDialog : public ArchiveInfoDialog { +public: + AppleSingleArchiveInfoDialog(AppleSingleArchive* pArchive, CWnd* pParentWnd = NULL) : + fpArchive(pArchive), + ArchiveInfoDialog(IDD_ARCHIVEINFO_APPLESINGLE, pParentWnd) + {} + virtual ~AppleSingleArchiveInfoDialog(void) {} + +private: + virtual BOOL OnInitDialog(void) override; + + AppleSingleArchive* fpArchive; +}; + #endif /*APP_ARCHIVEINFODIALOG_H*/ diff --git a/app/CiderPress.rc b/app/CiderPress.rc index 51110fb..0bc270a 100644 --- a/app/CiderPress.rc +++ b/app/CiderPress.rc @@ -1226,6 +1226,20 @@ BEGIN CTEXT ">count<",IDC_PROGRESS_COUNTER_COUNT,7,19,172,8 END +IDD_ARCHIVEINFO_APPLESINGLE DIALOGEX 0, 0, 320, 74 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "AppleSingle File" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + DEFPUSHBUTTON "Done",IDOK,158,53,50,14 + LTEXT "Filename:",IDC_STATIC,7,18,68,8,0,WS_EX_RIGHT + LTEXT "Info:",IDC_STATIC,7,29,68,8,0,WS_EX_RIGHT + GROUPBOX "Archive Info",IDC_STATIC,6,7,307,36 + LTEXT "",IDC_AI_FILENAME,84,18,218,8 + LTEXT "",IDC_AIBNY_RECORDS,84,29,218,8 + PUSHBUTTON "Help",IDHELP,105,53,50,14 +END + ///////////////////////////////////////////////////////////////////////////// // @@ -1807,6 +1821,13 @@ BEGIN TOPMARGIN, 7 BOTTOMMARGIN, 50 END + + IDD_ARCHIVEINFO_APPLESINGLE, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 302 + TOPMARGIN, 7 + END END #endif // APSTUDIO_INVOKED diff --git a/app/GenericArchive.h b/app/GenericArchive.h index 8ab0f32..a37aa06 100644 --- a/app/GenericArchive.h +++ b/app/GenericArchive.h @@ -419,6 +419,7 @@ public: kArchiveNuFX, kArchiveBNY, kArchiveACU, + kArchiveAppleSingle, kArchiveDiskImage, }; @@ -768,6 +769,38 @@ public: */ static void UNIXTimeToDateTime(const time_t* pWhen, NuDateTime *pDateTime); + /* + * Reads a 16-bit big-endian value from a buffer. Does not guard + * against buffer overrun. + */ + static uint16_t Get16BE(const uint8_t* ptr) { + return ptr[1] | (ptr[0] << 8); + } + + /* + * Reads a 32-bit big-endian value from a buffer. Does not guard + * against buffer overrun. + */ + static uint32_t Get32BE(const uint8_t* ptr) { + return ptr[3] | (ptr[2] << 8) | (ptr[1] << 16) | (ptr[0] << 24); + } + + /* + * Reads a 16-bit little-endian value from a buffer. Does not guard + * against buffer overrun. + */ + static uint16_t Get16LE(const uint8_t* ptr) { + return ptr[0] | (ptr[1] << 8); + } + + /* + * Reads a 32-bit little-endian value from a buffer. Does not guard + * against buffer overrun. + */ + static uint32_t Get32LE(const uint8_t* ptr) { + return ptr[0] | (ptr[1] << 8) | (ptr[2] << 16) | (ptr[3] << 24); + } + protected: /* * Delete the "entries" list. diff --git a/app/Main.cpp b/app/Main.cpp index 4ace631..71f11c7 100644 --- a/app/Main.cpp +++ b/app/Main.cpp @@ -14,6 +14,7 @@ #include "DiskArchive.h" #include "BNYArchive.h" #include "ACUArchive.h" +#include "AppleSingleArchive.h" #include "ArchiveInfoDialog.h" #include "PrefsDialog.h" #include "EnterRegDialog.h" @@ -35,18 +36,43 @@ static const WCHAR kMainWindowClassName[] = L"faddenSoft.CiderPress.4"; * Filters for the "open file" command. In some cases a file may be opened * in more than one format, so it's necessary to keep track of what the * file filter was set to when the file was opened. + * + * With Vista-style dialogs, the second part of the string (the filespec) + * will sometimes be included in the pop-up. Sometimes not. It's + * deterministic but I haven't been able to figure out what the pattern is -- + * it's not simply length of a given filter or of the entire string, or based + * on the presence of certain characters. The filter works correctly, so it + * doesn't seem to be malformed. It's just ugly to have the open dialog + * popup show an enormous, redundant filter string. + * + * I tried substituting '\0' for '|' and placing the string directly into + * the dialog; no change. + * + * CFileDialog::ApplyOFNToShellDialog in {VisualStudio}\VC\atlmfc\src\mfc\dlgfile.cpp + * appears to be doing the parsing. Single-stepping through the code shows + * that it's working fine, so something later on is choosing to merge + * pszName and pszSpec when generating the pop-up menu. + * + * The good news is that if I exclude the list of extensions from the name + * section, the popup will (so far) always includes the spec. The bad news + * is that this means we won't display the list of extensions on WinXP, which + * uses the older style of dialog. We could switch from public constants to + * a function that generates the filter based on a bit mask and the current + * OS version, but that might be more trouble than it's worth. */ const WCHAR MainWindow::kOpenNuFX[] = - L"ShrinkIt Archives (.shk .sdk .bxy .sea .bse)|*.shk;*.sdk;*.bxy;*.sea;*.bse|"; + L"ShrinkIt Archives" /* (.shk .sdk .bxy .sea .bse)*/ L"|*.shk;*.sdk;*.bxy;*.sea;*.bse|"; const WCHAR MainWindow::kOpenBinaryII[] = - L"Binary II Archives (.bny .bqy .bxy)|*.bny;*.bqy;*.bxy|"; + L"Binary II Archives" /* (.bny .bqy .bxy)*/ L"|*.bny;*.bqy;*.bxy|"; const WCHAR MainWindow::kOpenACU[] = - L"ACU Archives (.acu)|*.acu|"; + L"ACU Archives" /* (.acu)*/ L"|*.acu|"; +const WCHAR MainWindow::kOpenAppleSingle[] = + L"AppleSingle files" /* (.as *.*)*/ L"|*.as;*.*|"; const WCHAR MainWindow::kOpenDiskImage[] = - L"Disk Images (.shk .sdk .dsk .po .do .d13 .2mg .img .nib .nb2 .raw .hdv .dc .dc6 .ddd .app .fdi .iso .gz .zip)|" - L"*.shk;*.sdk;*.dsk;*.po;*.do;*.d13;*.2mg;*.img;*.nib;*.nb2;*.raw;*.hdv;*.dc;*.dc6;*.ddd;*.app;*.fdi;*.iso;*.gz;*.zip|"; + L"Disk Images" /* (.shk .sdk .dsk .po .do .d13 .2mg .img .nib .nb2 .raw .hdv .dc .dc6 .ddd .app .fdi .iso .gz .zip)*/ L"|" + L"*.shk;*.sdk;*.dsk;*.po;*.do;*.d13;*.2mg;*.img;*.nib;*.nb2;*.raw;*.hdv;*.dc;*.dc6;*.ddd;*.app;*.fdi;*.iso;*.gz;*.zip|"; const WCHAR MainWindow::kOpenAll[] = - L"All Files (*.*)|*.*|"; + L"All Files" /* (*.*)*/ L"|*.*|"; const WCHAR MainWindow::kOpenEnd[] = L"|"; @@ -61,6 +87,7 @@ static const struct { { L"bny", kFilterIndexBinaryII }, { L"bqy", kFilterIndexBinaryII }, { L"acu", kFilterIndexACU }, + { L"as", kFilterIndexAppleSingle }, { L"dsk", kFilterIndexDiskImage }, { L"po", kFilterIndexDiskImage }, { L"do", kFilterIndexDiskImage }, @@ -80,6 +107,7 @@ static const struct { const WCHAR MainWindow::kModeNuFX[] = L"nufx"; const WCHAR MainWindow::kModeBinaryII[] = L"bin2"; const WCHAR MainWindow::kModeACU[] = L"acu"; +const WCHAR MainWindow::kModeAppleSingle[] = L"as"; const WCHAR MainWindow::kModeDiskImage[] = L"disk"; @@ -403,6 +431,8 @@ void MainWindow::ProcessCommandLine(void) filterIndex = kFilterIndexBinaryII; else if (wcsicmp(argv[i], kModeACU) == 0) filterIndex = kFilterIndexACU; + else if (wcsicmp(argv[i], kModeAppleSingle) == 0) + filterIndex = kFilterIndexAppleSingle; else if (wcsicmp(argv[i], kModeDiskImage) == 0) filterIndex = kFilterIndexDiskImage; else { @@ -1143,7 +1173,7 @@ void MainWindow::OnFileNewArchive(void) } bail: - LOGI("--- OnFileNewArchive done"); + LOGD("--- OnFileNewArchive done"); } void MainWindow::OnFileOpen(void) @@ -1153,13 +1183,15 @@ void MainWindow::OnFileOpen(void) CString openFilters; CString saveFolder; - /* set up filters; the order is significant */ + /* set up filters; the order must match enum FilterIndex */ openFilters = kOpenNuFX; openFilters += kOpenBinaryII; openFilters += kOpenACU; + openFilters += kOpenAppleSingle; openFilters += kOpenDiskImage; openFilters += kOpenAll; openFilters += kOpenEnd; + LOGD("filters: '%ls'", openFilters); CFileDialog dlg(TRUE, L"shk", NULL, OFN_FILEMUSTEXIST, openFilters, this); @@ -1288,8 +1320,11 @@ void MainWindow::OnFileArchiveInfo(void) case GenericArchive::kArchiveACU: pDlg = new AcuArchiveInfoDialog((AcuArchive*) fpOpenArchive, this); break; + case GenericArchive::kArchiveAppleSingle: + pDlg = new AppleSingleArchiveInfoDialog((AppleSingleArchive*) fpOpenArchive, this); + break; default: - LOGI("Unexpected archive type %d", fpOpenArchive->GetArchiveKind()); + LOGW("Unexpected archive type %d", fpOpenArchive->GetArchiveKind()); ASSERT(false); return; }; @@ -1484,11 +1519,19 @@ void MainWindow::HandleDoubleClick(void) TmpExtractAndOpen(pEntry, GenericEntry::kDataThread, kModeACU); handled = true; } else + if ((ext != NULL && ( + wcsicmp(ext, L".as") == 0)) || + (fileType == 0xe0 && auxType == 0x0001)) + { + LOGI(" Guessing AppleSingle"); + TmpExtractAndOpen(pEntry, GenericEntry::kDataThread, kModeAppleSingle); + handled = true; + } else if (fileType == 0x64496d67 && auxType == 0x64437079 && pEntry->GetUncompressedLen() == 819284) { /* type is dImg, creator is dCpy, length is 800K + DC stuff */ - LOGI(" Looks like a disk image"); + LOGI(" Looks like a DiskCopy disk image"); TmpExtractAndOpen(pEntry, GenericEntry::kDataThread, kModeDiskImage); handled = true; } @@ -1864,6 +1907,16 @@ int MainWindow::LoadArchive(const WCHAR* fileName, const WCHAR* extension, * up here, and maybe do a little "open it up and see" stuff as well. * In general, though, if we don't recognize the extension, it's * probably a disk image. + * + * TODO: different idea: always pass the file to each of the different + * archive handlers, which will provide an "is this your file" method. + * If the filter matches, open according to the filter. If it doesn't, + * open it according to whatever stepped up to claim it. Consider + * altering the UI to offer a disambiguation dialog that shows all the + * things it could possibly be (though that might be annoying if it comes + * up every time on e.g. .SDK files). The ultimate goal is to avoid + * saying, "I can't open that", when we actually could if the filter was + * set to the right thing. */ if (filterIndex == kFilterIndexGeneric) { int i; @@ -1906,6 +1959,19 @@ try_again: goto bail; } } else + if (filterIndex == kFilterIndexAppleSingle) { + /* try AppleSingle and nothing else */ + ASSERT(!createFile); + LOGD(" Trying AppleSingle"); + pOpenArchive = new AppleSingleArchive; + openResult = pOpenArchive->Open(fileName, readOnly, &errStr); + if (openResult != GenericArchive::kResultSuccess) { + if (!errStr.IsEmpty()) + ShowFailureMsg(this, errStr, IDS_FAILED); + result = -1; + goto bail; + } + } else if (filterIndex == kFilterIndexDiskImage) { /* try various disk image formats */ ASSERT(!createFile); diff --git a/app/Main.h b/app/Main.h index 70556a5..30d72f4 100644 --- a/app/Main.h +++ b/app/Main.h @@ -25,13 +25,14 @@ #define WMU_LATE_INIT (WM_USER+0) #define WMU_START (WM_USER+1) // used by ActionProgressDialog -typedef enum { +enum FilterIndex { kFilterIndexNuFX = 1, kFilterIndexBinaryII = 2, kFilterIndexACU = 3, - kFilterIndexDiskImage = 4, - kFilterIndexGeneric = 5, // *.* filter used -} FilterIndex; + kFilterIndexAppleSingle = 4, + kFilterIndexDiskImage = 5, + kFilterIndexGeneric = 6 // *.* filter used +}; struct FileCollectionEntry; // fwd @@ -247,6 +248,7 @@ public: static const WCHAR kOpenNuFX[]; static const WCHAR kOpenBinaryII[]; static const WCHAR kOpenACU[]; + static const WCHAR kOpenAppleSingle[]; static const WCHAR kOpenDiskImage[]; static const WCHAR kOpenAll[]; static const WCHAR kOpenEnd[]; @@ -255,6 +257,7 @@ private: static const WCHAR kModeNuFX[]; static const WCHAR kModeBinaryII[]; static const WCHAR kModeACU[]; + static const WCHAR kModeAppleSingle[]; static const WCHAR kModeDiskImage[]; // Command handlers diff --git a/app/MyApp.h b/app/MyApp.h index 40c6327..25538df 100644 --- a/app/MyApp.h +++ b/app/MyApp.h @@ -15,7 +15,7 @@ #define kAppMajorVersion 4 #define kAppMinorVersion 0 #define kAppBugVersion 0 -#define kAppDevString L"d2" +#define kAppDevString L"d3" /* * Windows application object. diff --git a/app/NufxArchive.cpp b/app/NufxArchive.cpp index 1bcaa6f..ef4c408 100644 --- a/app/NufxArchive.cpp +++ b/app/NufxArchive.cpp @@ -357,7 +357,7 @@ void NufxEntry::AnalyzeRecord(const NuRecord* pRecord) SetHasDataFork(true); SetDataForkLen(pThread->actualThreadEOF); } else { - LOGI("WARNING: ignoring second disk image / data fork"); + LOGW("WARNING: ignoring second disk image / data fork"); } } if (threadID == kNuThreadIDRsrcFork) { @@ -365,7 +365,7 @@ void NufxEntry::AnalyzeRecord(const NuRecord* pRecord) SetHasRsrcFork(true); SetRsrcForkLen(pThread->actualThreadEOF); } else { - LOGI("WARNING: ignoring second data fork"); + LOGW("WARNING: ignoring second data fork"); } } if (threadID == kNuThreadIDDiskImage) { @@ -373,7 +373,7 @@ void NufxEntry::AnalyzeRecord(const NuRecord* pRecord) SetHasDiskImage(true); SetDataForkLen(pThread->actualThreadEOF); } else { - LOGI("WARNING: ignoring second disk image / data fork"); + LOGW("WARNING: ignoring second disk image / data fork"); } } if (threadID == kNuThreadIDComment) { diff --git a/app/app.vcxproj b/app/app.vcxproj index 47d728e..30d75e1 100644 --- a/app/app.vcxproj +++ b/app/app.vcxproj @@ -160,6 +160,7 @@ + @@ -258,6 +259,7 @@ + diff --git a/app/app.vcxproj.filters b/app/app.vcxproj.filters index 422234e..7d9a972 100644 --- a/app/app.vcxproj.filters +++ b/app/app.vcxproj.filters @@ -197,6 +197,9 @@ Header Files + + Header Files + @@ -414,5 +417,8 @@ Source Files + + Source Files + \ No newline at end of file diff --git a/app/resource.h b/app/resource.h index 47124cb..49d3bc5 100644 --- a/app/resource.h +++ b/app/resource.h @@ -68,6 +68,7 @@ #define IDD_IMPORT_BAS 189 #define IDD_PASTE_SPECIAL 190 #define IDD_PROGRESS_COUNTER 191 +#define IDD_ARCHIVEINFO_APPLESINGLE 192 #define IDC_NUFXLIB_VERS_TEXT 1001 #define IDC_CONTENT_LIST 1002 #define IDC_COL_PATHNAME 1005 @@ -567,7 +568,7 @@ // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 192 +#define _APS_NEXT_RESOURCE_VALUE 193 #define _APS_NEXT_COMMAND_VALUE 40102 #define _APS_NEXT_CONTROL_VALUE 1454 #define _APS_NEXT_SYMED_VALUE 102