/* * 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) { // could do ">=" to preserve empty resource forks 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); } // This doesn't matter, since we only have the file name, but it keeps // the entry from getting a weird default. pNewEntry->SetFssep(':'); 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); } }