mirror of
https://github.com/fadden/ciderpress.git
synced 2024-12-27 06:29:21 +00:00
887 lines
24 KiB
C++
887 lines
24 KiB
C++
/*
|
|
* CiderPress
|
|
* Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved.
|
|
* See the file LICENSE for distribution terms.
|
|
*/
|
|
/*
|
|
* Drive diskimglib. Similar to MDC.
|
|
*/
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <limits.h>
|
|
#include <errno.h>
|
|
#include "zlib.h"
|
|
#include "../diskimg/DiskImg.h"
|
|
#include "../prebuilt/NufxLib.h"
|
|
#include "StringArray.h"
|
|
|
|
using namespace DiskImgLib;
|
|
|
|
#define nil NULL
|
|
#define ASSERT assert
|
|
#define NELEM(x) (sizeof(x) / sizeof((x)[0]))
|
|
typedef const char* LPCTSTR;
|
|
|
|
#define UNIX_LIKE
|
|
#define HAVE_DIRENT_H // linux
|
|
#define MAX_PATH_LEN 1024
|
|
|
|
/* get a grip on this opendir/readdir stuff */
|
|
#if defined(UNIX_LIKE)
|
|
# if defined(HAVE_DIRENT_H)
|
|
# include <dirent.h>
|
|
# define DIR_NAME_LEN(dirent) ((int)strlen((dirent)->d_name))
|
|
typedef struct dirent DIR_TYPE;
|
|
# elif defined(HAVE_SYS_DIR_H)
|
|
# include <sys/dir.h>
|
|
# define DIR_NAME_LEN(direct) ((direct)->d_namlen)
|
|
typedef struct direct DIR_TYPE;
|
|
# elif defined(HAVE_NDIR_H)
|
|
# include <sys/ndir.h>
|
|
# define DIR_NAME_LEN(direct) ((direct)->d_namlen)
|
|
typedef struct direct DIR_TYPE;
|
|
# else
|
|
# error "Port this?"
|
|
# endif
|
|
#endif
|
|
|
|
/*
|
|
* Globals.
|
|
*/
|
|
FILE* gLog = nil;
|
|
pid_t gPid = getpid();
|
|
|
|
struct Stats {
|
|
long numFiles;
|
|
long numDirectories;
|
|
long goodDiskImages;
|
|
} gStats = { 0 };
|
|
|
|
typedef struct ScanOpts {
|
|
FILE* outfp;
|
|
} ScanOpts;
|
|
|
|
typedef enum RecordKind {
|
|
kRecordKindUnknown = 0,
|
|
kRecordKindDisk,
|
|
kRecordKindFile,
|
|
kRecordKindForkedFile,
|
|
kRecordKindDirectory,
|
|
kRecordKindVolumeDirectory,
|
|
} RecordKind;
|
|
|
|
|
|
//#define kFilenameExtDelim '.' /* separates extension from filename */
|
|
|
|
/* time_t values for bad dates */
|
|
#define kDateNone ((time_t) -2)
|
|
#define kDateInvalid ((time_t) -1) // should match return from mktime()
|
|
|
|
/*
|
|
* "buf" must hold at least 64 chars.
|
|
*/
|
|
void
|
|
FormatDate(time_t when, char* buf)
|
|
{
|
|
if (when == kDateNone) {
|
|
strcpy(buf, "[No Date]");
|
|
} else if (when == kDateInvalid) {
|
|
strcpy(buf, "<invalid>");
|
|
} else {
|
|
struct tm* ptm;
|
|
|
|
ptm = localtime(&when);
|
|
strftime(buf, 64, "%d-%b-%y %H:%M", ptm);
|
|
}
|
|
}
|
|
|
|
#if 0
|
|
/*
|
|
* Find the filename component of a local pathname. Uses the fssep passed
|
|
* in. If the fssep is '\0' (as is the case for DOS 3.3), then the entire
|
|
* pathname is returned.
|
|
*
|
|
* Always returns a pointer to a string; never returns nil.
|
|
*/
|
|
const char*
|
|
FilenameOnly(const char* pathname, char fssep)
|
|
{
|
|
const char* retstr;
|
|
const char* pSlash;
|
|
char* tmpStr = nil;
|
|
|
|
ASSERT(pathname != nil);
|
|
if (fssep == '\0') {
|
|
retstr = pathname;
|
|
goto bail;
|
|
}
|
|
|
|
pSlash = strrchr(pathname, fssep);
|
|
if (pSlash == nil) {
|
|
retstr = pathname; /* whole thing is the filename */
|
|
goto bail;
|
|
}
|
|
|
|
pSlash++;
|
|
if (*pSlash == '\0') {
|
|
if (strlen(pathname) < 2) {
|
|
retstr = pathname; /* the pathname is just "/"? Whatever */
|
|
goto bail;
|
|
}
|
|
|
|
/* some bonehead put an fssep on the very end; back up before it */
|
|
/* (not efficient, but this should be rare, and I'm feeling lazy) */
|
|
tmpStr = strdup(pathname);
|
|
tmpStr[strlen(pathname)-1] = '\0';
|
|
pSlash = strrchr(tmpStr, fssep);
|
|
|
|
if (pSlash == nil) {
|
|
retstr = pathname; /* just a filename with a '/' after it */
|
|
goto bail;
|
|
}
|
|
|
|
pSlash++;
|
|
if (*pSlash == '\0') {
|
|
retstr = pathname; /* I give up! */
|
|
goto bail;
|
|
}
|
|
|
|
retstr = pathname + (pSlash - tmpStr);
|
|
|
|
} else {
|
|
retstr = pSlash;
|
|
}
|
|
|
|
bail:
|
|
free(tmpStr);
|
|
return retstr;
|
|
}
|
|
|
|
/*
|
|
* Return the filename extension found in a full pathname.
|
|
*
|
|
* An extension is the stuff following the last '.' in the filename. If
|
|
* there is nothing following the last '.', then there is no extension.
|
|
*
|
|
* Returns a pointer to the '.' preceding the extension, or nil if no
|
|
* extension was found.
|
|
*
|
|
* We guarantee that there is at least one character after the '.'.
|
|
*/
|
|
const char*
|
|
FindExtension(const char* pathname, char fssep)
|
|
{
|
|
const char* pFilename;
|
|
const char* pExt;
|
|
|
|
/*
|
|
* We have to isolate the filename so that we don't get excited
|
|
* about "/foo.bar/file".
|
|
*/
|
|
pFilename = FilenameOnly(pathname, fssep);
|
|
ASSERT(pFilename != nil);
|
|
pExt = strrchr(pFilename, kFilenameExtDelim);
|
|
|
|
/* also check for "/blah/foo.", which doesn't count */
|
|
if (pExt != nil && *(pExt+1) != '\0')
|
|
return pExt;
|
|
|
|
return nil;
|
|
}
|
|
#endif
|
|
|
|
|
|
/*
|
|
* Analyze a file's characteristics.
|
|
*/
|
|
void
|
|
AnalyzeFile(const A2File* pFile, RecordKind* pRecordKind,
|
|
unsigned long* pTotalLen, unsigned long* pTotalCompLen)
|
|
{
|
|
if (pFile->IsVolumeDirectory()) {
|
|
/* volume directory entry */
|
|
ASSERT(pFile->GetRsrcLength() < 0);
|
|
*pRecordKind = kRecordKindVolumeDirectory;
|
|
*pTotalLen = pFile->GetDataLength();
|
|
*pTotalCompLen = pFile->GetDataLength();
|
|
} else if (pFile->IsDirectory()) {
|
|
/* directory entry */
|
|
ASSERT(pFile->GetRsrcLength() < 0);
|
|
*pRecordKind = kRecordKindDirectory;
|
|
*pTotalLen = pFile->GetDataLength();
|
|
*pTotalCompLen = pFile->GetDataLength();
|
|
} else if (pFile->GetRsrcLength() >= 0) {
|
|
/* has resource fork */
|
|
*pRecordKind = kRecordKindForkedFile;
|
|
*pTotalLen = pFile->GetDataLength() + pFile->GetRsrcLength();
|
|
*pTotalCompLen =
|
|
pFile->GetDataSparseLength() + pFile->GetRsrcSparseLength();
|
|
} else {
|
|
/* just data fork */
|
|
*pRecordKind = kRecordKindFile;
|
|
*pTotalLen = pFile->GetDataLength();
|
|
*pTotalCompLen = pFile->GetDataSparseLength();
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Determine whether the access bits on the record make it a read-only
|
|
* file or not.
|
|
*
|
|
* Uses a simplified view of the access flags.
|
|
*/
|
|
bool
|
|
IsRecordReadOnly(int accessBits)
|
|
{
|
|
if (accessBits == 0x21L || accessBits == 0x01L)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
/* ProDOS file type names; must be entirely in upper case */
|
|
static const char gFileTypeNames[256][4] = {
|
|
"NON", "BAD", "PCD", "PTX", "TXT", "PDA", "BIN", "FNT",
|
|
"FOT", "BA3", "DA3", "WPF", "SOS", "$0D", "$0E", "DIR",
|
|
"RPD", "RPI", "AFD", "AFM", "AFR", "SCL", "PFS", "$17",
|
|
"$18", "ADB", "AWP", "ASP", "$1C", "$1D", "$1E", "$1F",
|
|
"TDM", "$21", "$22", "$23", "$24", "$25", "$26", "$27",
|
|
"$28", "$29", "8SC", "8OB", "8IC", "8LD", "P8C", "$2F",
|
|
"$30", "$31", "$32", "$33", "$34", "$35", "$36", "$37",
|
|
"$38", "$39", "$3A", "$3B", "$3C", "$3D", "$3E", "$3F",
|
|
"DIC", "OCR", "FTD", "$43", "$44", "$45", "$46", "$47",
|
|
"$48", "$49", "$4A", "$4B", "$4C", "$4D", "$4E", "$4F",
|
|
"GWP", "GSS", "GDB", "DRW", "GDP", "HMD", "EDU", "STN",
|
|
"HLP", "COM", "CFG", "ANM", "MUM", "ENT", "DVU", "FIN",
|
|
"$60", "$61", "$62", "$63", "$64", "$65", "$66", "$67",
|
|
"$68", "$69", "$6A", "BIO", "$6C", "TDR", "PRE", "HDV",
|
|
"$70", "$71", "$72", "$73", "$74", "$75", "$76", "$77",
|
|
"$78", "$79", "$7A", "$7B", "$7C", "$7D", "$7E", "$7F",
|
|
"$80", "$81", "$82", "$83", "$84", "$85", "$86", "$87",
|
|
"$88", "$89", "$8A", "$8B", "$8C", "$8D", "$8E", "$8F",
|
|
"$90", "$91", "$92", "$93", "$94", "$95", "$96", "$97",
|
|
"$98", "$99", "$9A", "$9B", "$9C", "$9D", "$9E", "$9F",
|
|
"WP ", "$A1", "$A2", "$A3", "$A4", "$A5", "$A6", "$A7",
|
|
"$A8", "$A9", "$AA", "GSB", "TDF", "BDF", "$AE", "$AF",
|
|
"SRC", "OBJ", "LIB", "S16", "RTL", "EXE", "PIF", "TIF",
|
|
"NDA", "CDA", "TOL", "DVR", "LDF", "FST", "$BE", "DOC",
|
|
"PNT", "PIC", "ANI", "PAL", "$C4", "OOG", "SCR", "CDV",
|
|
"FON", "FND", "ICN", "$CB", "$CC", "$CD", "$CE", "$CF",
|
|
"$D0", "$D1", "$D2", "$D3", "$D4", "MUS", "INS", "MDI",
|
|
"SND", "$D9", "$DA", "DBM", "$DC", "DDD", "$DE", "$DF",
|
|
"LBR", "$E1", "ATK", "$E3", "$E4", "$E5", "$E6", "$E7",
|
|
"$E8", "$E9", "$EA", "$EB", "$EC", "$ED", "R16", "PAS",
|
|
"CMD", "$F1", "$F2", "$F3", "$F4", "$F5", "$F6", "$F7",
|
|
"$F8", "OS ", "INT", "IVR", "BAS", "VAR", "REL", "SYS"
|
|
};
|
|
|
|
/*
|
|
* Return a pointer to the three-letter representation of the file type name.
|
|
*
|
|
* Note to self: code down below tests first char for '?'.
|
|
*/
|
|
/*static*/ const char*
|
|
GetFileTypeString(unsigned long fileType)
|
|
{
|
|
if (fileType < NELEM(gFileTypeNames))
|
|
return gFileTypeNames[fileType];
|
|
else
|
|
return "???";
|
|
}
|
|
|
|
/*
|
|
* Sanitize a string. The Mac likes to stick control characters into
|
|
* things, e.g. ^C and ^M.
|
|
*/
|
|
static void
|
|
MacSanitize(char* str)
|
|
{
|
|
while (*str != '\0') {
|
|
if (*str < 0x20 || *str >= 0x7f)
|
|
*str = '?';
|
|
str++;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Load the contents of a DiskFS.
|
|
*
|
|
* Recursively handle sub-volumes.
|
|
*/
|
|
int
|
|
LoadDiskFSContents(DiskFS* pDiskFS, const char* volName,
|
|
ScanOpts* pScanOpts)
|
|
{
|
|
static const char* kBlankFileName = "<blank filename>";
|
|
DiskFS::SubVolume* pSubVol = nil;
|
|
A2File* pFile;
|
|
|
|
ASSERT(pDiskFS != nil);
|
|
pFile = pDiskFS->GetNextFile(nil);
|
|
while (pFile != nil) {
|
|
char subVolName[128] = "";
|
|
char dispName[128] = "";
|
|
//CString subVolName, dispName;
|
|
RecordKind recordKind;
|
|
unsigned long totalLen, totalCompLen;
|
|
char tmpbuf[16];
|
|
|
|
AnalyzeFile(pFile, &recordKind, &totalLen, &totalCompLen);
|
|
if (recordKind == kRecordKindVolumeDirectory) {
|
|
/* skip these */
|
|
pFile = pDiskFS->GetNextFile(pFile);
|
|
continue;
|
|
}
|
|
|
|
/* prepend volName for sub-volumes; must be valid Win32 dirname */
|
|
if (volName[0] != '\0')
|
|
sprintf(subVolName, "_%s", volName);
|
|
|
|
const char* ccp = pFile->GetPathName();
|
|
ASSERT(ccp != nil);
|
|
if (strlen(ccp) == 0)
|
|
ccp = kBlankFileName;
|
|
|
|
if (subVolName[0] == '\0')
|
|
strcpy(dispName, ccp);
|
|
else {
|
|
sprintf(dispName, "%s:%s", subVolName, ccp);
|
|
//dispName = subVolName;
|
|
//dispName += ':';
|
|
//dispName += ccp;
|
|
}
|
|
ccp = dispName;
|
|
|
|
int len = strlen(ccp);
|
|
if (len <= 32) {
|
|
fprintf(pScanOpts->outfp, "%c%-32.32s ",
|
|
IsRecordReadOnly(pFile->GetAccess()) ? '*' : ' ',
|
|
ccp);
|
|
} else {
|
|
fprintf(pScanOpts->outfp, "%c..%-30.30s ",
|
|
IsRecordReadOnly(pFile->GetAccess()) ? '*' : ' ',
|
|
ccp + len - 30);
|
|
}
|
|
switch (recordKind) {
|
|
case kRecordKindUnknown:
|
|
fprintf(pScanOpts->outfp, "%s- $%04lX ",
|
|
GetFileTypeString(pFile->GetFileType()),
|
|
pFile->GetAuxType());
|
|
break;
|
|
case kRecordKindDisk:
|
|
sprintf(tmpbuf, "%ldk", totalLen / 1024);
|
|
fprintf(pScanOpts->outfp, "Disk %-6s ", tmpbuf);
|
|
break;
|
|
case kRecordKindFile:
|
|
case kRecordKindForkedFile:
|
|
case kRecordKindDirectory:
|
|
if (pDiskFS->GetDiskImg()->GetFSFormat() == DiskImg::kFormatMacHFS)
|
|
{
|
|
if (recordKind != kRecordKindDirectory &&
|
|
pFile->GetFileType() >= 0 && pFile->GetFileType() <= 0xff &&
|
|
pFile->GetAuxType() >= 0 && pFile->GetAuxType() <= 0xffff)
|
|
{
|
|
/* ProDOS type embedded in HFS */
|
|
fprintf(pScanOpts->outfp, "%s%c $%04lX ",
|
|
GetFileTypeString(pFile->GetFileType()),
|
|
recordKind == kRecordKindForkedFile ? '+' : ' ',
|
|
pFile->GetAuxType());
|
|
} else {
|
|
char typeStr[5];
|
|
char creatorStr[5];
|
|
unsigned long val;
|
|
|
|
val = pFile->GetAuxType();
|
|
creatorStr[0] = (unsigned char) (val >> 24);
|
|
creatorStr[1] = (unsigned char) (val >> 16);
|
|
creatorStr[2] = (unsigned char) (val >> 8);
|
|
creatorStr[3] = (unsigned char) val;
|
|
creatorStr[4] = '\0';
|
|
|
|
val = pFile->GetFileType();
|
|
typeStr[0] = (unsigned char) (val >> 24);
|
|
typeStr[1] = (unsigned char) (val >> 16);
|
|
typeStr[2] = (unsigned char) (val >> 8);
|
|
typeStr[3] = (unsigned char) val;
|
|
typeStr[4] = '\0';
|
|
|
|
MacSanitize(creatorStr);
|
|
MacSanitize(typeStr);
|
|
|
|
if (recordKind == kRecordKindDirectory) {
|
|
fprintf(pScanOpts->outfp, "DIR %-4s ", creatorStr);
|
|
} else {
|
|
fprintf(pScanOpts->outfp, "%-4s%c %-4s ",
|
|
typeStr,
|
|
pFile->GetRsrcLength() > 0 ? '+' : ' ',
|
|
creatorStr);
|
|
}
|
|
}
|
|
} else {
|
|
fprintf(pScanOpts->outfp, "%s%c $%04lX ",
|
|
GetFileTypeString(pFile->GetFileType()),
|
|
recordKind == kRecordKindForkedFile ? '+' : ' ',
|
|
pFile->GetAuxType());
|
|
}
|
|
break;
|
|
default:
|
|
ASSERT(0);
|
|
fprintf(pScanOpts->outfp, "ERROR ");
|
|
break;
|
|
}
|
|
|
|
char date[64];
|
|
if (pFile->GetModWhen() == 0)
|
|
FormatDate(kDateNone, date);
|
|
else
|
|
FormatDate(pFile->GetModWhen(), date);
|
|
fprintf(pScanOpts->outfp, "%-15s ", (LPCTSTR) date);
|
|
|
|
const char* fmtStr;
|
|
switch (pFile->GetFSFormat()) {
|
|
case DiskImg::kFormatDOS33:
|
|
case DiskImg::kFormatDOS32:
|
|
case DiskImg::kFormatUNIDOS:
|
|
fmtStr = "DOS ";
|
|
break;
|
|
case DiskImg::kFormatProDOS:
|
|
fmtStr = "ProDOS";
|
|
break;
|
|
case DiskImg::kFormatPascal:
|
|
fmtStr = "Pascal";
|
|
break;
|
|
case DiskImg::kFormatMacHFS:
|
|
fmtStr = "HFS ";
|
|
break;
|
|
case DiskImg::kFormatCPM:
|
|
fmtStr = "CP/M ";
|
|
break;
|
|
case DiskImg::kFormatMSDOS:
|
|
fmtStr = "MS-DOS";
|
|
break;
|
|
case DiskImg::kFormatRDOS33:
|
|
case DiskImg::kFormatRDOS32:
|
|
case DiskImg::kFormatRDOS3:
|
|
fmtStr = "RDOS ";
|
|
break;
|
|
default:
|
|
fmtStr = "??? ";
|
|
break;
|
|
}
|
|
if (pFile->GetQuality() == A2File::kQualityDamaged)
|
|
fmtStr = "BROKEN";
|
|
|
|
fprintf(pScanOpts->outfp, "%s ", fmtStr);
|
|
|
|
|
|
#if 0
|
|
/* compute the percent size */
|
|
if ((!totalLen && totalCompLen) || (totalLen && !totalCompLen))
|
|
fprintf(pScanOpts->outfp, "--- "); /* weird */
|
|
else if (totalLen < totalCompLen)
|
|
fprintf(pScanOpts->outfp, ">100%% "); /* compression failed? */
|
|
else {
|
|
sprintf(tmpbuf, "%02d%%", ComputePercent(totalCompLen, totalLen));
|
|
fprintf(pScanOpts->outfp, "%4s ", tmpbuf);
|
|
}
|
|
#endif
|
|
|
|
if (!totalLen && totalCompLen)
|
|
fprintf(pScanOpts->outfp, " ????"); /* weird */
|
|
else
|
|
fprintf(pScanOpts->outfp, "%8ld", totalLen);
|
|
|
|
fprintf(pScanOpts->outfp, "\n");
|
|
|
|
pFile = pDiskFS->GetNextFile(pFile);
|
|
}
|
|
|
|
/*
|
|
* Load all sub-volumes.
|
|
*/
|
|
pSubVol = pDiskFS->GetNextSubVolume(nil);
|
|
while (pSubVol != nil) {
|
|
const char* subVolName;
|
|
int ret;
|
|
|
|
subVolName = pSubVol->GetDiskFS()->GetVolumeName();
|
|
if (subVolName == nil)
|
|
subVolName = "+++"; // could probably do better than this
|
|
|
|
ret = LoadDiskFSContents(pSubVol->GetDiskFS(), subVolName, pScanOpts);
|
|
if (ret != 0)
|
|
return ret;
|
|
pSubVol = pDiskFS->GetNextSubVolume(pSubVol);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Open a disk image and dump the contents.
|
|
*
|
|
* Returns 0 on success, nonzero on failure.
|
|
*/
|
|
int
|
|
ScanDiskImage(const char* pathName, ScanOpts* pScanOpts)
|
|
{
|
|
ASSERT(pathName != nil);
|
|
ASSERT(pScanOpts != nil);
|
|
ASSERT(pScanOpts->outfp != nil);
|
|
|
|
DIError dierr;
|
|
char errMsg[256] = "";
|
|
DiskImg diskImg;
|
|
DiskFS* pDiskFS = nil;
|
|
|
|
dierr = diskImg.OpenImage(pathName, '/', true);
|
|
if (dierr != kDIErrNone) {
|
|
sprintf(errMsg, "Unable to open '%s': %s", pathName,
|
|
DIStrError(dierr));
|
|
goto bail;
|
|
}
|
|
|
|
dierr = diskImg.AnalyzeImage();
|
|
if (dierr != kDIErrNone) {
|
|
sprintf(errMsg, "Analysis of '%s' failed: %s", pathName,
|
|
DIStrError(dierr));
|
|
goto bail;
|
|
}
|
|
|
|
if (diskImg.GetFSFormat() == DiskImg::kFormatUnknown ||
|
|
diskImg.GetSectorOrder() == DiskImg::kSectorOrderUnknown)
|
|
{
|
|
sprintf(errMsg, "Unable to identify filesystem on '%s'", pathName);
|
|
goto bail;
|
|
}
|
|
|
|
/* create an appropriate DiskFS object */
|
|
pDiskFS = diskImg.OpenAppropriateDiskFS();
|
|
if (pDiskFS == nil) {
|
|
/* unknown FS should've been caught above! */
|
|
ASSERT(false);
|
|
sprintf(errMsg, "Format of '%s' not recognized.", pathName);
|
|
goto bail;
|
|
}
|
|
|
|
pDiskFS->SetScanForSubVolumes(DiskFS::kScanSubEnabled);
|
|
|
|
/* object created; prep it */
|
|
dierr = pDiskFS->Initialize(&diskImg, DiskFS::kInitFull);
|
|
if (dierr != kDIErrNone) {
|
|
sprintf(errMsg, "Error reading list of files from disk: %s",
|
|
DIStrError(dierr));
|
|
goto bail;
|
|
}
|
|
|
|
fprintf(pScanOpts->outfp, "File: %s\n", pathName);
|
|
|
|
int kbytes;
|
|
if (pDiskFS->GetDiskImg()->GetHasBlocks())
|
|
kbytes = pDiskFS->GetDiskImg()->GetNumBlocks() / 2;
|
|
else if (pDiskFS->GetDiskImg()->GetHasSectors())
|
|
kbytes = (pDiskFS->GetDiskImg()->GetNumTracks() *
|
|
pDiskFS->GetDiskImg()->GetNumSectPerTrack()) / 4;
|
|
else
|
|
kbytes = 0;
|
|
fprintf(pScanOpts->outfp, "Disk: %s%s (%dKB)\n", pDiskFS->GetVolumeID(),
|
|
pDiskFS->GetFSDamaged() ? " [*]" : "", kbytes);
|
|
|
|
fprintf(pScanOpts->outfp,
|
|
" Name Type Auxtyp Modified"
|
|
" Format Length\n");
|
|
fprintf(pScanOpts->outfp,
|
|
"------------------------------------------------------"
|
|
"------------------------\n");
|
|
if (LoadDiskFSContents(pDiskFS, "", pScanOpts) != 0) {
|
|
sprintf(errMsg, "Failed while loading contents of '%s'.", pathName);
|
|
goto bail;
|
|
}
|
|
fprintf(pScanOpts->outfp,
|
|
"------------------------------------------------------"
|
|
"------------------------\n\n");
|
|
|
|
gStats.goodDiskImages++;
|
|
|
|
bail:
|
|
delete pDiskFS;
|
|
|
|
if (errMsg[0] != '\0') {
|
|
fprintf(pScanOpts->outfp, "Unable to process '%s'\n", pathName);
|
|
fprintf(pScanOpts->outfp, " %s\n\n", (LPCTSTR) errMsg);
|
|
return -1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Check a file's status.
|
|
*
|
|
* [ Someday we may want to modify this to handle symbolic links. ]
|
|
*/
|
|
int
|
|
CheckFileStatus(const char* pathname, struct stat* psb, bool* pExists,
|
|
bool* pIsReadable, bool* pIsDir)
|
|
{
|
|
int result = 0;
|
|
int cc;
|
|
|
|
assert(pathname != nil);
|
|
assert(psb != nil);
|
|
assert(pExists != nil);
|
|
assert(pIsReadable != nil);
|
|
assert(pIsDir != nil);
|
|
|
|
*pExists = true;
|
|
*pIsReadable = true;
|
|
*pIsDir = false;
|
|
|
|
cc = stat(pathname, psb);
|
|
if (cc) {
|
|
if (errno == ENOENT)
|
|
*pExists = false;
|
|
else
|
|
result = -1; // stat failed
|
|
goto bail;
|
|
}
|
|
|
|
if (S_ISDIR(psb->st_mode))
|
|
*pIsDir = true;
|
|
|
|
/*
|
|
* Test if we can read this file. How do we do that? The easy but slow
|
|
* way is to call access(2), the harder way is to figure out
|
|
* what user/group we are and compare the appropriate file mode.
|
|
*/
|
|
if (access(pathname, R_OK) < 0)
|
|
*pIsReadable = false;
|
|
|
|
bail:
|
|
return result;
|
|
}
|
|
|
|
|
|
/* forward decl */
|
|
int ProcessFile(const char* pathname, ScanOpts* pScanOpts);
|
|
|
|
/*
|
|
* UNIX-style recursive directory descent. Scan the contents of a directory.
|
|
* If a subdirectory is found, follow it; otherwise, call ProcessFile to
|
|
* handle the file.
|
|
*/
|
|
int
|
|
ProcessDirectory(const char* dirName, ScanOpts* pScanOpts)
|
|
{
|
|
StringArray strArray;
|
|
int result = -1;
|
|
DIR* dirp = nil;
|
|
DIR_TYPE* entry;
|
|
char nbuf[MAX_PATH_LEN]; /* malloc might be better; this soaks stack */
|
|
char fssep;
|
|
int len;
|
|
|
|
assert(pScanOpts != nil);
|
|
assert(dirName != nil);
|
|
|
|
#ifdef _DEBUG
|
|
fprintf(gLog, "+++ Processing directory '%s'\n", dirName);
|
|
#endif
|
|
|
|
dirp = opendir(dirName);
|
|
if (dirp == nil) {
|
|
//err = errno ? errno : -1;
|
|
goto bail;
|
|
}
|
|
|
|
fssep = '/';
|
|
|
|
/* could use readdir_r, but we don't care about reentrancy here */
|
|
while ((entry = readdir(dirp)) != nil) {
|
|
/* skip the dotsies */
|
|
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
|
continue;
|
|
|
|
len = strlen(dirName);
|
|
if (len + DIR_NAME_LEN(entry) +2 > MAX_PATH_LEN) {
|
|
fprintf(stderr, "ERROR: Filename exceeds %d bytes: %s%c%s\n",
|
|
MAX_PATH_LEN, dirName, fssep, entry->d_name);
|
|
goto bail;
|
|
}
|
|
|
|
/* form the new name, inserting an fssep if needed */
|
|
strcpy(nbuf, dirName);
|
|
if (dirName[len-1] != fssep)
|
|
nbuf[len++] = fssep;
|
|
strcpy(nbuf+len, entry->d_name);
|
|
|
|
strArray.Add(nbuf);
|
|
}
|
|
|
|
/* sort the list, then process the files */
|
|
strArray.Sort(StringArray::CmpAscendingAlpha);
|
|
for (int i = 0; i < strArray.GetCount(); i++)
|
|
(void) ProcessFile(strArray.GetEntry(i), pScanOpts);
|
|
|
|
result = 0;
|
|
|
|
bail:
|
|
if (dirp != nil)
|
|
(void)closedir(dirp);
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
* Process a file.
|
|
*
|
|
* Returns with an error if the file doesn't exist or isn't readable.
|
|
*/
|
|
int
|
|
ProcessFile(const char* pathname, ScanOpts* pScanOpts)
|
|
{
|
|
int result = -1;
|
|
bool exists, isDir, isReadable;
|
|
struct stat sb;
|
|
|
|
assert(pathname != nil);
|
|
assert(pScanOpts != nil);
|
|
|
|
#ifdef _DEBUG
|
|
fprintf(gLog, "+++ Processing file or dir '%s'\n", pathname);
|
|
#endif
|
|
|
|
if (CheckFileStatus(pathname, &sb, &exists, &isReadable, &isDir) != 0) {
|
|
fprintf(stderr, "ERROR: unexpected error while examining '%s'\n",
|
|
pathname);
|
|
goto bail;
|
|
}
|
|
|
|
if (!exists) {
|
|
fprintf(stderr, "ERROR: couldn't find '%s'\n", pathname);
|
|
goto bail;
|
|
}
|
|
if (!isReadable) {
|
|
fprintf(stderr, "ERROR: file '%s' isn't readable\n", pathname);
|
|
goto bail;
|
|
}
|
|
|
|
if (isDir) {
|
|
result = ProcessDirectory(pathname, pScanOpts);
|
|
gStats.numDirectories++;
|
|
} else {
|
|
result = ScanDiskImage(pathname, pScanOpts);
|
|
gStats.numFiles++;
|
|
}
|
|
|
|
bail:
|
|
return result;
|
|
}
|
|
|
|
|
|
/*
|
|
* Handle a debug message from the DiskImg library.
|
|
*/
|
|
/*static*/ void
|
|
MsgHandler(const char* file, int line, const char* msg)
|
|
{
|
|
ASSERT(file != nil);
|
|
ASSERT(msg != nil);
|
|
|
|
#ifdef _DEBUG
|
|
fprintf(gLog, "%05u %s", gPid, msg);
|
|
#endif
|
|
}
|
|
/*
|
|
* Handle a global error message from the NufxLib library by shoving it
|
|
* through the DiskImgLib message function.
|
|
*/
|
|
NuResult
|
|
NufxErrorMsgHandler(NuArchive* /*pArchive*/, void* vErrorMessage)
|
|
{
|
|
const NuErrorMessage* pErrorMessage = (const NuErrorMessage*) vErrorMessage;
|
|
|
|
if (pErrorMessage->isDebug) {
|
|
Global::PrintDebugMsg(pErrorMessage->file, pErrorMessage->line,
|
|
"<nufxlib> [D] %s\n", pErrorMessage->message);
|
|
} else {
|
|
Global::PrintDebugMsg(pErrorMessage->file, pErrorMessage->line,
|
|
"<nufxlib> %s\n", pErrorMessage->message);
|
|
}
|
|
|
|
return kNuOK;
|
|
}
|
|
|
|
/*
|
|
* Process every argument.
|
|
*/
|
|
int
|
|
main(int argc, char** argv)
|
|
{
|
|
ScanOpts scanOpts;
|
|
scanOpts.outfp = stdout;
|
|
|
|
#ifdef _DEBUG
|
|
const char* kLogFile = "mdc-log.txt";
|
|
gLog = fopen(kLogFile, "w");
|
|
if (gLog == nil) {
|
|
fprintf(stderr, "ERROR: unable to open log file\n");
|
|
exit(1);
|
|
}
|
|
#endif
|
|
|
|
long major, minor, bug;
|
|
Global::GetVersion(&major, &minor, &bug);
|
|
|
|
printf("MDC for Linux v2.2.0 (DiskImg library v%ld.%ld.%ld)\n",
|
|
major, minor, bug);
|
|
printf("Copyright (C) 2006 by faddenSoft, LLC. All rights reserved.\n");
|
|
printf("MDC is part of CiderPress, available from http://www.faddensoft.com/.\n");
|
|
NuGetVersion(&major, &minor, &bug, nil, nil);
|
|
printf("Linked against NufxLib v%ld.%ld.%ld and zlib version %s.\n",
|
|
major, minor, bug, zlibVersion());
|
|
|
|
if (argc == 1) {
|
|
fprintf(stderr, "\nUsage: mdc file ...\n");
|
|
goto done;
|
|
}
|
|
|
|
#ifdef _DEBUG
|
|
printf("Log file is '%s'\n", kLogFile);
|
|
#endif
|
|
printf("\n");
|
|
|
|
Global::SetDebugMsgHandler(MsgHandler);
|
|
Global::AppInit();
|
|
|
|
NuSetGlobalErrorMessageHandler(NufxErrorMsgHandler);
|
|
|
|
time_t start = time(NULL);
|
|
printf("Run started at %.24s\n\n", ctime(&start));
|
|
|
|
while (--argc) {
|
|
ProcessFile(*++argv, &scanOpts);
|
|
}
|
|
|
|
printf("Scan completed in %ld seconds:\n", time(NULL) - start);
|
|
printf(" Directories : %ld\n", gStats.numDirectories);
|
|
printf(" Files : %ld (%ld good disk images)\n", gStats.numFiles,
|
|
gStats.goodDiskImages);
|
|
|
|
Global::AppCleanup();
|
|
|
|
done:
|
|
#ifdef _DEBUG
|
|
fclose(gLog);
|
|
#endif
|
|
|
|
return 0;
|
|
}
|
|
|