mirror of
https://github.com/fadden/ciderpress.git
synced 2024-11-27 08:49:20 +00:00
1481 lines
43 KiB
C
1481 lines
43 KiB
C
/*
|
|
* NuFX archive manipulation library
|
|
* 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-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"
|
|
|
|
#ifdef MAC_LIKE
|
|
# include <sys/xattr.h>
|
|
#endif
|
|
|
|
/*
|
|
* 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 != NULL);
|
|
Assert(pWhen != NULL);
|
|
|
|
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 != NULL);
|
|
Assert(pDateTime != NULL);
|
|
|
|
#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 != NULL);
|
|
|
|
#if defined(UNIX_LIKE) || defined(WINDOWS_LIKE)
|
|
{
|
|
time_t now = time(NULL);
|
|
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? */
|
|
Boolean isForked; /* does file have a non-empty resource fork? */
|
|
|
|
uint32_t dataEof;
|
|
|
|
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.
|
|
*
|
|
* TODO: if we're not using "mask dataless", scanning threads may not
|
|
* get the right answer, because GSHK omits theads for zero-length forks.
|
|
* We could check pRecord->recStorageType, though we have to be careful
|
|
* because that's overloaded for disk images. In any event, the result
|
|
* from this method isn't relevant unless we're trying to use forked
|
|
* files on the native filesystem.
|
|
*/
|
|
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 != NULL);
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
#if defined(MAC_LIKE)
|
|
# if defined(HAS_RESOURCE_FORKS)
|
|
/*
|
|
* String to append to the filename to access the resource fork.
|
|
*
|
|
* This appears to be the correct way to access the resource fork, since
|
|
* at least OS X 10.1. Up until 10.7 ("Lion", July 2011), you could also
|
|
* access the fork with "/rsrc".
|
|
*/
|
|
static const char kMacRsrcPath[] = "/..namedfork/rsrc";
|
|
|
|
/*
|
|
* Generates the resource fork pathname from the file path.
|
|
*
|
|
* The caller must free the string returned.
|
|
*/
|
|
static UNICHAR* GetResourcePath(const UNICHAR* pathnameUNI)
|
|
{
|
|
Assert(pathnameUNI != NULL);
|
|
|
|
// sizeof(kMacRsrcPath) includes the string and the terminating null byte
|
|
const size_t bufLen =
|
|
strlen(pathnameUNI) * sizeof(UNICHAR) + sizeof(kMacRsrcPath);
|
|
char* buf;
|
|
|
|
buf = (char*) malloc(bufLen);
|
|
snprintf(buf, bufLen, "%s%s", pathnameUNI, kMacRsrcPath);
|
|
return buf;
|
|
}
|
|
# endif /*HAS_RESOURCE_FORKS*/
|
|
|
|
/*
|
|
* Due to historical reasons, the XATTR_FINDERINFO_NAME (defined to be
|
|
* ``com.apple.FinderInfo'') extended attribute must be 32 bytes; see the
|
|
* ATTR_CMN_FNDRINFO section in getattrlist(2).
|
|
*
|
|
* The FinderInfo block is the concatenation of a FileInfo structure
|
|
* and an ExtendedFileInfo (or ExtendedFolderInfo) structure -- see
|
|
* ATTR_CMN_FNDRINFO in getattrlist(2).
|
|
*
|
|
* All we're really interested in is the file type and creator code,
|
|
* which are stored big-endian in the first 8 bytes.
|
|
*/
|
|
static const int kFinderInfoSize = 32;
|
|
|
|
/*
|
|
* Set the file type and creator type.
|
|
*/
|
|
static NuError Nu_SetFinderInfo(NuArchive* pArchive, const NuRecord* pRecord,
|
|
const UNICHAR* pathnameUNI)
|
|
{
|
|
uint8_t fiBuf[kFinderInfoSize];
|
|
|
|
size_t actual = getxattr(pathnameUNI, XATTR_FINDERINFO_NAME,
|
|
fiBuf, sizeof(fiBuf), 0, 0);
|
|
if (actual == (size_t) -1 && errno == ENOATTR) {
|
|
// doesn't yet have Finder info
|
|
memset(fiBuf, 0, sizeof(fiBuf));
|
|
} else if (actual != kFinderInfoSize) {
|
|
Nu_ReportError(NU_BLOB, errno,
|
|
"Finder info on '%s' returned %d", pathnameUNI, (int) actual);
|
|
return kNuErrFile;
|
|
}
|
|
|
|
uint8_t proType = (uint8_t) pRecord->recFileType;
|
|
uint16_t proAux = (uint16_t) pRecord->recExtraType;
|
|
|
|
/*
|
|
* Attempt to use one of the convenience types. If nothing matches,
|
|
* use the generic pdos/pXYZ approach. Note that PSYS/PS16 will
|
|
* lose the file's aux type.
|
|
*
|
|
* I'm told this is from page 336 of _Programmer's Reference for
|
|
* System 6.0_.
|
|
*/
|
|
uint8_t* fileTypeBuf = fiBuf;
|
|
uint8_t* creatorBuf = fiBuf + 4;
|
|
|
|
memcpy(creatorBuf, "pdos", 4);
|
|
if (proType == 0x00 && proAux == 0x0000) {
|
|
memcpy(fileTypeBuf, "BINA", 4);
|
|
} else if (proType == 0x04 && proAux == 0x0000) {
|
|
memcpy(fileTypeBuf, "TEXT", 4);
|
|
} else if (proType == 0xff) {
|
|
memcpy(fileTypeBuf, "PSYS", 4);
|
|
} else if (proType == 0xb3 && (proAux & 0xff00) != 0xdb00) {
|
|
memcpy(fileTypeBuf, "PS16", 4);
|
|
} else if (proType == 0xd7 && proAux == 0x0000) {
|
|
memcpy(fileTypeBuf, "MIDI", 4);
|
|
} else if (proType == 0xd8 && proAux == 0x0000) {
|
|
memcpy(fileTypeBuf, "AIFF", 4);
|
|
} else if (proType == 0xd8 && proAux == 0x0001) {
|
|
memcpy(fileTypeBuf, "AIFC", 4);
|
|
} else if (proType == 0xe0 && proAux == 0x0005) {
|
|
memcpy(creatorBuf, "dCpy", 4);
|
|
memcpy(fileTypeBuf, "dImg", 4);
|
|
} else {
|
|
fileTypeBuf[0] = 'p';
|
|
fileTypeBuf[1] = proType;
|
|
fileTypeBuf[2] = (uint8_t) (proAux >> 8);
|
|
fileTypeBuf[3] = (uint8_t) proAux;
|
|
}
|
|
|
|
if (setxattr(pathnameUNI, XATTR_FINDERINFO_NAME, fiBuf, sizeof(fiBuf),
|
|
0, 0) != 0)
|
|
{
|
|
Nu_ReportError(NU_BLOB, errno,
|
|
"Unable to set Finder info on '%s'", pathnameUNI);
|
|
return kNuErrFile;
|
|
}
|
|
|
|
return kNuErrNone;
|
|
}
|
|
#endif /*MAC_LIKE*/
|
|
|
|
|
|
/*
|
|
* 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 UNICHAR* pathnameUNI,
|
|
NuFileInfo* pFileInfo)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
Assert(pArchive != NULL);
|
|
Assert(pathnameUNI != NULL);
|
|
Assert(pFileInfo != NULL);
|
|
|
|
pFileInfo->isValid = false;
|
|
|
|
#if defined(UNIX_LIKE) || defined(WINDOWS_LIKE)
|
|
{
|
|
struct stat sbuf;
|
|
int cc;
|
|
|
|
cc = stat(pathnameUNI, &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->isForked = false;
|
|
# if defined(MAC_LIKE) && defined(HAS_RESOURCE_FORKS)
|
|
if (!pFileInfo->isDirectory) {
|
|
/*
|
|
* Check for the presence of a resource fork. You can check
|
|
* these from a terminal with "ls -l@" -- look for the
|
|
* "com.apple.ResourceFork" attribute.
|
|
*
|
|
* We can either use getxattr() and check for the presence of
|
|
* the attribute, or get the file length with stat(). I
|
|
* don't know if xattr has always worked with resource forks,
|
|
* so we'll stick with stat for now.
|
|
*/
|
|
UNICHAR* rsrcPath = GetResourcePath(pathnameUNI);
|
|
|
|
struct stat res_sbuf;
|
|
|
|
if (stat(rsrcPath, &res_sbuf) == 0) {
|
|
pFileInfo->isForked = (res_sbuf.st_size != 0);
|
|
}
|
|
|
|
free(rsrcPath);
|
|
}
|
|
# endif
|
|
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 UNICHAR* pathnameUNI, Boolean isForkedFile, Boolean checkRsrcFork,
|
|
Boolean* pExists, NuFileInfo* pFileInfo)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
|
|
Assert(pArchive != NULL);
|
|
Assert(pathnameUNI != NULL);
|
|
Assert(checkRsrcFork == true || checkRsrcFork == false);
|
|
Assert(pExists != NULL);
|
|
Assert(pFileInfo != NULL);
|
|
|
|
#if defined(UNIX_LIKE) || defined(WINDOWS_LIKE)
|
|
# if !defined(MAC_LIKE)
|
|
/*
|
|
* On Unix and Windows we ignore "isForkedFile" and "checkRsrcFork".
|
|
* The file must not exist at all.
|
|
*/
|
|
Assert(pArchive->lastFileCreatedUNI == NULL);
|
|
# endif
|
|
|
|
*pExists = true;
|
|
err = Nu_GetFileInfo(pArchive, pathnameUNI, pFileInfo);
|
|
if (err == kNuErrFileNotFound) {
|
|
err = kNuErrNone;
|
|
*pExists = false;
|
|
}
|
|
|
|
# if defined(MAC_LIKE)
|
|
/*
|
|
* On Mac OS X, we'll use the resource fork, but we may not want to
|
|
* overwrite existing data.
|
|
*/
|
|
if (*pExists && checkRsrcFork) {
|
|
*pExists = pFileInfo->isForked;
|
|
}
|
|
# endif
|
|
|
|
#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 UNICHAR* pathnameUNI)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
|
|
Assert(pArchive != NULL);
|
|
Assert(pRecord != NULL);
|
|
Assert(pathnameUNI != NULL);
|
|
|
|
#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(pathnameUNI, &utbuf) < 0) {
|
|
Nu_ReportError(NU_BLOB, errno,
|
|
"Unable to set time stamp on '%s'", pathnameUNI);
|
|
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 UNICHAR* pathnameUNI)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
|
|
Assert(pArchive != NULL);
|
|
Assert(pRecord != NULL);
|
|
Assert(pathnameUNI != NULL);
|
|
|
|
#if defined(UNIX_LIKE) || defined(WINDOWS_LIKE)
|
|
/* only need to do something if the file was "locked" */
|
|
if (Nu_IsRecordLocked(pRecord)) {
|
|
mode_t mask;
|
|
|
|
/* set it to 444, modified by umask */
|
|
mask = umask(0);
|
|
umask(mask);
|
|
//DBUG(("+++ chmod '%s' %03o (mask=%03o)\n", pathname,
|
|
// (S_IRUSR | S_IRGRP | S_IROTH) & ~mask, mask));
|
|
if (chmod(pathnameUNI, (S_IRUSR | S_IRGRP | S_IROTH) & ~mask) < 0) {
|
|
Nu_ReportError(NU_BLOB, errno,
|
|
"unable to set access for '%s' to %03o", pathnameUNI,
|
|
(int) mask);
|
|
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.
|
|
*
|
|
* 20150103: we don't seem to be doing the truncation here, so prepRsrc
|
|
* is unused.
|
|
*/
|
|
static NuError Nu_PrepareForWriting(NuArchive* pArchive,
|
|
const UNICHAR* pathnameUNI, Boolean prepRsrc, NuFileInfo* pFileInfo)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
|
|
Assert(pArchive != NULL);
|
|
Assert(pathnameUNI != NULL);
|
|
Assert(prepRsrc == true || prepRsrc == false);
|
|
Assert(pFileInfo != NULL);
|
|
|
|
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(pathnameUNI, S_IWUSR | pFileInfo->unixMode) < 0) {
|
|
Nu_ReportError(NU_BLOB, errno,
|
|
"unable to set access for '%s'", pathnameUNI);
|
|
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 != NULL);
|
|
Assert(dir != NULL);
|
|
|
|
#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 UNICHAR* pathStartUNI, const char* pathEnd, char fssep)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
NuFileInfo fileInfo;
|
|
char* tmpBuf = NULL;
|
|
|
|
Assert(pArchive != NULL);
|
|
Assert(pathStartUNI != NULL);
|
|
Assert(pathEnd != NULL);
|
|
Assert(fssep != '\0');
|
|
|
|
/* pathStart might have whole path, but we only want up to "pathEnd" */
|
|
tmpBuf = strdup(pathStartUNI);
|
|
tmpBuf[pathEnd - pathStartUNI +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 != NULL) {
|
|
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 UNICHAR* pathnameUNI,
|
|
UNICHAR fssep)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
const char* pathStart;
|
|
const char* pathEnd;
|
|
|
|
Assert(pArchive != NULL);
|
|
Assert(pathnameUNI != NULL);
|
|
Assert(fssep != '\0');
|
|
|
|
pathStart = pathnameUNI;
|
|
|
|
#if !defined(MAC_LIKE) /* On the Mac, if it's a full path, treat it like one */
|
|
// 20150103: not sure what use case this is for
|
|
if (pathnameUNI[0] == fssep)
|
|
pathStart++;
|
|
#endif
|
|
|
|
/* NOTE: not expecting names like "foo/bar/ack/", with terminating fssep */
|
|
pathEnd = strrchr(pathStart, fssep);
|
|
if (pathEnd == NULL) {
|
|
/* no subdirectory components found */
|
|
goto bail;
|
|
}
|
|
pathEnd--;
|
|
|
|
Assert(pathEnd >= pathStart);
|
|
if (pathEnd - pathStart < 0)
|
|
goto bail;
|
|
|
|
/*
|
|
* On some filesystems, strncasecmp would be appropriate here. However,
|
|
* this is meant solely as an optimization to avoid extra stat() calls,
|
|
* so we want to use the most restrictive case.
|
|
*/
|
|
if (pArchive->lastDirCreatedUNI &&
|
|
strncmp(pathStart, pArchive->lastDirCreatedUNI,
|
|
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 UNICHAR* pathnameUNI, Boolean openRsrc, FILE** pFp)
|
|
{
|
|
#if defined(MAC_LIKE) && defined(HAS_RESOURCE_FORKS)
|
|
if (openRsrc) {
|
|
UNICHAR* rsrcPath = GetResourcePath(pathnameUNI);
|
|
*pFp = fopen(rsrcPath, kNuFileOpenWriteTrunc);
|
|
free(rsrcPath);
|
|
} else {
|
|
*pFp = fopen(pathnameUNI, kNuFileOpenWriteTrunc);
|
|
}
|
|
#else
|
|
*pFp = fopen(pathnameUNI, kNuFileOpenWriteTrunc);
|
|
#endif
|
|
if (*pFp == NULL)
|
|
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 UNICHAR* newPathnameUNI, UNICHAR newFssep,
|
|
FILE** pFp)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
Boolean exists, isForkedFile, extractingRsrc = false;
|
|
NuFileInfo fileInfo;
|
|
NuErrorStatus errorStatus;
|
|
NuResult result;
|
|
|
|
Assert(pArchive != NULL);
|
|
Assert(pRecord != NULL);
|
|
Assert(pThread != NULL);
|
|
Assert(newPathnameUNI != NULL);
|
|
Assert(pFp != NULL);
|
|
|
|
/* set up some defaults, in case something goes wrong */
|
|
errorStatus.operation = kNuOpExtract;
|
|
errorStatus.err = kNuErrInternal;
|
|
errorStatus.sysErr = 0;
|
|
errorStatus.message = NULL;
|
|
errorStatus.pRecord = pRecord;
|
|
errorStatus.pathnameUNI = newPathnameUNI;
|
|
errorStatus.origPathname = NULL;
|
|
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, newPathnameUNI, 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 != NULL) {
|
|
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 != NULL) {
|
|
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", newPathnameUNI));
|
|
if (pArchive->errorHandlerFunc != NULL) {
|
|
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, newPathnameUNI, extractingRsrc,
|
|
&fileInfo);
|
|
BailError(err);
|
|
} else if (!fileInfo.isValid) {
|
|
err = Nu_CreatePathIFN(pArchive, newPathnameUNI, newFssep);
|
|
BailError(err);
|
|
}
|
|
|
|
/*
|
|
* Open sesame.
|
|
*/
|
|
err = Nu_OpenFileForWrite(pArchive, newPathnameUNI, extractingRsrc, pFp);
|
|
BailError(err);
|
|
|
|
|
|
#if defined(HAS_RESOURCE_FORKS)
|
|
pArchive->lastFileCreatedUNI = newPathnameUNI;
|
|
#endif
|
|
|
|
bail:
|
|
if (err != kNuErrNone) {
|
|
if (err != kNuErrSkipped && err != kNuErrRename &&
|
|
err != kNuErrFileExists)
|
|
{
|
|
Nu_ReportError(NU_BLOB, err, "Unable to open '%s'%s",
|
|
newPathnameUNI, 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 UNICHAR* pathnameUNI)
|
|
{
|
|
NuError err;
|
|
|
|
Assert(pArchive != NULL);
|
|
Assert(pRecord != NULL);
|
|
Assert(fp != NULL);
|
|
|
|
fclose(fp);
|
|
|
|
err = Nu_SetFileDates(pArchive, pRecord, pathnameUNI);
|
|
BailError(err);
|
|
|
|
#if defined(MAC_LIKE)
|
|
/* could also do this earlier and pass the fd for fsetxattr */
|
|
/* NOTE: must do this before Nu_SetFileAccess */
|
|
err = Nu_SetFinderInfo(pArchive, pRecord, pathnameUNI);
|
|
BailError(err);
|
|
#endif
|
|
|
|
err = Nu_SetFileAccess(pArchive, pRecord, pathnameUNI);
|
|
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 UNICHAR* pathnameUNI, Boolean openRsrc, FILE** pFp)
|
|
{
|
|
*pFp = fopen(pathnameUNI, kNuFileOpenReadOnly);
|
|
if (*pFp == NULL)
|
|
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 UNICHAR* pathnameUNI,
|
|
Boolean openRsrc, FILE** pFp)
|
|
{
|
|
NuError err = kNuErrNone;
|
|
NuError openErr = kNuErrNone;
|
|
NuErrorStatus errorStatus;
|
|
NuResult result;
|
|
|
|
Assert(pArchive != NULL);
|
|
Assert(pathnameUNI != NULL);
|
|
Assert(pFp != NULL);
|
|
|
|
#if defined(MAC_LIKE) && defined(HAS_RESOURCE_FORKS)
|
|
UNICHAR* rsrcPath = NULL;
|
|
if (openRsrc) {
|
|
rsrcPath = GetResourcePath(pathnameUNI);
|
|
pathnameUNI = rsrcPath;
|
|
}
|
|
#endif
|
|
|
|
retry:
|
|
/*
|
|
* Open sesame.
|
|
*/
|
|
err = Nu_OpenFileForRead(pArchive, pathnameUNI, openRsrc, pFp);
|
|
if (err == kNuErrNone) /* success! */
|
|
goto bail;
|
|
|
|
if (err == ENOENT)
|
|
openErr = kNuErrFileNotFound;
|
|
|
|
if (pArchive->errorHandlerFunc != NULL) {
|
|
errorStatus.operation = kNuOpAdd;
|
|
errorStatus.err = openErr;
|
|
errorStatus.sysErr = 0;
|
|
errorStatus.message = NULL;
|
|
errorStatus.pRecord = NULL;
|
|
errorStatus.pathnameUNI = pathnameUNI;
|
|
errorStatus.origPathname = NULL;
|
|
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 != NULL);
|
|
} else {
|
|
if (err != kNuErrSkipped && err != kNuErrRename &&
|
|
err != kNuErrFileExists)
|
|
{
|
|
Nu_ReportError(NU_BLOB, err, "Unable to open '%s'%s",
|
|
pathnameUNI, openRsrc ? " (rsrc fork)" : "");
|
|
}
|
|
}
|
|
#if defined(MAC_LIKE) && defined(HAS_RESOURCE_FORKS)
|
|
free(rsrcPath);
|
|
#endif
|
|
return err;
|
|
}
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* Delete and rename files
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Delete a file.
|
|
*/
|
|
NuError Nu_DeleteFile(const UNICHAR* pathnameUNI)
|
|
{
|
|
#if defined(UNIX_LIKE) || defined(WINDOWS_LIKE)
|
|
int cc;
|
|
|
|
DBUG(("--- Deleting '%s'\n", pathnameUNI));
|
|
|
|
cc = unlink(pathnameUNI);
|
|
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 UNICHAR* fromPathUNI, const UNICHAR* toPathUNI)
|
|
{
|
|
#if defined(UNIX_LIKE) || defined(WINDOWS_LIKE)
|
|
int cc;
|
|
|
|
DBUG(("--- Renaming '%s' to '%s'\n", fromPathUNI, toPathUNI));
|
|
|
|
cc = rename(fromPathUNI, toPathUNI);
|
|
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 != NULL);
|
|
Assert(pOffset != NULL);
|
|
|
|
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 != NULL);
|
|
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 != NULL);
|
|
Assert(dstFp != NULL);
|
|
Assert(srcFp != NULL);
|
|
Assert(length >= 0); /* can be == 0, e.g. empty data fork from HFS */
|
|
|
|
/* 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);
|
|
if (err != kNuErrNone) {
|
|
Nu_ReportError(NU_BLOB, err,
|
|
"Nu_FRead failed while copying file section "
|
|
"(fp=0x%08lx, readLen=%ld, length=%ld, err=%d)\n",
|
|
(long) srcFp, readLen, length, err);
|
|
goto bail;
|
|
}
|
|
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 NULL pointer if the archive isn't open yet.)
|
|
*/
|
|
NuError Nu_GetFileLength(NuArchive* pArchive, FILE* fp, long* pLength)
|
|
{
|
|
NuError err;
|
|
long oldpos;
|
|
|
|
Assert(fp != NULL);
|
|
Assert(pLength != NULL);
|
|
|
|
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
|
|
}
|
|
|