mirror of
https://github.com/fadden/nulib2.git
synced 2024-11-18 23:05:04 +00:00
1295 lines
36 KiB
C
1295 lines
36 KiB
C
/*
|
|
* NuFX archive manipulation library
|
|
* Copyright (C) 2000 by Andy McFadden, All Rights Reserved.
|
|
* This is free software; you can redistribute it and/or modify it under the
|
|
* terms of the GNU Library General Public License, see the file COPYING.LIB.
|
|
*
|
|
* Operations on output (i.e. non-archive) files, largely system-specific.
|
|
* Portions taken from NuLib, including some code that Devin Reade worked on.
|
|
*
|
|
* It could be argued that "create file" should be a callback function,
|
|
* since it is so heavily system-specific, and most of the other
|
|
* system dependencies are handled by the application rather than the
|
|
* NuFX library. It would also provide a cleaner solution for renaming
|
|
* extracted files. However, the goal of the library is to do the work
|
|
* for the application, not the other way around; and while it might be
|
|
* nice to offload all direct file handling on the application, it
|
|
* complicates rather than simplifies the interface.
|
|
*/
|
|
#include "NufxLibPriv.h"
|
|
|
|
/*
|
|
* For systems (e.g. Visual C++ 6.0) that don't have these standard values.
|
|
*/
|
|
#ifndef S_IRUSR
|
|
# define S_IRUSR 0400
|
|
# define S_IWUSR 0200
|
|
# define S_IXUSR 0100
|
|
# define S_IRWXU (S_IRUSR|S_IWUSR|S_IXUSR)
|
|
# define S_IRGRP (S_IRUSR >> 3)
|
|
# define S_IWGRP (S_IWUSR >> 3)
|
|
# define S_IXGRP (S_IXUSR >> 3)
|
|
# define S_IRWXG (S_IRWXU >> 3)
|
|
# define S_IROTH (S_IRGRP >> 3)
|
|
# define S_IWOTH (S_IWGRP >> 3)
|
|
# define S_IXOTH (S_IXGRP >> 3)
|
|
# define S_IRWXO (S_IRWXG >> 3)
|
|
#endif
|
|
#ifndef S_ISREG
|
|
# define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
|
|
# define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
|
|
#endif
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* DateTime conversions
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Dates and times in a NuFX archive are always considered to be in the
|
|
* local time zone. The use of GMT time stamps would have been more
|
|
* appropriate for an archive, but local time works well enough.
|
|
*
|
|
* Regarding Y2K on the Apple II:
|
|
*
|
|
* Dave says P8 drivers should return year values in the range 0..99, where
|
|
* 40..99 = 1940..1999, and 0..39 = 2000..2039. Year values 100..127 should
|
|
* never be used. For ProDOS 8, the year 2000 is "00".
|
|
*
|
|
* The IIgs ReadTimeHex call uses "year minus 1900". For GS/OS, the year
|
|
* 2000 is "100".
|
|
*
|
|
* The NuFX file type note says the archive format should work like
|
|
* The IIgs ReadTimeHex call, which uses "year minus 1900" as its
|
|
* format. GS/ShrinkIt v1.1 uses the IIgs date calls, and so stores the
|
|
* year 2000 as "100". P8 ShrinkIt v3.4 uses the P8 mapping, and stores
|
|
* it as "0". Neither really quite understands what the other is doing.
|
|
*
|
|
* For our purposes, we will follow the NuFX standard and emit "100"
|
|
* for the year 2000, but will accept and understand "0" as well.
|
|
*/
|
|
|
|
|
|
#if defined(UNIX_LIKE) || defined(WINDOWS_LIKE)
|
|
/*
|
|
* Convert from local time in a NuDateTime struct to GMT seconds since 1970.
|
|
*
|
|
* If the conversion is invalid, "*pWhen" is set to zero.
|
|
*/
|
|
static void
|
|
Nu_DateTimeToGMTSeconds(const NuDateTime* pDateTime, time_t* pWhen)
|
|
{
|
|
struct tm tmbuf;
|
|
time_t when;
|
|
|
|
Assert(pDateTime != nil);
|
|
Assert(pWhen != nil);
|
|
|
|
tmbuf.tm_sec = pDateTime->second;
|
|
tmbuf.tm_min = pDateTime->minute;
|
|
tmbuf.tm_hour = pDateTime->hour;
|
|
tmbuf.tm_mday = pDateTime->day +1;
|
|
tmbuf.tm_mon = pDateTime->month;
|
|
tmbuf.tm_year = pDateTime->year;
|
|
if (pDateTime->year < 40)
|
|
tmbuf.tm_year += 100; /* P8 uses 0-39 for 2000-2039 */
|
|
tmbuf.tm_wday = 0;
|
|
tmbuf.tm_yday = 0;
|
|
tmbuf.tm_isdst = -1; /* let it figure DST and time zone */
|
|
|
|
#if defined(HAVE_MKTIME)
|
|
when = mktime(&tmbuf);
|
|
#elif defined(HAVE_TIMELOCAL)
|
|
when = timelocal(&tmbuf);
|
|
#else
|
|
# error "need time converter"
|
|
#endif
|
|
|
|
if (when == (time_t) -1)
|
|
*pWhen = 0;
|
|
else
|
|
*pWhen = when;
|
|
}
|
|
|
|
/*
|
|
* Convert from GMT seconds since 1970 to local time in a NuDateTime struct.
|
|
*/
|
|
static void
|
|
Nu_GMTSecondsToDateTime(const time_t* pWhen, NuDateTime *pDateTime)
|
|
{
|
|
struct tm* ptm;
|
|
|
|
Assert(pWhen != nil);
|
|
Assert(pDateTime != nil);
|
|
|
|
#if defined(HAVE_LOCALTIME_R) && defined(USE_REENTRANT_CALLS)
|
|
struct tm res;
|
|
ptm = localtime_r(pWhen, &res);
|
|
#else
|
|
/* NOTE: not thread-safe */
|
|
ptm = localtime(pWhen);
|
|
#endif
|
|
pDateTime->second = ptm->tm_sec;
|
|
pDateTime->minute = ptm->tm_min;
|
|
pDateTime->hour = ptm->tm_hour;
|
|
pDateTime->day = ptm->tm_mday -1;
|
|
pDateTime->month = ptm->tm_mon;
|
|
pDateTime->year = ptm->tm_year;
|
|
pDateTime->extra = 0;
|
|
pDateTime->weekDay = ptm->tm_wday +1;
|
|
}
|
|
#endif
|
|
|
|
|
|
/*
|
|
* Fill in the current time.
|
|
*/
|
|
void
|
|
Nu_SetCurrentDateTime(NuDateTime* pDateTime)
|
|
{
|
|
Assert(pDateTime != nil);
|
|
|
|
#if defined(UNIX_LIKE) || defined(WINDOWS_LIKE)
|
|
{
|
|
time_t now = time(nil);
|
|
Nu_GMTSecondsToDateTime(&now, pDateTime);
|
|
}
|
|
#else
|
|
#error "Port this"
|
|
#endif
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns "true" if "pWhen1" is older than "pWhen2". Returns false if
|
|
* "pWhen1" is the same age or newer than "pWhen2".
|
|
*
|
|
* On systems with mktime, it would be straightforward to convert the dates
|
|
* to time in seconds, and compare them that way. However, I don't want
|
|
* to rely on that function too heavily, so we just compare fields.
|
|
*/
|
|
Boolean
|
|
Nu_IsOlder(const NuDateTime* pWhen1, const NuDateTime* pWhen2)
|
|
{
|
|
long result, year1, year2;
|
|
|
|
/* adjust for P8 ShrinkIt Y2K problem */
|
|
year1 = pWhen1->year;
|
|
if (year1 < 40)
|
|
year1 += 100;
|
|
year2 = pWhen2->year;
|
|
if (year2 < 40)
|
|
year2 += 100;
|
|
|
|
result = year1 - year2;
|
|
if (!result)
|
|
result = pWhen1->month - pWhen2->month;
|
|
if (!result)
|
|
result = pWhen1->day - pWhen2->day;
|
|
if (!result)
|
|
result = pWhen1->hour - pWhen2->hour;
|
|
if (!result)
|
|
result = pWhen1->minute - pWhen2->minute;
|
|
if (!result)
|
|
result = pWhen1->second - pWhen2->second;
|
|
|
|
if (result < 0)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* Get/set file info
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*
|
|
* System-independent (mostly) file info struct.
|
|
*/
|
|
typedef struct NuFileInfo {
|
|
Boolean isValid; /* init to "false", set "true" after we get data */
|
|
|
|
Boolean isRegularFile; /* is this a regular file? */
|
|
Boolean isDirectory; /* is this a directory? */
|
|
|
|
ulong dataEof;
|
|
ulong rsrcEof;
|
|
|
|
ulong fileType;
|
|
ulong auxType;
|
|
NuDateTime modWhen;
|
|
mode_t unixMode; /* UNIX-style permissions */
|
|
} NuFileInfo;
|
|
|
|
#define kDefaultFileType 0 /* "NON" */
|
|
#define kDefaultAuxType 0 /* $0000 */
|
|
|
|
|
|
/*
|
|
* Determine whether the record has both data and resource forks.
|
|
*/
|
|
static Boolean
|
|
Nu_IsForkedFile(NuArchive* pArchive, const NuRecord* pRecord)
|
|
{
|
|
const NuThread* pThread;
|
|
NuThreadID threadID;
|
|
Boolean gotData, gotRsrc;
|
|
int i;
|
|
|
|
gotData = gotRsrc = false;
|
|
|
|
for (i = 0; i < (int)pRecord->recTotalThreads; i++) {
|
|
pThread = Nu_GetThread(pRecord, i);
|
|
Assert(pThread != nil);
|
|
|
|
threadID = NuMakeThreadID(pThread->thThreadClass,pThread->thThreadKind);
|
|
if (threadID == kNuThreadIDDataFork)
|
|
gotData = true;
|
|
else if (threadID == kNuThreadIDRsrcFork)
|
|
gotRsrc = true;
|
|
}
|
|
|
|
if (gotData && gotRsrc)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
|
|
/*
|
|
* Get the file info into a NuFileInfo struct. Fields which are
|
|
* inappropriate for the current system are set to default values.
|
|
*/
|
|
static NuError
|
|
Nu_GetFileInfo(NuArchive* pArchive, const char* pathname,
|
|
NuFileInfo* pFileInfo)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
Assert(pArchive != nil);
|
|
Assert(pathname != nil);
|
|
Assert(pFileInfo != nil);
|
|
|
|
pFileInfo->isValid = false;
|
|
|
|
#if defined(UNIX_LIKE) || defined(WINDOWS_LIKE)
|
|
{
|
|
struct stat sbuf;
|
|
int cc;
|
|
|
|
cc = stat(pathname, &sbuf);
|
|
if (cc) {
|
|
if (errno == ENOENT)
|
|
err = kNuErrFileNotFound;
|
|
else
|
|
err = kNuErrFileStat;
|
|
goto bail;
|
|
}
|
|
|
|
pFileInfo->isRegularFile = false;
|
|
if (S_ISREG(sbuf.st_mode))
|
|
pFileInfo->isRegularFile = true;
|
|
pFileInfo->isDirectory = false;
|
|
if (S_ISDIR(sbuf.st_mode))
|
|
pFileInfo->isDirectory = true;
|
|
|
|
/* BUG: should check for 32-bit overflow from 64-bit off_t */
|
|
pFileInfo->dataEof = sbuf.st_size;
|
|
pFileInfo->rsrcEof = 0;
|
|
pFileInfo->fileType = kDefaultFileType;
|
|
pFileInfo->auxType = kDefaultAuxType;
|
|
Nu_GMTSecondsToDateTime(&sbuf.st_mtime, &pFileInfo->modWhen);
|
|
pFileInfo->unixMode = sbuf.st_mode;
|
|
pFileInfo->isValid = true;
|
|
}
|
|
#else
|
|
#error "Port this"
|
|
#endif
|
|
|
|
bail:
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* Determine whether a specific fork in the file exists.
|
|
*
|
|
* On systems that don't support forked files, the "checkRsrcFork" argument
|
|
* is ignored. If forked files are supported, and we are extracting a
|
|
* file with data and resource forks, we only claim it exists if it has
|
|
* nonzero length.
|
|
*/
|
|
static NuError
|
|
Nu_FileForkExists(NuArchive* pArchive, const char* pathname,
|
|
Boolean isForkedFile, Boolean checkRsrcFork, Boolean* pExists,
|
|
NuFileInfo* pFileInfo)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
|
|
Assert(pArchive != nil);
|
|
Assert(pathname != nil);
|
|
Assert(checkRsrcFork == true || checkRsrcFork == false);
|
|
Assert(pExists != nil);
|
|
Assert(pFileInfo != nil);
|
|
|
|
#if defined(UNIX_LIKE) || defined(WINDOWS_LIKE)
|
|
/*
|
|
* We ignore "isForkedFile" and "checkRsrcFork". The file must not
|
|
* exist at all.
|
|
*/
|
|
Assert(pArchive->lastFileCreated == nil);
|
|
|
|
*pExists = true;
|
|
err = Nu_GetFileInfo(pArchive, pathname, pFileInfo);
|
|
if (err == kNuErrFileNotFound) {
|
|
err = kNuErrNone;
|
|
*pExists = false;
|
|
}
|
|
|
|
#elif defined(__ORCAC__)
|
|
/*
|
|
* If the file doesn't exist, great. If it does, and "lastFileCreated"
|
|
* matches up with this one, then we know that it exists because we
|
|
* created it.
|
|
*
|
|
* This is great unless the record has two data forks or something
|
|
* equally dopey, so we check to be sure that the fork we want to
|
|
* put the data into is currently empty.
|
|
*
|
|
* It is possible, though asinine, for a Mac OS or GS/OS extraction
|
|
* program to put the data and resource forks of a record into
|
|
* separate files, so we can't just assume that because we wrote
|
|
* the data fork to file A it is okay for file B to exist. That's
|
|
* why we compare the pathname instead of just remembering that
|
|
* we already created a file for this record.
|
|
*/
|
|
#error "Finish me"
|
|
|
|
#else
|
|
#error "Port this"
|
|
#endif
|
|
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* Set the dates on a file according to what's in the record.
|
|
*/
|
|
static NuError
|
|
Nu_SetFileDates(NuArchive* pArchive, const NuRecord* pRecord,
|
|
const char* pathname)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
|
|
Assert(pArchive != nil);
|
|
Assert(pRecord != nil);
|
|
Assert(pathname != nil);
|
|
|
|
#if defined(UNIX_LIKE) || defined(WINDOWS_LIKE)
|
|
{
|
|
struct utimbuf utbuf;
|
|
|
|
/* ignore create time, and set access time equal to mod time */
|
|
Nu_DateTimeToGMTSeconds(&pRecord->recModWhen, &utbuf.modtime);
|
|
utbuf.actime = utbuf.modtime;
|
|
|
|
/* only do it if the NuDateTime was valid */
|
|
if (utbuf.modtime) {
|
|
if (utime(pathname, &utbuf) < 0) {
|
|
Nu_ReportError(NU_BLOB, errno,
|
|
"Unable to set time stamp on '%s'", pathname);
|
|
err = kNuErrFileSetDate;
|
|
goto bail;
|
|
}
|
|
}
|
|
}
|
|
|
|
#else
|
|
#error "Port this"
|
|
#endif
|
|
|
|
bail:
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns "true" if the record is locked (in the ProDOS sense).
|
|
*
|
|
* Bits 31-8 reserved, must be zero
|
|
* Bit 7 (D) 1 = destroy enabled
|
|
* Bit 6 (R) 1 = rename enabled
|
|
* Bit 5 (B) 1 = file needs to be backed up
|
|
* Bits 4-3 reserved, must be zero
|
|
* Bit 2 (I) 1 = file is invisible
|
|
* Bit 1 (W) 1 = write enabled
|
|
* Bit 0 (R) 1 = read enabled
|
|
*
|
|
* A "locked" file would be 00?00001, "unlocked" 11?00011, with many
|
|
* possible variations. For our purposes, we treat all files as unlocked
|
|
* unless they match the classic "locked" bit pattern.
|
|
*/
|
|
static Boolean
|
|
Nu_IsRecordLocked(const NuRecord* pRecord)
|
|
{
|
|
if (pRecord->recAccess == 0x21L || pRecord->recAccess == 0x01L)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Set the file access permissions based on what's in the record.
|
|
*
|
|
* This assumes that the file is currently writable, so we only need
|
|
* to do something if the original file was "locked".
|
|
*/
|
|
static NuError
|
|
Nu_SetFileAccess(NuArchive* pArchive, const NuRecord* pRecord,
|
|
const char* pathname)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
|
|
Assert(pArchive != nil);
|
|
Assert(pRecord != nil);
|
|
Assert(pathname != nil);
|
|
|
|
#if defined(UNIX_LIKE) || defined(WINDOWS_LIKE)
|
|
/* only need to do something if the file was "locked" */
|
|
if (Nu_IsRecordLocked(pRecord)) {
|
|
/* set it to 444 */
|
|
if (chmod(pathname, S_IRUSR | S_IRGRP | S_IROTH) < 0) {
|
|
Nu_ReportError(NU_BLOB, errno,
|
|
"unable to set access for '%s'", pathname);
|
|
err = kNuErrFileSetAccess;
|
|
goto bail;
|
|
}
|
|
}
|
|
|
|
#else
|
|
#error "Port this"
|
|
#endif
|
|
|
|
bail:
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* Create/open an output file
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Prepare an existing file for writing.
|
|
*
|
|
* Generally this just involves ensuring that the file is writable. If
|
|
* this is a convenient place to truncate it, we should do that too.
|
|
*/
|
|
static NuError
|
|
Nu_PrepareForWriting(NuArchive* pArchive, const char* pathname,
|
|
Boolean prepRsrc, NuFileInfo* pFileInfo)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
|
|
Assert(pArchive != nil);
|
|
Assert(pathname != nil);
|
|
Assert(prepRsrc == true || prepRsrc == false);
|
|
Assert(pFileInfo != nil);
|
|
|
|
Assert(pFileInfo->isValid == true);
|
|
|
|
/* don't go playing with directories, pipes, etc */
|
|
if (pFileInfo->isRegularFile != true)
|
|
return kNuErrNotRegularFile;
|
|
|
|
#if defined(UNIX_LIKE) || defined(WINDOWS_LIKE)
|
|
if (!(pFileInfo->unixMode & S_IWUSR)) {
|
|
/* make it writable by owner, plus whatever it was before */
|
|
if (chmod(pathname, S_IWUSR | pFileInfo->unixMode) < 0) {
|
|
Nu_ReportError(NU_BLOB, errno,
|
|
"unable to set access for '%s'", pathname);
|
|
err = kNuErrFileSetAccess;
|
|
goto bail;
|
|
}
|
|
}
|
|
|
|
return kNuErrNone;
|
|
|
|
#else
|
|
#error "Port this"
|
|
#endif
|
|
|
|
bail:
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* Invoke the system-dependent directory creation function.
|
|
*/
|
|
static NuError
|
|
Nu_Mkdir(NuArchive* pArchive, const char* dir)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
|
|
Assert(pArchive != nil);
|
|
Assert(dir != nil);
|
|
|
|
#if defined(UNIX_LIKE)
|
|
if (mkdir(dir, S_IRWXU | S_IRGRP|S_IXGRP | S_IROTH|S_IXOTH) < 0) {
|
|
err = errno ? errno : kNuErrDirCreate;
|
|
Nu_ReportError(NU_BLOB, err, "Unable to create dir '%s'", dir);
|
|
goto bail;
|
|
}
|
|
|
|
#elif defined(WINDOWS_LIKE)
|
|
if (mkdir(dir) < 0) {
|
|
err = errno ? errno : kNuErrDirCreate;
|
|
Nu_ReportError(NU_BLOB, err, "Unable to create dir '%s'", dir);
|
|
goto bail;
|
|
}
|
|
|
|
#else
|
|
#error "Port this"
|
|
#endif
|
|
|
|
bail:
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* Create a single subdirectory if it doesn't exist. If the next-highest
|
|
* subdirectory level doesn't exist either, cut down the pathname and
|
|
* recurse.
|
|
*/
|
|
static NuError
|
|
Nu_CreateSubdirIFN(NuArchive* pArchive, const char* pathStart,
|
|
const char* pathEnd, char fssep)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
NuFileInfo fileInfo;
|
|
char* tmpBuf = nil;
|
|
|
|
Assert(pArchive != nil);
|
|
Assert(pathStart != nil);
|
|
Assert(pathEnd != nil);
|
|
Assert(fssep != '\0');
|
|
|
|
/* pathStart might have whole path, but we only want up to "pathEnd" */
|
|
tmpBuf = strdup(pathStart);
|
|
tmpBuf[pathEnd - pathStart +1] = '\0';
|
|
|
|
err = Nu_GetFileInfo(pArchive, tmpBuf, &fileInfo);
|
|
if (err == kNuErrFileNotFound) {
|
|
/* dir doesn't exist; move up a level and check parent */
|
|
pathEnd = strrchr(tmpBuf, fssep);
|
|
if (pathEnd != nil) {
|
|
pathEnd--;
|
|
Assert(pathEnd >= tmpBuf);
|
|
err = Nu_CreateSubdirIFN(pArchive, tmpBuf, pathEnd, fssep);
|
|
BailError(err);
|
|
}
|
|
|
|
/* parent is taken care of; create this one */
|
|
err = Nu_Mkdir(pArchive, tmpBuf);
|
|
BailError(err);
|
|
} else if (err != kNuErrNone) {
|
|
goto bail;
|
|
} else {
|
|
/* file does exist, make sure it's a directory */
|
|
Assert(fileInfo.isValid == true);
|
|
if (!fileInfo.isDirectory) {
|
|
err = kNuErrNotDir;
|
|
Nu_ReportError(NU_BLOB, err, "Unable to create path '%s'", tmpBuf);
|
|
goto bail;
|
|
}
|
|
}
|
|
|
|
bail:
|
|
Nu_Free(pArchive, tmpBuf);
|
|
return err;
|
|
}
|
|
|
|
/*
|
|
* Create subdirectories, if needed. The paths leading up to the filename
|
|
* in "pathname" will be created.
|
|
*
|
|
* If "pathname" is just a filename, or the set of directories matches
|
|
* the last directory we created, we don't do anything.
|
|
*/
|
|
static NuError
|
|
Nu_CreatePathIFN(NuArchive* pArchive, const char* pathname, char fssep)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
const char* pathStart;
|
|
const char* pathEnd;
|
|
|
|
Assert(pArchive != nil);
|
|
Assert(pathname != nil);
|
|
Assert(fssep != '\0');
|
|
|
|
pathStart = pathname;
|
|
if (pathname[0] == fssep)
|
|
pathStart++;
|
|
|
|
/* NOTE: not expecting names like "foo/bar/ack/", with terminating fssep */
|
|
pathEnd = strrchr(pathStart, fssep);
|
|
if (pathEnd == nil) {
|
|
/* no subdirectory components found */
|
|
goto bail;
|
|
}
|
|
pathEnd--;
|
|
|
|
Assert(pathEnd >= pathStart);
|
|
if (pathEnd - pathStart < 0)
|
|
goto bail;
|
|
|
|
/* (on some filesystems, strncasecmp would be appropriate here) */
|
|
if (pArchive->lastDirCreated &&
|
|
strncmp(pathStart, pArchive->lastDirCreated, pathEnd - pathStart +1) == 0)
|
|
{
|
|
/* we created this one recently, don't do it again */
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* Test to determine which directories exist. The most likely case
|
|
* is that some or all of the components have already been created,
|
|
* so we start with the last one and work backward.
|
|
*/
|
|
err = Nu_CreateSubdirIFN(pArchive, pathStart, pathEnd, fssep);
|
|
BailError(err);
|
|
|
|
bail:
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* Open the file for writing, possibly truncating it.
|
|
*/
|
|
static NuError
|
|
Nu_OpenFileForWrite(NuArchive* pArchive, const char* pathname,
|
|
Boolean openRsrc, FILE** pFp)
|
|
{
|
|
*pFp = fopen(pathname, kNuFileOpenWriteTrunc);
|
|
if (*pFp == nil)
|
|
return errno ? errno : -1;
|
|
return kNuErrNone;
|
|
}
|
|
|
|
|
|
/*
|
|
* Open an output file and prepare it for writing.
|
|
*
|
|
* There are a number of things to take into consideration, including
|
|
* deal with "file exists" conditions, handling Mac/IIgs file types,
|
|
* coping with resource forks on extended files, and handling the
|
|
* "freshen" option that requires us to only update files that are
|
|
* older than what we have.
|
|
*/
|
|
NuError
|
|
Nu_OpenOutputFile(NuArchive* pArchive, const NuRecord* pRecord,
|
|
const NuThread* pThread, const char* newPathname, char newFssep,
|
|
FILE** pFp)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
Boolean exists, isForkedFile, extractingRsrc = false;
|
|
NuFileInfo fileInfo;
|
|
NuErrorStatus errorStatus;
|
|
NuResult result;
|
|
|
|
Assert(pArchive != nil);
|
|
Assert(pRecord != nil);
|
|
Assert(pThread != nil);
|
|
Assert(newPathname != nil);
|
|
Assert(pFp != nil);
|
|
|
|
/* set up some defaults, in case something goes wrong */
|
|
errorStatus.operation = kNuOpExtract;
|
|
errorStatus.err = kNuErrInternal;
|
|
errorStatus.sysErr = 0;
|
|
errorStatus.message = nil;
|
|
errorStatus.pRecord = pRecord;
|
|
errorStatus.pathname = newPathname;
|
|
errorStatus.filenameSeparator = newFssep;
|
|
/*errorStatus.origArchiveTouched = false;*/
|
|
errorStatus.canAbort = true;
|
|
errorStatus.canRetry = true;
|
|
errorStatus.canIgnore = false;
|
|
errorStatus.canSkip = true;
|
|
errorStatus.canRename = true;
|
|
errorStatus.canOverwrite = true;
|
|
|
|
/* decide if this is a forked file (i.e. has *both* forks) */
|
|
isForkedFile = Nu_IsForkedFile(pArchive, pRecord);
|
|
|
|
/* decide if we're extracting a resource fork */
|
|
if (NuMakeThreadID(pThread->thThreadClass, pThread->thThreadKind) ==
|
|
kNuThreadIDRsrcFork)
|
|
{
|
|
extractingRsrc = true;
|
|
}
|
|
|
|
/*
|
|
* Determine whether the file and fork already exists. If the file
|
|
* is one we just created, and the fork we want to write to is
|
|
* empty, this will *not* set "exists".
|
|
*/
|
|
fileInfo.isValid = false;
|
|
err = Nu_FileForkExists(pArchive, newPathname, isForkedFile,
|
|
extractingRsrc, &exists, &fileInfo);
|
|
BailError(err);
|
|
|
|
if (exists) {
|
|
Assert(fileInfo.isValid == true);
|
|
|
|
/*
|
|
* The file exists when it shouldn't. Decide what to do, based
|
|
* on the options configured by the application.
|
|
*/
|
|
|
|
/*
|
|
* Start by checking to see if we're willing to overwrite older files.
|
|
* If not, see if the application wants to rename the file, or force
|
|
* the overwrite. Most likely they'll just want to skip it.
|
|
*/
|
|
if ((pArchive->valOnlyUpdateOlder) &&
|
|
!Nu_IsOlder(&fileInfo.modWhen, &pRecord->recModWhen))
|
|
{
|
|
if (pArchive->errorHandlerFunc != nil) {
|
|
errorStatus.err = kNuErrNotNewer;
|
|
result = (*pArchive->errorHandlerFunc)(pArchive, &errorStatus);
|
|
|
|
switch (result) {
|
|
case kNuAbort:
|
|
err = kNuErrAborted;
|
|
goto bail;
|
|
case kNuRetry:
|
|
case kNuRename:
|
|
err = kNuErrRename;
|
|
goto bail;
|
|
case kNuSkip:
|
|
err = kNuErrSkipped;
|
|
goto bail;
|
|
case kNuOverwrite:
|
|
break; /* fall back into main code */
|
|
case kNuIgnore:
|
|
default:
|
|
err = kNuErrSyntax;
|
|
Nu_ReportError(NU_BLOB, err,
|
|
"Wasn't expecting result %d here", result);
|
|
goto bail;
|
|
}
|
|
} else {
|
|
err = kNuErrNotNewer;
|
|
goto bail;
|
|
}
|
|
}
|
|
|
|
/* If they "might" allow overwrites, and they have an error-handling
|
|
* callback defined, call that to find out what they want to do
|
|
* here. Options include skipping the file, overwriting the file,
|
|
* and extracting to a different file.
|
|
*/
|
|
if (pArchive->valHandleExisting == kNuMaybeOverwrite) {
|
|
if (pArchive->errorHandlerFunc != nil) {
|
|
errorStatus.err = kNuErrFileExists;
|
|
result = (*pArchive->errorHandlerFunc)(pArchive, &errorStatus);
|
|
|
|
switch (result) {
|
|
case kNuAbort:
|
|
err = kNuErrAborted;
|
|
goto bail;
|
|
case kNuRetry:
|
|
case kNuRename:
|
|
err = kNuErrRename;
|
|
goto bail;
|
|
case kNuSkip:
|
|
err = kNuErrSkipped;
|
|
goto bail;
|
|
case kNuOverwrite:
|
|
break; /* fall back into main code */
|
|
case kNuIgnore:
|
|
default:
|
|
err = kNuErrSyntax;
|
|
Nu_ReportError(NU_BLOB, err,
|
|
"Wasn't expecting result %d here", result);
|
|
goto bail;
|
|
}
|
|
} else {
|
|
/* no error handler, return an error to the caller */
|
|
err = kNuErrFileExists;
|
|
goto bail;
|
|
}
|
|
} else if (pArchive->valHandleExisting == kNuNeverOverwrite) {
|
|
err = kNuErrSkipped;
|
|
goto bail;
|
|
}
|
|
} else {
|
|
/*
|
|
* The file doesn't exist. If we're doing a "freshen" from the
|
|
* archive, we don't want to create a new file, so we return an
|
|
* error to the user instead. However, we give the application
|
|
* a chance to straighten things out. Most likely they'll just
|
|
* return kNuSkip.
|
|
*/
|
|
if (pArchive->valHandleExisting == kNuMustOverwrite) {
|
|
DBUG(("+++ can't freshen nonexistent file '%s'\n", newPathname));
|
|
if (pArchive->errorHandlerFunc != nil) {
|
|
errorStatus.err = kNuErrDuplicateNotFound;
|
|
|
|
/* give them a chance to rename */
|
|
result = (*pArchive->errorHandlerFunc)(pArchive, &errorStatus);
|
|
switch (result) {
|
|
case kNuAbort:
|
|
err = kNuErrAborted;
|
|
goto bail;
|
|
case kNuRetry:
|
|
case kNuRename:
|
|
err = kNuErrRename;
|
|
goto bail;
|
|
case kNuSkip:
|
|
err = kNuErrSkipped;
|
|
goto bail;
|
|
case kNuOverwrite:
|
|
break; /* fall back into main code */
|
|
case kNuIgnore:
|
|
default:
|
|
err = kNuErrSyntax;
|
|
Nu_ReportError(NU_BLOB, err,
|
|
"Wasn't expecting result %d here", result);
|
|
goto bail;
|
|
}
|
|
} else {
|
|
/* no error handler, return an error to the caller */
|
|
err = kNuErrDuplicateNotFound;
|
|
goto bail;
|
|
}
|
|
}
|
|
}
|
|
|
|
Assert(err == kNuErrNone);
|
|
|
|
/*
|
|
* After the above, if the file exists then we need to prepare it for
|
|
* writing. On some systems -- notably those with forked files -- it
|
|
* may be easiest to delete the entire file and start over. On
|
|
* simpler systems, an (optional) chmod followed by an open that
|
|
* truncates the file should be sufficient.
|
|
*
|
|
* If the file didn't exist, we need to be sure that the path leading
|
|
* up to its eventual location exists. This might require creating
|
|
* several directories. We distinguish the case of "file isn't there"
|
|
* from "file is there but fork isn't" by seeing if we were able to
|
|
* get valid file info.
|
|
*/
|
|
if (exists) {
|
|
Assert(fileInfo.isValid == true);
|
|
err = Nu_PrepareForWriting(pArchive, newPathname, extractingRsrc,
|
|
&fileInfo);
|
|
BailError(err);
|
|
} else if (!fileInfo.isValid) {
|
|
err = Nu_CreatePathIFN(pArchive, newPathname, newFssep);
|
|
BailError(err);
|
|
}
|
|
|
|
/*
|
|
* Open sesame.
|
|
*/
|
|
err = Nu_OpenFileForWrite(pArchive, newPathname, extractingRsrc, pFp);
|
|
BailError(err);
|
|
|
|
|
|
#if defined(HAS_RESOURCE_FORKS)
|
|
pArchive->lastFileCreated = newPathname;
|
|
#endif
|
|
|
|
bail:
|
|
if (err != kNuErrNone) {
|
|
if (err != kNuErrSkipped && err != kNuErrRename &&
|
|
err != kNuErrFileExists)
|
|
{
|
|
Nu_ReportError(NU_BLOB, err, "Unable to open '%s'%s",
|
|
newPathname, extractingRsrc ? " (rsrc fork)" : "");
|
|
}
|
|
}
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* Close the output file, adjusting the modification date and access
|
|
* permissions as needed.
|
|
*
|
|
* On GS/OS and Mac OS, we may need to set the file type here, depending on
|
|
* how much we managed to do when the file was first created. IIRC,
|
|
* the GS/OS Open call should allow setting the file type.
|
|
*
|
|
* BUG: on GS/OS, if we set the file access after writing the data fork,
|
|
* we may not be able to open the same file for writing the rsrc fork.
|
|
* We can't suppress setting the access permissions, because we don't know
|
|
* if the application will want to write both forks to the same file, or
|
|
* for that matter will want to write the resource fork at all. Looks
|
|
* like we will have to be smart enough to reset the access permissions
|
|
* when writing a rsrc fork to a file with just a data fork. This isn't
|
|
* quite right, but it's close enough.
|
|
*/
|
|
NuError
|
|
Nu_CloseOutputFile(NuArchive* pArchive, const NuRecord* pRecord, FILE* fp,
|
|
const char* pathname)
|
|
{
|
|
NuError err;
|
|
|
|
Assert(pArchive != nil);
|
|
Assert(pRecord != nil);
|
|
Assert(fp != nil);
|
|
|
|
fclose(fp);
|
|
|
|
err = Nu_SetFileDates(pArchive, pRecord, pathname);
|
|
BailError(err);
|
|
|
|
err = Nu_SetFileAccess(pArchive, pRecord, pathname);
|
|
BailError(err);
|
|
|
|
bail:
|
|
return kNuErrNone;
|
|
}
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* Open an input file
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Open the file for reading, in "binary" mode when necessary.
|
|
*/
|
|
static NuError
|
|
Nu_OpenFileForRead(NuArchive* pArchive, const char* pathname,
|
|
Boolean openRsrc, FILE** pFp)
|
|
{
|
|
*pFp = fopen(pathname, kNuFileOpenReadOnly);
|
|
if (*pFp == nil)
|
|
return errno ? errno : -1;
|
|
return kNuErrNone;
|
|
}
|
|
|
|
|
|
/*
|
|
* Open an input file and prepare it for reading.
|
|
*
|
|
* If the file can't be found, we give the application an opportunity to
|
|
* skip the absent file, retry, or abort the whole thing.
|
|
*/
|
|
NuError
|
|
Nu_OpenInputFile(NuArchive* pArchive, const char* pathname,
|
|
Boolean openRsrc, FILE** pFp)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
NuError openErr = kNuErrNone;
|
|
NuErrorStatus errorStatus;
|
|
NuResult result;
|
|
|
|
Assert(pArchive != nil);
|
|
Assert(pathname != nil);
|
|
Assert(pFp != nil);
|
|
|
|
retry:
|
|
/*
|
|
* Open sesame.
|
|
*/
|
|
err = Nu_OpenFileForRead(pArchive, pathname, openRsrc, pFp);
|
|
if (err == kNuErrNone) /* success! */
|
|
goto bail;
|
|
|
|
if (err == ENOENT)
|
|
openErr = kNuErrFileNotFound;
|
|
|
|
if (pArchive->errorHandlerFunc != nil) {
|
|
errorStatus.operation = kNuOpAdd;
|
|
errorStatus.err = openErr;
|
|
errorStatus.sysErr = 0;
|
|
errorStatus.message = nil;
|
|
errorStatus.pRecord = nil;
|
|
errorStatus.pathname = pathname;
|
|
errorStatus.filenameSeparator = '\0';
|
|
/*errorStatus.origArchiveTouched = false;*/
|
|
errorStatus.canAbort = true;
|
|
errorStatus.canRetry = true;
|
|
errorStatus.canIgnore = false;
|
|
errorStatus.canSkip = true;
|
|
errorStatus.canRename = false;
|
|
errorStatus.canOverwrite = false;
|
|
|
|
DBUG(("--- invoking error handler function\n"));
|
|
result = (*pArchive->errorHandlerFunc)(pArchive, &errorStatus);
|
|
|
|
switch (result) {
|
|
case kNuAbort:
|
|
err = kNuErrAborted;
|
|
goto bail;
|
|
case kNuRetry:
|
|
goto retry;
|
|
case kNuSkip:
|
|
err = kNuErrSkipped;
|
|
goto bail;
|
|
case kNuRename:
|
|
case kNuOverwrite:
|
|
case kNuIgnore:
|
|
default:
|
|
err = kNuErrSyntax;
|
|
Nu_ReportError(NU_BLOB, err,
|
|
"Wasn't expecting result %d here", result);
|
|
goto bail;
|
|
}
|
|
} else {
|
|
DBUG(("+++ no error callback in OpenInputFile\n"));
|
|
}
|
|
|
|
bail:
|
|
if (err == kNuErrNone) {
|
|
Assert(*pFp != nil);
|
|
} else {
|
|
if (err != kNuErrSkipped && err != kNuErrRename &&
|
|
err != kNuErrFileExists)
|
|
{
|
|
Nu_ReportError(NU_BLOB, err, "Unable to open '%s'%s",
|
|
pathname, openRsrc ? " (rsrc fork)" : "");
|
|
}
|
|
}
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* Delete and rename files
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Delete a file.
|
|
*/
|
|
NuError
|
|
Nu_DeleteFile(const char* pathname)
|
|
{
|
|
#if defined(UNIX_LIKE) || defined(WINDOWS_LIKE)
|
|
int cc;
|
|
|
|
DBUG(("--- Deleting '%s'\n", pathname));
|
|
|
|
cc = unlink(pathname);
|
|
if (cc < 0)
|
|
return errno ? errno : -1;
|
|
else
|
|
return kNuErrNone;
|
|
#else
|
|
#error "Port this"
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
* Rename a file from "fromPath" to "toPath".
|
|
*/
|
|
NuError
|
|
Nu_RenameFile(const char* fromPath, const char* toPath)
|
|
{
|
|
#if defined(UNIX_LIKE) || defined(WINDOWS_LIKE)
|
|
int cc;
|
|
|
|
DBUG(("--- Renaming '%s' to '%s'\n", fromPath, toPath));
|
|
|
|
cc = rename(fromPath, toPath);
|
|
if (cc < 0)
|
|
return errno ? errno : -1;
|
|
else
|
|
return kNuErrNone;
|
|
#else
|
|
#error "Port this"
|
|
#endif
|
|
}
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* NuError wrappers for std functions
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Wrapper for ftell().
|
|
*/
|
|
NuError
|
|
Nu_FTell(FILE* fp, long* pOffset)
|
|
{
|
|
Assert(fp != nil);
|
|
Assert(pOffset != nil);
|
|
|
|
errno = 0;
|
|
*pOffset = ftell(fp);
|
|
if (*pOffset < 0) {
|
|
Nu_ReportError(NU_NILBLOB, errno, "file ftell failed");
|
|
return errno ? errno : kNuErrFileSeek;
|
|
}
|
|
return kNuErrNone;
|
|
}
|
|
|
|
/*
|
|
* Wrapper for fseek().
|
|
*/
|
|
NuError
|
|
Nu_FSeek(FILE* fp, long offset, int ptrname)
|
|
{
|
|
Assert(fp != nil);
|
|
Assert(ptrname == SEEK_SET || ptrname == SEEK_CUR || ptrname == SEEK_END);
|
|
|
|
errno = 0;
|
|
if (fseek(fp, offset, ptrname) < 0) {
|
|
Nu_ReportError(NU_NILBLOB, errno,
|
|
"file fseek(%ld, %d) failed", offset, ptrname);
|
|
return errno ? errno : kNuErrFileSeek;
|
|
}
|
|
return kNuErrNone;
|
|
}
|
|
|
|
/*
|
|
* Wrapper for fread(). Note the arguments resemble read(2) rather than the
|
|
* slightly silly ones used by fread(3S).
|
|
*/
|
|
NuError
|
|
Nu_FRead(FILE* fp, void* buf, size_t nbyte)
|
|
{
|
|
size_t result;
|
|
|
|
errno = 0;
|
|
result = fread(buf, nbyte, 1, fp);
|
|
if (result != 1)
|
|
return errno ? errno : kNuErrFileRead;
|
|
return kNuErrNone;
|
|
}
|
|
|
|
/*
|
|
* Wrapper for fwrite(). Note the arguments resemble write(2) rather than the
|
|
* slightly silly ones used by fwrite(3S).
|
|
*/
|
|
NuError
|
|
Nu_FWrite(FILE* fp, const void* buf, size_t nbyte)
|
|
{
|
|
size_t result;
|
|
|
|
errno = 0;
|
|
result = fwrite(buf, nbyte, 1, fp);
|
|
if (result != 1)
|
|
return errno ? errno : kNuErrFileWrite;
|
|
return kNuErrNone;
|
|
}
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* Misc functions
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Copy a section from one file to another.
|
|
*/
|
|
NuError
|
|
Nu_CopyFileSection(NuArchive* pArchive, FILE* dstFp, FILE* srcFp, long length)
|
|
{
|
|
NuError err;
|
|
long readLen;
|
|
|
|
Assert(pArchive != nil);
|
|
Assert(dstFp != nil);
|
|
Assert(srcFp != nil);
|
|
Assert(length);
|
|
|
|
/* nice big buffer, for speed... could use getc/putc for simplicity */
|
|
err = Nu_AllocCompressionBufferIFN(pArchive);
|
|
BailError(err);
|
|
|
|
DBUG(("+++ Copying %ld bytes\n", length));
|
|
|
|
while (length) {
|
|
readLen = length > kNuGenCompBufSize ? kNuGenCompBufSize : length;
|
|
|
|
err = Nu_FRead(srcFp, pArchive->compBuf, readLen);
|
|
BailError(err);
|
|
err = Nu_FWrite(dstFp, pArchive->compBuf, readLen);
|
|
BailError(err);
|
|
|
|
length -= readLen;
|
|
}
|
|
|
|
bail:
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* Find the length of an open file.
|
|
*
|
|
* On UNIX it would be easier to just call fstat(), but fseek is portable.
|
|
*
|
|
* Only useful for files < 2GB in size.
|
|
*
|
|
* (pArchive is only used for BailError message reporting, so it's okay
|
|
* to call here with a nil pointer if the archive isn't open yet.)
|
|
*/
|
|
NuError
|
|
Nu_GetFileLength(NuArchive* pArchive, FILE* fp, long* pLength)
|
|
{
|
|
NuError err;
|
|
long oldpos;
|
|
|
|
Assert(fp != nil);
|
|
Assert(pLength != nil);
|
|
|
|
err = Nu_FTell(fp, &oldpos);
|
|
BailError(err);
|
|
|
|
err = Nu_FSeek(fp, 0, SEEK_END);
|
|
BailError(err);
|
|
|
|
err = Nu_FTell(fp, pLength);
|
|
BailError(err);
|
|
|
|
err = Nu_FSeek(fp, oldpos, SEEK_SET);
|
|
BailError(err);
|
|
|
|
bail:
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* Truncate an open file. This differs from ftruncate() in that it takes
|
|
* a FILE* instead of an fd, and the length is a long instead of off_t.
|
|
*/
|
|
NuError
|
|
Nu_TruncateOpenFile(FILE* fp, long length)
|
|
{
|
|
#if defined(HAVE_FTRUNCATE)
|
|
if (ftruncate(fileno(fp), length) < 0)
|
|
return errno ? errno : -1;
|
|
return kNuErrNone;
|
|
#elif defined(HAVE_CHSIZE)
|
|
if (chsize(fileno(fp), length) < 0)
|
|
return errno ? errno : -1;
|
|
return kNuErrNone;
|
|
#else
|
|
/* not fatal; return this to indicate that it's an unsupported operation */
|
|
return kNuErrInternal;
|
|
#endif
|
|
}
|
|
|