nulib2/nulib2/List.c
Andy McFadden f37b387cc6 Fix handling of entries with missing threads
When GSHK adds files to an archive, it doesn't create threads for
zero-length data and resource forks.  NufxLib had a workaround
for this, but it wasn't handling all possible cases.  We now
fully handle "Miranda threads" (if you cannot afford a thread,
one will be provided for you).

This broke test-basic, because a callback gets called one extra
time now due to the additional thread.  It also broke test-twirl,
which uses "mask dataless" and is sensitive to the order in which
threads appear.  (test-twirl actually works just fine, but the
CRC check is too simple-minded, and is arguably incorrect.)

Since this can apparently break things, I'm making this a minor
version bump, to 3.1.0-a1.

I also tweaked the NuLib2 file listing to test for the extended
file storage type, rather than simply scanning for data threads.
Forked files are now listed as such, even when they're missing
the actual resource fork data thread.
2015-12-26 12:05:13 -08:00

469 lines
13 KiB
C

/*
* NuLib2
* Copyright (C) 2000-2007 by Andy McFadden, All Rights Reserved.
* This is free software; you can redistribute it and/or modify it under the
* terms of the BSD License, see the file COPYING.
*
* List files in the archive.
*/
#include "NuLib2.h"
/* kinds of records */
enum RecordKind {
kRecordKindUnknown = 0,
kRecordKindDisk,
kRecordKindFile,
kRecordKindForkedFile
};
static const char* gShortFormatNames[] = {
"unc", "squ", "lz1", "lz2", "u12", "u16", "dfl", "bzp"
};
#if 0
/* days of the week */
static const char* gDayNames[] = {
"[ null ]",
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
};
#endif
/* months of the year */
static const char* gMonths[] = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};
/*
* Compute a percentage.
*/
int ComputePercent(uint32_t totalSize, uint32_t size)
{
int perc;
if (!totalSize && !size)
return 100; /* file is zero bytes long */
if (totalSize < 21474836)
perc = (totalSize * 100) / size;
else
perc = totalSize / (size/100);
/* don't say "0%" if it's not actually zero... it looks dumb */
if (!perc && size)
perc = 1;
return perc;
}
/*
* Convert a NuDateTime structure into something printable. This uses an
* abbreviated format, with date and time but not weekday or seconds.
*
* The buffer passed in must hold at least kDateOutputLen bytes.
*
* Returns "buffer" for the benefit of printf() calls.
*/
char* FormatDateShort(const NuDateTime* pDateTime, char* buffer)
{
/* is it valid? */
if (pDateTime->day > 30 || pDateTime->month > 11 || pDateTime->hour > 24 ||
pDateTime->minute > 59)
{
strcpy(buffer, " <invalid> ");
goto bail;
}
/* is it empty? */
if ((pDateTime->second | pDateTime->minute | pDateTime->hour |
pDateTime->year | pDateTime->day | pDateTime->month |
pDateTime->extra | pDateTime->weekDay) == 0)
{
strcpy(buffer, " [No Date] ");
goto bail;
}
sprintf(buffer, "%02d-%s-%02d %02d:%02d",
pDateTime->day+1, gMonths[pDateTime->month], pDateTime->year % 100,
pDateTime->hour, pDateTime->minute);
bail:
return buffer;
}
/*
* NuStream callback function. Displays the filename.
*/
static NuResult ShowContentsShort(NuArchive* pArchive, void* vpRecord)
{
const NuRecord* pRecord = (NuRecord*) vpRecord;
NulibState* pState;
Assert(pArchive != NULL);
(void) NuGetExtraData(pArchive, (void**) &pState);
Assert(pState != NULL);
if (!IsSpecified(pState, pRecord))
goto bail;
UNICHAR* filenameUNI = CopyMORToUNI(pRecord->filenameMOR);
printf("%s\n", filenameUNI == NULL ? "<unknown>" : filenameUNI);
free(filenameUNI);
bail:
return kNuOK;
}
/*
* Analyze the contents of a record to determine if it's a disk, file,
* or "other". Compute the total compressed and uncompressed lengths
* of all data threads. Return the "best" format.
*
* The "best format" and "record type" stuff assume that the entire
* record contains only a disk thread or a file thread, and that any
* format is interesting so long as it isn't "no compression". In
* general these will be true, because ShrinkIt and NuLib create files
* this way.
*
* You could, of course, create a single record with a data thread and
* a disk image thread, but it's a fair bet ShrinkIt would ignore one
* or the other.
*
* NOTE: GSHK likes to omit the data threads for zero-length data and
* resource forks. That screws up analysis by scanning for threads. We
* can work around missing resource forks by simply checking the record's
* storage type. We could be clever and detect records that have no
* data-class threads at all, and no additional threads other than a
* comment and filename, but this is just for display. ShrinkIt doesn't
* handle these records correctly in all cases, so flagging them in a
* user-visible way seems reasonable.
*/
static NuError AnalyzeRecord(const NuRecord* pRecord,
enum RecordKind* pRecordKind, uint16_t* pFormat, uint32_t* pTotalLen,
uint32_t* pTotalCompLen)
{
const NuThread* pThread;
NuThreadID threadID;
uint32_t idx;
*pRecordKind = kRecordKindUnknown;
*pTotalLen = *pTotalCompLen = 0;
*pFormat = kNuThreadFormatUncompressed;
for (idx = 0; idx < pRecord->recTotalThreads; idx++) {
pThread = NuGetThread(pRecord, idx);
Assert(pThread != NULL);
if (pThread->thThreadClass == kNuThreadClassData) {
/* replace what's there if this might be more interesting */
if (*pFormat == kNuThreadFormatUncompressed)
*pFormat = pThread->thThreadFormat;
threadID = NuMakeThreadID(pThread->thThreadClass,
pThread->thThreadKind);
if (threadID == kNuThreadIDRsrcFork)
*pRecordKind = kRecordKindForkedFile;
else if (threadID == kNuThreadIDDiskImage)
*pRecordKind = kRecordKindDisk;
else if (threadID == kNuThreadIDDataFork &&
*pRecordKind == kRecordKindUnknown)
*pRecordKind = kRecordKindFile;
/* sum up, so we get both forks of forked files */
*pTotalLen += pThread->actualThreadEOF;
*pTotalCompLen += pThread->thCompThreadEOF;
}
}
/*
* Fix up the case where we have a forked file, but GSHK decided not
* to include a resource fork in the record.
*/
if (pRecord->recStorageType == kNuStorageExtended &&
*pRecordKind != kRecordKindForkedFile)
{
*pRecordKind = kRecordKindForkedFile;
}
return kNuErrNone;
}
/*
* NuStream callback function. Displays the filename and several attributes.
*
* This is intended to mimic the output of some old version of ProDOS 8
* ShrinkIt.
*/
static NuResult ShowContentsVerbose(NuArchive* pArchive, void* vpRecord)
{
NuError err = kNuErrNone;
const NuRecord* pRecord = (NuRecord*) vpRecord;
enum RecordKind recordKind;
uint32_t totalLen, totalCompLen;
uint16_t format;
NulibState* pState;
char date1[kDateOutputLen];
char tmpbuf[16];
int len;
Assert(pArchive != NULL);
(void) NuGetExtraData(pArchive, (void**) &pState);
Assert(pState != NULL);
if (!IsSpecified(pState, pRecord))
goto bail;
err = AnalyzeRecord(pRecord, &recordKind, &format, &totalLen,
&totalCompLen);
if (err != kNuErrNone)
goto bail;
/*
* Display the filename, truncating if it's longer than 27 characters.
*
* Attempting to do column layout with printf string formatting (e.g.
* "%-27s") doesn't really work for UTF-8 because printf() is
* operating on bytes, and the conversion to a Unicode code point
* is happening in the terminal. We need to do the spacing ourselves,
* using the fact that one MOR character turns into one Unicode character.
*
* If the display isn't converting multi-byte sequences to individual
* characters, this won't look right, but we can't make everybody happy.
*/
static const char kSpaces[27+1] = " ";
UNICHAR* filenameUNI;
len = strlen(pRecord->filenameMOR);
if (len <= 27) {
filenameUNI = CopyMORToUNI(pRecord->filenameMOR);
printf("%c%s%s ", IsRecordReadOnly(pRecord) ? '+' : ' ',
filenameUNI, &kSpaces[len]);
} else {
filenameUNI = CopyMORToUNI(pRecord->filenameMOR + len - 25);
printf("%c..%s ", IsRecordReadOnly(pRecord) ? '+' : ' ',
filenameUNI);
}
free(filenameUNI);
switch (recordKind) {
case kRecordKindUnknown:
printf("%s- $%04X ",
GetFileTypeString(pRecord->recFileType),
pRecord->recExtraType);
break;
case kRecordKindDisk:
sprintf(tmpbuf, "%dk", totalLen / 1024);
printf("Disk %-6s ", tmpbuf);
break;
case kRecordKindFile:
case kRecordKindForkedFile:
printf("%s%c $%04X ",
GetFileTypeString(pRecord->recFileType),
recordKind == kRecordKindForkedFile ? '+' : ' ',
pRecord->recExtraType);
break;
default:
Assert(0);
printf("ERROR ");
}
printf("%s ", FormatDateShort(&pRecord->recArchiveWhen, date1));
if (format >= NELEM(gShortFormatNames))
printf("??? ");
else
printf("%s ", gShortFormatNames[format]);
/* compute the percent size */
if ((!totalLen && totalCompLen) || (totalLen && !totalCompLen))
printf("--- "); /* weird */
else if (totalLen < totalCompLen)
printf(">100%% "); /* compression failed? */
else {
sprintf(tmpbuf, "%02d%%", ComputePercent(totalCompLen, totalLen));
printf("%4s ", tmpbuf);
}
if (!totalLen && totalCompLen)
printf(" ????"); /* weird */
else
printf("%8u", totalLen);
printf("\n");
NState_AddToTotals(pState, totalLen, totalCompLen);
bail:
if (err != kNuErrNone) {
filenameUNI = CopyMORToUNI(pRecord->filenameMOR);
printf("(ERROR on '%s')\n",
filenameUNI == NULL ? "<unknown>" : filenameUNI);
free(filenameUNI);
}
return kNuOK;
}
/*
* Print a short listing of the contents of an archive.
*/
NuError DoListShort(NulibState* pState)
{
NuError err;
NuArchive* pArchive = NULL;
Assert(pState != NULL);
if (NState_GetModBinaryII(pState))
return BNYDoListShort(pState);
err = OpenArchiveReadOnly(pState);
if (err == kNuErrIsBinary2)
return BNYDoListShort(pState);
if (err != kNuErrNone)
goto bail;
pArchive = NState_GetNuArchive(pState);
Assert(pArchive != NULL);
err = NuContents(pArchive, ShowContentsShort);
/* fall through with err */
bail:
if (pArchive != NULL)
(void) NuClose(pArchive);
return err;
}
/*
* Print a more verbose listing of the contents of an archive.
*/
NuError DoListVerbose(NulibState* pState)
{
NuError err;
NuArchive* pArchive = NULL;
const NuMasterHeader* pHeader;
char date1[kDateOutputLen];
char date2[kDateOutputLen];
long totalLen, totalCompLen;
const char* cp;
Assert(pState != NULL);
if (NState_GetModBinaryII(pState))
return BNYDoListVerbose(pState);
err = OpenArchiveReadOnly(pState);
if (err == kNuErrIsBinary2)
return BNYDoListVerbose(pState);
if (err != kNuErrNone)
goto bail;
pArchive = NState_GetNuArchive(pState);
Assert(pArchive != NULL);
/*
* Try to get just the filename.
*/
if (IsFilenameStdin(NState_GetArchiveFilename(pState)))
cp = "<stdin>";
else
cp = FilenameOnly(pState, NState_GetArchiveFilename(pState));
/* grab the master header block */
err = NuGetMasterHeader(pArchive, &pHeader);
if (err != kNuErrNone)
goto bail;
printf(" %-15.15s Created:%s Mod:%s Recs:%5u\n\n",
cp,
FormatDateShort(&pHeader->mhArchiveCreateWhen, date1),
FormatDateShort(&pHeader->mhArchiveModWhen, date2),
pHeader->mhTotalRecords);
printf(" Name Type Auxtyp Archived"
" Fmat Size Un-Length\n");
printf("-------------------------------------------------"
"----------------------------\n");
err = NuContents(pArchive, ShowContentsVerbose);
if (err != kNuErrNone)
goto bail;
/*
* Show the totals. NuFX overhead can be as much as 25% for archives
* with lots of small files.
*/
NState_GetTotals(pState, &totalLen, &totalCompLen);
printf("-------------------------------------------------"
"----------------------------\n");
printf(" Uncomp: %ld Comp: %ld %%of orig: %d%%\n",
totalLen, totalCompLen,
totalLen == 0 ? 0 : ComputePercent(totalCompLen, totalLen));
#ifdef DEBUG_VERBOSE
if (!NState_GetFilespecCount(pState)) {
printf(" Overhead: %ld (%d%%)\n",
pHeader->mhMasterEOF - totalCompLen,
ComputePercent(pHeader->mhMasterEOF - totalCompLen,
pHeader->mhMasterEOF));
}
#endif
/*(void) NuDebugDumpArchive(pArchive);*/
bail:
if (pArchive != NULL)
(void) NuClose(pArchive);
return err;
}
/*
* Null callback, for those times when you don't really want to do anything.
*/
static NuResult NullCallback(NuArchive* pArchive, void* vpRecord)
{
return kNuOK;
}
/*
* Print very detailed output, suitable for debugging (requires that
* debug messages be enabled in nufxlib).
*/
NuError DoListDebug(NulibState* pState)
{
NuError err;
NuArchive* pArchive = NULL;
Assert(pState != NULL);
if (NState_GetModBinaryII(pState))
return BNYDoListDebug(pState);
err = OpenArchiveReadOnly(pState);
if (err == kNuErrIsBinary2)
return BNYDoListDebug(pState);
if (err != kNuErrNone)
goto bail;
pArchive = NState_GetNuArchive(pState);
Assert(pArchive != NULL);
/* have to do something to force the library to scan the archive */
err = NuContents(pArchive, NullCallback);
if (err != kNuErrNone)
goto bail;
err = NuDebugDumpArchive(pArchive);
if (err != kNuErrNone)
fprintf(stderr, "ERROR: debugging not enabled in nufxlib\n");
/* fall through with err */
bail:
if (pArchive != NULL)
(void) NuClose(pArchive);
return err;
}