/* * NuLib2 * Copyright (C) 2000-2007 by Andy McFadden, All Rights Reserved. * This is free software; you can redistribute it and/or modify it under the * terms of the BSD License, see the file COPYING. * * Binary II support. * * The BNY format is significantly different from the NuFX format, so * support wasn't included in NufxLib. Since there's no reason to * create BNY archives when SHK is available, I've only implemented * data extraction features. * * (Technically, BNY isn't an archive format. It was a way to encapsulate * file attributes and file data for transmission over a modem, and was * meant to be added and stripped on the fly.) * * TO DO: add "junk skipping" like NufxLib has. */ #include "NuLib2.h" #ifdef HAVE_FCNTL_H # include #endif #ifndef O_BINARY # define O_BINARY 0 #endif /* how to open a file */ #ifdef FOPEN_WANTS_B # define kFileOpenReadOnly "rb" #else # define kFileOpenReadOnly "r" #endif /* * =========================================================================== * Utility functions * =========================================================================== */ /* * Open a file read-only. We abstract this so we get "r" vs "rb" right. * (Moves this to SysUtils.c if anybody else needs it.) */ NuError OpenFileReadOnly(const char* filename, FILE** pFp) { NuError err = kNuErrNone; *pFp = fopen(filename, kFileOpenReadOnly); if (*pFp == NULL) err = errno ? errno : kNuErrFileOpen; return err; } /* * Determine whether the current file we're examining is described by * the file specification given on the command line. * * If no filespec was provided, then all records are "specified". */ Boolean NameIsSpecified(NulibState* pState, const char* filename) { char* const* pSpec; int i; if (!NState_GetFilespecCount(pState)) return true; pSpec = NState_GetFilespecPointer(pState); for (i = NState_GetFilespecCount(pState); i > 0; i--, pSpec++) { #ifdef NU_CASE_SENSITIVE if (NState_GetModRecurse(pState)) { if (strncmp(*pSpec, filename, strlen(*pSpec)) == 0) return true; } else { if (strcmp(*pSpec, filename) == 0) return true; } #else if (NState_GetModRecurse(pState)) { if (strncasecmp(*pSpec, filename, strlen(*pSpec)) == 0) return true; } else { if (strcasecmp(*pSpec, filename) == 0) return true; } #endif } return false; } /* * =========================================================================== * Binary II goodies * =========================================================================== */ /* * State for the currently open Binary II archive. */ typedef struct BNYArchive { NulibState* pState; FILE* fp; Boolean first; } BNYArchive; #define kBNYBlockSize 128 /* BNY files are broken into blocks */ #define kBNYMaxFileName 64 #define kBNYMaxNativeName 48 #define kBNYFlagCompressed (1<<7) #define kBNYFlagEncrypted (1<<6) #define kBNYFlagSparse (1) /* * An entry in a Binary II archive. Each archive is essentially a stream * of files; only the "filesToFollow" value gives any indication that * something else follows this entry. */ typedef struct BNYEntry { uint16_t access; uint16_t fileType; uint32_t auxType; uint8_t storageType; uint32_t fileSize; /* in 512-byte blocks */ uint16_t prodosModDate; uint16_t prodosModTime; NuDateTime modWhen; /* computed from previous two fields */ uint16_t prodosCreateDate; uint16_t prodosCreateTime; NuDateTime createWhen; /* computed from previous two fields */ uint32_t eof; uint32_t realEOF; /* eof is bogus for directories */ char fileName[kBNYMaxFileName+1]; /* ASCII only */ char nativeName[kBNYMaxNativeName+1]; uint32_t diskSpace; /* in 512-byte blocks */ uint8_t osType; /* not exactly same as NuFileSysID */ uint16_t nativeFileType; uint8_t phantomFlag; uint8_t dataFlags; /* advisory flags */ uint8_t version; uint8_t filesToFollow; /* #of files after this one */ uint8_t blockBuf[kBNYBlockSize]; } BNYEntry; /* * Test for the magic number on a file in SQueezed format. */ static inline Boolean IsSqueezed(uint8_t one, uint8_t two) { return (one == 0x76 && two == 0xff); } /* * Test if this entry is a directory. */ static inline Boolean IsDir(BNYEntry* pEntry) { /* * NuLib and "unblu.c" compared against file type 15 (DIR), so I'm * going to do that too, but it would probably be better to compare * against storageType 0x0d. */ return (pEntry->fileType == 15); } /* * Initialize a BNYArchive structure. */ static BNYArchive* BNYInit(NulibState* pState) { BNYArchive* pBny; pBny = Malloc(sizeof(*pBny)); memset(pBny, 0, sizeof(*pBny)); pBny->pState = pState; return pBny; } /* * Free up a BNYArchive, disposing of anything inside it. */ static void BNYFree(BNYArchive* pBny) { /* don't need to do this on stdin, but won't really hurt */ if (pBny->fp != NULL) fclose(pBny->fp); Free(pBny); } /* * Open a Binary II archive read-only. Might be a file on disk or a * stream on stdin. */ static NuError BNYOpenReadOnly(BNYArchive* pBny) { NuError err = kNuErrNone; NulibState* pState; Assert(pBny != NULL); Assert(pBny->pState != NULL); Assert(pBny->fp == NULL); pState = pBny->pState; if (IsFilenameStdin(NState_GetArchiveFilename(pState))) { pBny->fp = stdin; /* * Since the archive is on stdin, we can't ask the user questions. * On a UNIX system we could open /dev/tty, but that's not portable, * and I don't think archives on stdin are going to be popular * enough to make this worth doing. */ NState_SetInputUnavailable(pState, true); } else { err = OpenFileReadOnly(NState_GetArchiveFilename(pState), &pBny->fp); if (err != kNuErrNone) { ReportError(err, "unable to open '%s'", NState_GetArchiveFilename(pState)); goto bail; } } bail: return err; } /* * Wrapper for fread(). Note the arguments resemble read(2) rather * than fread(3S). */ static NuError BNYRead(BNYArchive* pBny, void* buf, size_t nbyte) { size_t result; Assert(pBny != NULL); Assert(buf != NULL); Assert(nbyte > 0); Assert(pBny->fp != NULL); errno = 0; result = fread(buf, 1, nbyte, pBny->fp); if (result != nbyte) return errno ? errno : kNuErrFileRead; return kNuErrNone; } /* * Seek within an archive. Because we need to handle streaming archives, * and don't need to special-case anything, we only allow relative * forward seeks. */ static NuError BNYSeek(BNYArchive* pBny, long offset) { Assert(pBny != NULL); Assert(pBny->fp != NULL); Assert(pBny->pState != NULL); Assert(offset > 0); /*DBUG(("--- seeking forward %ld bytes\n", offset));*/ if (IsFilenameStdin(NState_GetArchiveFilename(pBny->pState))) { /* OPT: might be faster to fread a chunk at a time */ while (offset--) (void) getc(pBny->fp); if (ferror(pBny->fp) || feof(pBny->fp)) return kNuErrFileSeek; } else { if (fseek(pBny->fp, offset, SEEK_CUR) < 0) return kNuErrFileSeek; } return kNuErrNone; } /* * Convert from ProDOS compact date format to the expanded DateTime format. */ static void BNYConvertDateTime(uint16_t prodosDate, uint16_t prodosTime, NuDateTime* pWhen) { pWhen->second = 0; pWhen->minute = prodosTime & 0x3f; pWhen->hour = (prodosTime >> 8) & 0x1f; pWhen->day = (prodosDate & 0x1f) -1; pWhen->month = ((prodosDate >> 5) & 0x0f) -1; pWhen->year = (prodosDate >> 9) & 0x7f; if (pWhen->year < 40) pWhen->year += 100; /* P8 uses 0-39 for 2000-2039 */ pWhen->extra = 0; pWhen->weekDay = 0; } /* * Decode a Binary II header. * * See the File Type Note for $e0/8000 to decipher the buffer offsets * and meanings. */ static NuError BNYDecodeHeader(BNYArchive* pBny, BNYEntry* pEntry) { NuError err = kNuErrNone; uint8_t* raw; int len; Assert(pBny != NULL); Assert(pEntry != NULL); raw = pEntry->blockBuf; if (raw[0] != 0x0a || raw[1] != 0x47 || raw[2] != 0x4c || raw[18] != 0x02) { err = kNuErrBadData; ReportError(err, "this doesn't look like a Binary II header"); goto bail; } pEntry->access = raw[3] | raw[111] << 8; pEntry->fileType = raw[4] | raw[112] << 8; pEntry->auxType = raw[5] | raw[6] << 8 | raw[109] << 16 | raw[110] << 24; pEntry->storageType = raw[7]; pEntry->fileSize = raw[8] | raw[9] << 8; pEntry->prodosModDate = raw[10] | raw[11] << 8; pEntry->prodosModTime = raw[12] | raw[13] << 8; BNYConvertDateTime(pEntry->prodosModDate, pEntry->prodosModTime, &pEntry->modWhen); pEntry->prodosCreateDate = raw[14] | raw[15] << 8; pEntry->prodosCreateTime = raw[16] | raw[17] << 8; BNYConvertDateTime(pEntry->prodosCreateDate, pEntry->prodosCreateTime, &pEntry->createWhen); pEntry->eof = raw[20] | raw[21] << 8 | raw[22] << 16 | raw[116] << 24; len = raw[23]; if (len > kBNYMaxFileName) { err = kNuErrBadData; ReportError(err, "invalid filename length %d", len); goto bail; } memcpy(pEntry->fileName, &raw[24], len); pEntry->fileName[len] = '\0'; pEntry->nativeName[0] = '\0'; if (len <= 15 && raw[39] != 0) { len = raw[39]; if (len > kBNYMaxNativeName) { err = kNuErrBadData; ReportError(err, "invalid filename length %d", len); goto bail; } memcpy(pEntry->nativeName, &raw[40], len); pEntry->nativeName[len] = '\0'; } pEntry->diskSpace = raw[117] | raw[118] << 8 | raw[119] << 16 | raw[120] << 24; pEntry->osType = raw[121]; pEntry->nativeFileType = raw[122] | raw[123] << 8; pEntry->phantomFlag = raw[124]; pEntry->dataFlags = raw[125]; pEntry->version = raw[126]; pEntry->filesToFollow = raw[127]; /* directories are given an EOF but don't actually have any content */ if (IsDir(pEntry)) pEntry->realEOF = 0; else pEntry->realEOF = pEntry->eof; bail: return err; } /* * Normalize the pathname by running it through the usual NuLib2 * function. The trick here is that the function usually takes a * NuPathnameProposal, which we don't happen to have handy. Rather * than generalize the NuLib2 code, we just create a fake proposal, * which is a bit dicey but shouldn't break too easily. * * This takes care of -e, -ee, and -j. * * We return the new path, which is stored in NulibState's temporary * filename buffer. */ const char* BNYNormalizePath(BNYArchive* pBny, BNYEntry* pEntry) { NuPathnameProposal pathProposal; NuRecord fakeRecord; NuThread fakeThread; /* make uninitialized data obvious */ memset(&fakeRecord, 0xa1, sizeof(fakeRecord)); memset(&fakeThread, 0xa5, sizeof(fakeThread)); pathProposal.pathnameUNI = pEntry->fileName; pathProposal.filenameSeparator = '/'; /* BNY always uses ProDOS conv */ pathProposal.pRecord = &fakeRecord; pathProposal.pThread = &fakeThread; pathProposal.newPathnameUNI = NULL; pathProposal.newFilenameSeparator = '\0'; pathProposal.newDataSink = NULL; /* need the filetype and auxtype for -e/-ee */ fakeRecord.recFileType = pEntry->fileType; fakeRecord.recExtraType = pEntry->auxType; /* need the components of a ThreadID */ fakeThread.thThreadClass = kNuThreadClassData; fakeThread.thThreadKind = 0x0000; /* data fork */ return NormalizePath(pBny->pState, &pathProposal); } /* * Copy all data from the Binary II file to "outfp", reading in 128-byte * blocks. We need to read multiples of 128 bytes so that we're ready to * read the next entry without additional seeking. * * Some programs (like ProTERM) apply a 128-byte header without padding * out the file length, so we may come up short at the end of the file. * * Uses pEntry->blockBuf, which already has the first 128 bytes in it. */ static NuError BNYCopyBlocks(BNYArchive* pBny, BNYEntry* pEntry, FILE* outfp) { NuError err = kNuErrNone; long bytesLeft; Assert(pEntry->realEOF > 0); bytesLeft = pEntry->realEOF; while (bytesLeft > 0) { long toWrite; toWrite = bytesLeft; if (toWrite > kBNYBlockSize) toWrite = kBNYBlockSize; if (outfp != NULL) { if (fwrite(pEntry->blockBuf, toWrite, 1, outfp) != 1) { err = errno ? errno : kNuErrFileWrite; ReportError(err, "BNY write failed"); goto bail; } } bytesLeft -= toWrite; if (bytesLeft) { size_t actual; errno = 0; actual = fread(pEntry->blockBuf, 1, kBNYBlockSize, pBny->fp); if (actual != kBNYBlockSize) { /* failure or partial read */ if (actual != bytesLeft) { err = errno ? errno : kNuErrFileRead; ReportError(err, "BNY read failed"); goto bail; } else { DBUG(("+++ partial read %d/%d\n", actual, kBNYBlockSize)); } } } } bail: return err; } /* * =========================================================================== * Unsqueeze * =========================================================================== */ /* * This was ripped fairly directly from Squeeze.c in NufxLib. Because * there's relatively little code, and providing direct access to the * compression functions is a little unwieldy, I've cut & pasted the * necessary pieces here. */ #define FULL_SQ_HEADER #define kSqBufferSize 8192 /* must be enough to hold full SQ header */ #define kNuSQMagic 0xff76 /* magic value for file header */ #define kNuSQRLEDelim 0x90 /* RLE delimiter */ #define kNuSQEOFToken 256 /* distinguished stop symbol */ #define kNuSQNumVals 257 /* 256 symbols + stop */ /* * State during uncompression. */ typedef struct USQState { uint32_t dataInBuffer; uint8_t* dataPtr; int bitPosn; int bits; /* * Decoding tree; first "nodeCount" values are populated. Positive * values are indicies to another node in the tree, negative values * are literals (+1 because "negative zero" doesn't work well). */ int nodeCount; struct { short child[2]; /* left/right kids, must be signed 16-bit */ } decTree[kNuSQNumVals-1]; } USQState; /* * Decode the next symbol from the Huffman stream. */ static NuError USQDecodeHuffSymbol(USQState* pUsqState, int* pVal) { short val = 0; int bits, bitPosn; bits = pUsqState->bits; /* local copy */ bitPosn = pUsqState->bitPosn; do { if (++bitPosn > 7) { /* grab the next byte and use that */ bits = *pUsqState->dataPtr++; bitPosn = 0; if (!pUsqState->dataInBuffer--) return kNuErrBufferUnderrun; val = pUsqState->decTree[val].child[1 & bits]; } else { /* still got bits; shift right and use it */ val = pUsqState->decTree[val].child[1 & (bits >>= 1)]; } } while (val >= 0); /* val is negative literal; add one to make it zero-based then negate it */ *pVal = -(val + 1); pUsqState->bits = bits; pUsqState->bitPosn = bitPosn; return kNuErrNone; } /* * Read two bytes of signed data out of the buffer. */ static inline NuError USQReadShort(USQState* pUsqState, short* pShort) { if (pUsqState->dataInBuffer < 2) return kNuErrBufferUnderrun; *pShort = *pUsqState->dataPtr++; *pShort |= (*pUsqState->dataPtr++) << 8; pUsqState->dataInBuffer -= 2; return kNuErrNone; } /* * Expand "SQ" format. * * Because we have a stop symbol, knowing the uncompressed length of * the file is not essential. */ static NuError BNYUnSqueeze(BNYArchive* pBny, BNYEntry* pEntry, FILE* outfp) { NuError err = kNuErrNone; USQState usqState; uint32_t compRemaining, getSize; #ifdef FULL_SQ_HEADER uint16_t magic, fileChecksum, checksum; #endif short nodeCount; int i, inrep; uint8_t* tmpBuf = NULL; uint8_t lastc = 0; tmpBuf = Malloc(kSqBufferSize); if (tmpBuf == NULL) { err = kNuErrMalloc; goto bail; } usqState.dataInBuffer = 0; usqState.dataPtr = tmpBuf; usqState.bits = 0; compRemaining = pEntry->realEOF; #ifdef FULL_SQ_HEADER if (compRemaining < 8) #else if (compRemaining < 3) #endif { err = kNuErrBadData; ReportError(err, "too short to be valid SQ data"); goto bail; } /* * Round up to the nearest 128-byte boundary. We need to read * everything out of the file in case this is a streaming archive. * Because the compressed data has an embedded stop symbol, it's okay * to "overrun" the expansion code. */ compRemaining = ((compRemaining + kBNYBlockSize-1) / kBNYBlockSize) * kBNYBlockSize; /* want to grab up to kSqBufferSize bytes */ if (compRemaining > kSqBufferSize) getSize = kSqBufferSize; else getSize = compRemaining; /* copy the <= 128 bytes we already have into the general buffer */ memcpy(usqState.dataPtr, pEntry->blockBuf, kBNYBlockSize); if (getSize > kBNYBlockSize) { getSize -= kBNYBlockSize; compRemaining -= kBNYBlockSize; usqState.dataInBuffer += kBNYBlockSize; } else { Assert(compRemaining <= kBNYBlockSize); usqState.dataInBuffer = getSize; getSize = 0; compRemaining = 0; } /* temporarily advance dataPtr past the block we copied in */ usqState.dataPtr += kBNYBlockSize; /* * Grab a big chunk. "compRemaining" is the amount of compressed * data left in the file, usqState.dataInBuffer is the amount of * compressed data left in the buffer. * * We always want to read 128-byte blocks. */ if (getSize) { Assert(getSize <= kSqBufferSize); err = BNYRead(pBny, usqState.dataPtr, getSize); if (err != kNuErrNone) { ReportError(err, "failed reading compressed data (%u bytes)", getSize); goto bail; } usqState.dataInBuffer += getSize; if (getSize > compRemaining) compRemaining = 0; else compRemaining -= getSize; } /* reset dataPtr */ usqState.dataPtr = tmpBuf; /* * Read the header. We assume that the header will fit in the * compression buffer ( sq allowed 300+ for the filename, plus * 257*2 for the tree, plus misc). */ Assert(kSqBufferSize > 1200); #ifdef FULL_SQ_HEADER err = USQReadShort(&usqState, (short*) &magic); if (err != kNuErrNone) goto bail; if (magic != kNuSQMagic) { err = kNuErrBadData; ReportError(err, "bad magic number in SQ block"); goto bail; } err = USQReadShort(&usqState, (short*) &fileChecksum); if (err != kNuErrNone) goto bail; checksum = 0; /* skip over the filename */ while (*usqState.dataPtr++ != '\0') usqState.dataInBuffer--; usqState.dataInBuffer--; #endif err = USQReadShort(&usqState, &nodeCount); if (err != kNuErrNone) goto bail; if (nodeCount < 0 || nodeCount >= kNuSQNumVals) { err = kNuErrBadData; ReportError(err, "invalid decode tree in SQ (%d nodes)", nodeCount); goto bail; } usqState.nodeCount = nodeCount; /* initialize for possibly empty tree (only happens on an empty file) */ usqState.decTree[0].child[0] = -(kNuSQEOFToken+1); usqState.decTree[0].child[1] = -(kNuSQEOFToken+1); /* read the nodes, ignoring "read errors" until we're done */ for (i = 0; i < nodeCount; i++) { err = USQReadShort(&usqState, &usqState.decTree[i].child[0]); err = USQReadShort(&usqState, &usqState.decTree[i].child[1]); } if (err != kNuErrNone) { err = kNuErrBadData; ReportError(err, "SQ data looks truncated at tree"); goto bail; } usqState.bitPosn = 99; /* force an immediate read */ /* * Start pulling data out of the file. We have to Huffman-decode * the input, and then feed that into an RLE expander. * * A completely lopsided (and broken) Huffman tree could require * 256 tree descents, so we want to try to ensure we have at least 256 * bits in the buffer. Otherwise, we could get a false buffer underrun * indication back from DecodeHuffSymbol. * * The SQ sources actually guarantee that a code will fit entirely * in 16 bits, but there's no reason not to use the larger value. */ inrep = false; while (1) { int val; if (usqState.dataInBuffer < 65 && compRemaining) { /* * Less than 256 bits, but there's more in the file. * * First thing we do is slide the old data to the start of * the buffer. */ if (usqState.dataInBuffer) { Assert(tmpBuf != usqState.dataPtr); memmove(tmpBuf, usqState.dataPtr, usqState.dataInBuffer); } usqState.dataPtr = tmpBuf; /* * Next we read as much as we can. */ if (kSqBufferSize - usqState.dataInBuffer < compRemaining) getSize = kSqBufferSize - usqState.dataInBuffer; else getSize = compRemaining; Assert(getSize <= kSqBufferSize); err = BNYRead(pBny, usqState.dataPtr + usqState.dataInBuffer, getSize); if (err != kNuErrNone) { ReportError(err, "failed reading compressed data (%u bytes)", getSize); goto bail; } usqState.dataInBuffer += getSize; if (getSize > compRemaining) compRemaining = 0; else compRemaining -= getSize; Assert(compRemaining < 32767*65536); Assert(usqState.dataInBuffer <= kSqBufferSize); } err = USQDecodeHuffSymbol(&usqState, &val); if (err != kNuErrNone) { ReportError(err, "failed decoding huff symbol"); goto bail; } if (val == kNuSQEOFToken) break; /* * Feed the symbol into the RLE decoder. */ if (inrep) { /* * Last char was RLE delim, handle this specially. We use * --val instead of val-- because we already emitted the * first occurrence of the char (right before the RLE delim). */ if (val == 0) { /* special case -- just an escaped RLE delim */ lastc = kNuSQRLEDelim; val = 2; } while (--val) { /*if (pCrc != NULL) *pCrc = Nu_CalcCRC16(*pCrc, &lastc, 1);*/ if (outfp != NULL) putc(lastc, outfp); #ifdef FULL_SQ_HEADER checksum += lastc; #endif } inrep = false; } else { /* last char was ordinary */ if (val == kNuSQRLEDelim) { /* set a flag and catch the count the next time around */ inrep = true; } else { lastc = val; /*if (pCrc != NULL) *pCrc = Nu_CalcCRC16(*pCrc, &lastc, 1);*/ if (outfp != NULL) putc(lastc, outfp); #ifdef FULL_SQ_HEADER checksum += lastc; #endif } } } if (inrep) { err = kNuErrBadData; ReportError(err, "got stop symbol when run length expected"); goto bail; } #ifdef FULL_SQ_HEADER /* verify the checksum stored in the SQ file */ if (checksum != fileChecksum) { err = kNuErrBadDataCRC; ReportError(err, "expected 0x%04x, got 0x%04x (SQ)", fileChecksum, checksum); goto bail; } else { DBUG(("--- SQ checksums match (0x%04x)\n", checksum)); } #endif /* * Gobble up any unused bytes in the last 128-byte block. There * shouldn't be more than that left over. */ if (compRemaining > kSqBufferSize) { err = kNuErrBadData; ReportError(err, "wow: found %u bytes left over", compRemaining); goto bail; } if (compRemaining) { DBUG(("+++ slurping up last %ld bytes\n", compRemaining)); err = BNYRead(pBny, tmpBuf, compRemaining); if (err != kNuErrNone) { ReportError(err, "failed reading leftovers"); goto bail; } } bail: if (outfp != NULL) fflush(outfp); Free(tmpBuf); return err; } /* * =========================================================================== * Iterators * =========================================================================== */ typedef NuError (*BNYIteratorFunc)(BNYArchive* pBny, BNYEntry* pEntry, Boolean* consumedFlag); /* * Iterate through a Binary II archive, calling "func" to perform * operations on the file. */ static NuError BNYIterate(NulibState* pState, BNYIteratorFunc func) { NuError err = kNuErrNone; BNYArchive* pBny = NULL; BNYEntry entry; Boolean consumed; int first = true; int toFollow; Assert(pState != NULL); Assert(func != NULL); NState_SetMatchCount(pState, 0); pBny = BNYInit(pState); if (pBny == NULL) { err = kNuErrMalloc; goto bail; } err = BNYOpenReadOnly(pBny); if (err != kNuErrNone) goto bail; toFollow = 1; /* assume 1 file in archive */ pBny->first = true; while (toFollow) { err = BNYRead(pBny, entry.blockBuf, sizeof(entry.blockBuf)); if (err != kNuErrNone) { ReportError(err, "failed while reading header in '%s'", NState_GetArchiveFilename(pState)); goto bail; } err = BNYDecodeHeader(pBny, &entry); if (err != kNuErrNone) { if (first) ReportError(err, "not a Binary II archive?"); goto bail; } /* * If the file has one or more blocks, read the first block now. * This will allow the various functions to evaluate the file * contents for SQueeze compression. */ if (entry.realEOF != 0) { err = BNYRead(pBny, entry.blockBuf, sizeof(entry.blockBuf)); if (err != kNuErrNone) { ReportError(err, "failed while reading '%s'", NState_GetArchiveFilename(pState)); goto bail; } } /* * Invoke the appropriate function if this file was requested. */ consumed = false; if (NameIsSpecified(pBny->pState, entry.fileName)) { NState_IncMatchCount(pState); err = (*func)(pBny, &entry, &consumed); if (err != kNuErrNone) goto bail; pBny->first = false; } /* * If they didn't "consume" the entire BNY entry, we need to * do it for them. We've already read the first block (if it * existed), so we don't need to eat that one again. */ if (!consumed) { int nblocks = (entry.realEOF + kBNYBlockSize-1) / kBNYBlockSize; if (nblocks > 1) { err = BNYSeek(pBny, (nblocks-1) * kBNYBlockSize); if (err != kNuErrNone) { ReportError(err, "failed while seeking forward"); goto bail; } } } if (!first) { if (entry.filesToFollow != toFollow -1) { ReportError(kNuErrNone, "WARNING: filesToFollow %d, expected %d\n", entry.filesToFollow, toFollow -1); } } toFollow = entry.filesToFollow; first = false; } if (!NState_GetMatchCount(pState)) printf("%s: no records match\n", gProgName); bail: if (pBny != NULL) BNYFree(pBny); if (err != kNuErrNone) { DBUG(("--- Iterator returning failure %d\n", err)); } return err; } /* * Get a quick table of contents. */ static NuError BNYListShort(BNYArchive* pBny, BNYEntry* pEntry, Boolean* pConsumedFlag) { NuError err = kNuErrNone; printf("%s\n", pEntry->fileName); return err; } /* * Get a verbose listing of contents. */ static NuError BNYListVerbose(BNYArchive* pBny, BNYEntry* pEntry, Boolean* pConsumedFlag) { NuError err = kNuErrNone; Boolean isSqueezed, isReadOnly; NulibState* pState; char date1[kDateOutputLen]; int len; pState = pBny->pState; if (pBny->first) { const char* ccp; if (IsFilenameStdin(NState_GetArchiveFilename(pState))) ccp = ""; else ccp = FilenameOnly(pState, NState_GetArchiveFilename(pState)); printf("%-59.59s Files:%5u\n\n", ccp, pEntry->filesToFollow+1); printf(" Name Type Auxtyp Modified" " Fmat Length\n"); printf("-------------------------------------------------" "----------------------\n"); } isSqueezed = false; if (pEntry->realEOF && IsSqueezed(pEntry->blockBuf[0], pEntry->blockBuf[1])) isSqueezed = true; len = strlen(pEntry->fileName); isReadOnly = pEntry->access == 0x21L || pEntry->access == 0x01L; if (len <= 27) { printf("%c%-27.27s ", isReadOnly ? '+' : ' ', pEntry->fileName); } else { printf("%c..%-25.25s ", isReadOnly ? '+' : ' ', pEntry->fileName + len - 25); } printf("%s $%04X ", GetFileTypeString(pEntry->fileType), pEntry->auxType); printf("%s ", FormatDateShort(&pEntry->modWhen, date1)); if (isSqueezed) printf("squ "); else printf("unc "); printf("%8u", pEntry->realEOF); printf("\n"); if (!pEntry->filesToFollow) { /* last entry, print footer */ printf("-------------------------------------------------" "----------------------\n"); } return err; } /* * Get a verbose table of contents. */ static NuError BNYListDebug(BNYArchive* pBny, BNYEntry* pEntry, Boolean* pConsumedFlag) { NuError err = kNuErrNone; printf("File name: '%s' Native name: '%s' BNY Version %d\n", pEntry->fileName, pEntry->nativeName, pEntry->version); printf(" Phantom: %s DataFlags: 0x%02x (%s%s%s)\n", pEntry->phantomFlag ? "YES" : "no", pEntry->dataFlags, pEntry->dataFlags & kBNYFlagCompressed ? "compr" : "", pEntry->dataFlags & kBNYFlagEncrypted ? "encry" : "", pEntry->dataFlags & kBNYFlagSparse ? "sparse" : ""); printf(" Modified %d/%02d/%02d %02d:%02d Created %d/%02d/%02d %02d:%02d\n", pEntry->modWhen.year+1900, pEntry->modWhen.month, pEntry->modWhen.day, pEntry->modWhen.hour, pEntry->modWhen.minute, pEntry->createWhen.year+1900, pEntry->createWhen.month, pEntry->createWhen.day, pEntry->createWhen.hour, pEntry->createWhen.minute); printf(" FileType: 0x%04x AuxType: 0x%08x StorageType: 0x%02x\n", pEntry->fileType, pEntry->auxType, pEntry->storageType); printf(" EOF: %u FileSize: %u blocks DiskSpace: %u blocks\n", pEntry->eof, pEntry->fileSize, pEntry->diskSpace); printf(" Access: 0x%04x OSType: %d NativeFileType: 0x%04x\n", pEntry->access, pEntry->osType, pEntry->nativeFileType); if (pEntry->realEOF) { printf(" *File begins 0x%02x 0x%02x%s\n", pEntry->blockBuf[0], pEntry->blockBuf[1], IsSqueezed(pEntry->blockBuf[0], pEntry->blockBuf[1]) ? " (probably SQueezed)" : ""); } printf(" FilesToFollow: %d\n", pEntry->filesToFollow); return err; } /* quick enum to simplify our lives a bit */ typedef enum { kBNYExtNormal, kBNYExtPipe, kBNYExtTest } ExtMode; /* * Handle "extraction" of a directory. */ static NuError BNYExtractDirectory(BNYArchive* pBny, BNYEntry* pEntry, ExtMode extMode) { NuError err = kNuErrNone; const char* newName; Boolean isDir; const char* actionStr = "HOSED"; if (extMode == kBNYExtTest) { actionStr = "skipping "; } else if (NState_GetModJunkPaths(pBny->pState)) { actionStr = "skipping "; } else { /* * Using the normalized name of a directory is a problem * when "-e" is set. Since Binary II officially only * allows ProDOS names, and everything under the sun that * supports "long" filenames can handle 15 chars of * [A-Z][a-z][0-9][.], I'm going to skip the normalization * step. * * The "right" way to handle this is to ignore the directory * entries and just create any directories for the fully * normalized pathname generated below. */ /*newName = BNYNormalizePath(pBny, pEntry);*/ newName = pEntry->fileName; if (newName == NULL) goto bail; err = TestFileExistence(newName, &isDir); if (err == kNuErrNone) { if (isDir) { actionStr = "skipping "; } else { err = kNuErrFileExists; ReportError(err, "unable to create directory '%s'", newName); goto bail; } } else if (err == kNuErrFileNotFound) { err = Mkdir(newName); if (err == kNuErrNone) { actionStr = "creating "; } else { ReportError(err, "failed creating directory '%s'", newName); } } } if (!NState_GetSuppressOutput(pBny->pState)) { printf("\rDONE %s %s (directory)\n", actionStr, pEntry->fileName); } bail: return err; } /* * Handle "extract", "extract to pipe", and "test". */ static NuError BNYExtract(BNYArchive* pBny, BNYEntry* pEntry, Boolean* pConsumedFlag) { NuError err = kNuErrNone; NulibState* pState; ExtMode extMode; const char* actionStr = "HOSED"; FILE* outfp = NULL; Boolean eolConv; pState = pBny->pState; switch (NState_GetCommand(pState)) { case kCommandExtract: extMode = kBNYExtNormal; break; case kCommandExtractToPipe: extMode = kBNYExtPipe; break; case kCommandTest: extMode = kBNYExtTest; break; default: err = kNuErrInternal; ReportError(err, "unexpected command %d in BNYExtract", NState_GetCommand(pState)); Assert(0); goto bail; } /*eolConv = NState_GetModConvertAll(pState);*/ eolConv = false; /* maybe someday */ /* * Binary II requires that all directories be listed explicitly. * If we see one, create it. */ if (IsDir(pEntry)) { err = BNYExtractDirectory(pBny, pEntry, extMode); goto bail; } /* * Open the file, taking various command line flags into account. * If we're writing to a pipe, just use that. If we're in test * mode, the output goes nowhere. */ if (extMode == kBNYExtPipe) { outfp = stdout; } else if (extMode == kBNYExtNormal) { /* * Normalize the filename. If the file is squeezed, and it ends * with ".QQ", strip off the .QQ part. (The SQ format actually * includes a filename within, but it could be confusing to use * that instead of the name in the BNY archive, so I've decided to * ignore it.) */ const char* newName; Boolean isDir; char* ext; /* if we find .qq on a squeezed file, just stomp the copy in pEntry */ if (strlen(pEntry->fileName) > 3) { ext = pEntry->fileName + strlen(pEntry->fileName) -3; if (IsSqueezed(pEntry->blockBuf[0], pEntry->blockBuf[1]) && strcasecmp(ext, ".qq") == 0) { *ext = '\0'; } } newName = BNYNormalizePath(pBny, pEntry); if (newName == NULL) goto bail; err = TestFileExistence(newName, &isDir); if (err == kNuErrNone) { /* file exists, what to do? */ if (isDir) { err = kNuErrFileExists; ReportError(err, "unable to replace directory '%s'", newName); goto bail; } if (!NState_GetModOverwriteExisting(pState)) { err = kNuErrFileExists; ReportError(err, "unable to overwrite '%s' (try '-s'?)", newName); goto bail; } } else if (err != kNuErrFileNotFound) { ReportError(err, "stat failed on '%s'", newName); goto bail; } else { Assert(err == kNuErrFileNotFound); err = kNuErrNone; } /* open it, overwriting anything present */ outfp = fopen(newName, "w"); if (outfp == NULL) { err = kNuErrFileOpen; goto bail; } } else { /* outfp == NULL means we're in test mode */ Assert(outfp == NULL); } /* * Show initial progress update message ("extracting" or "expanding", * depending on whether or not the file was squeezed). */ if (IsSqueezed(pEntry->blockBuf[0], pEntry->blockBuf[1])) actionStr = "expanding "; else actionStr = "extracting"; if (extMode == kBNYExtTest) actionStr = "verifying"; if (!NState_GetSuppressOutput(pState)) { printf("\r 0%% %s%c %s", actionStr, eolConv ? '+' : ' ', pEntry->fileName); } /* * Extract the file. Send the output, perhaps with EOL conversion, * to the output file. * * Thought for the day: assuming a file is Squeezed based only on * the magic number is bogus. If we get certain classes of failures, * and this isn't a streaming archive, we should back up and just * extract it as a plain file. * * If the file is empty, don't do anything. */ if (pEntry->realEOF) { if (IsSqueezed(pEntry->blockBuf[0], pEntry->blockBuf[1])) err = BNYUnSqueeze(pBny, pEntry, outfp); else err = BNYCopyBlocks(pBny, pEntry, outfp); if (err != kNuErrNone) goto bail; } /* * Show final progress update. */ if (!NState_GetSuppressOutput(pState)) { printf("\rDONE\n"); } *pConsumedFlag = true; bail: if (outfp != NULL && outfp != stdout) { #if defined(MAC_LIKE) if (SetFinderInfo(fileno(outfp), pEntry->fileType, pEntry->auxType) != kNuErrNone) { fprintf(stderr, "WARNING: unable to set finder info\n"); } #endif fclose(outfp); } return err; } /* * =========================================================================== * Entry points from NuLib2 * =========================================================================== */ NuError BNYDoExtract(NulibState* pState) { if (NState_GetModConvertText(pState) || NState_GetModConvertAll(pState)) { fprintf(stderr, "%s: Binary II extraction doesn't support '-l' or '-ll'\n", gProgName); return kNuErrSyntax; } if (NState_GetModUpdate(pState) || NState_GetModFreshen(pState)) { fprintf(stderr, "%s: Binary II extraction doesn't support '-f' or '-u'\n", gProgName); return kNuErrSyntax; } if (NState_GetModComments(pState)) { fprintf(stderr, "%s: Binary II extraction doesn't support '-c'\n", gProgName); return kNuErrSyntax; } if (NState_GetCommand(pState) == kCommandExtractToPipe) NState_SetSuppressOutput(pState, true); return BNYIterate(pState, BNYExtract); } NuError BNYDoTest(NulibState* pState) { return BNYIterate(pState, BNYExtract); } NuError BNYDoListShort(NulibState* pState) { return BNYIterate(pState, BNYListShort); } NuError BNYDoListVerbose(NulibState* pState) { return BNYIterate(pState, BNYListVerbose); } NuError BNYDoListDebug(NulibState* pState) { return BNYIterate(pState, BNYListDebug); }