mirror of
https://github.com/fadden/ciderpress.git
synced 2024-11-29 05:49:17 +00:00
84706d7ea4
This integrates the latest NufxLib sources, and updates CiderPress to work with the API changes.
581 lines
16 KiB
C
581 lines
16 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.
|
|
*
|
|
* Test local and storage names with Unicode and Mac OS Roman content.
|
|
*
|
|
* On Windows, opening files with fancy filenames requires UTF-16 and
|
|
* special functions. On Linux and Mac OS X we're just writing UTF-8 data,
|
|
* so they don't really need to do anything special other than be 8-bit
|
|
* clean. NufxLib functions take UTF-8 strings, so on Windows we define
|
|
* everything in UTF-16 and convert to UTF-8. (We need the UTF-16 form so
|
|
* we can use "wide" I/O functions to confirm that the file was created
|
|
* with the correct name.)
|
|
*
|
|
* To see files with the correct appearance with "ls", you may need to
|
|
* do something like:
|
|
*
|
|
* % LC_ALL=en_US.UTF-8 ls
|
|
*
|
|
* (Many users set LC_ALL=POSIX to avoid GNU grep slowdowns and altered
|
|
* sort ordering in ls.)
|
|
*/
|
|
#include <stdio.h>
|
|
#include <ctype.h>
|
|
#include "NufxLib.h"
|
|
#include "Common.h"
|
|
|
|
/*
|
|
* Test filenames.
|
|
*
|
|
* The local filename (kTestArchive) contains non-MOR Unicode values
|
|
* (two Japanese characters that Google Translate claims form the verb
|
|
* "shrink"). The temp file name is similar.
|
|
*
|
|
* The entry name uses a mix of simple ASCII, CP1252 MOR, and
|
|
* non-CP1252 MOR characters: fl ligature, double dagger, copyright symbol,
|
|
* Apple logo (the latter of which doesn't have a glyph on Windows or Linux).
|
|
* All of the characters have MOR translations.
|
|
*/
|
|
#ifdef USE_UTF16
|
|
const UNICHAR kTestArchive[] = L"nlTest\u7e2e\u3080.shk";
|
|
const UNICHAR kTestEntryName[] = L"nl-test\u2013\ufb01_\u2021_\u00a9\uf8ff!";
|
|
const UNICHAR kTestTempFile[] = L"nlTest\4e00\u6642\u30d5\u30a1\u30a4\u30eb.tmp";
|
|
#else
|
|
const UNICHAR kTestArchive[] = "nlTest\xe7\xb8\xae\xe3\x82\x80.shk";
|
|
const UNICHAR kTestEntryName[] = "nl-test\xe2\x80\x93\xef\xac\x81_\xe2\x80\xa1_"
|
|
"\xc2\xa9\xef\xa3\xbf!";
|
|
const UNICHAR kTestTempFile[] = "nlTest\xe4\xb8\x80\xe6\x99\x82\xe3\x83\x95"
|
|
"\xe3\x82\xa1\xe3\x82\xa4\xe3\x83\xab.tmp";
|
|
#endif
|
|
|
|
const UNICHAR kLocalFssep = '|';
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* Helper functions
|
|
* ===========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Get a single character of input from the user.
|
|
*/
|
|
static char TGetReplyChar(char defaultReply)
|
|
{
|
|
char tmpBuf[32];
|
|
|
|
if (fgets(tmpBuf, sizeof(tmpBuf), stdin) == NULL)
|
|
return defaultReply;
|
|
if (tmpBuf[0] == '\n' || tmpBuf[0] == '\r')
|
|
return defaultReply;
|
|
|
|
return tmpBuf[0];
|
|
}
|
|
|
|
NuError AddSimpleRecord(NuArchive* pArchive, const char* fileNameMOR,
|
|
NuRecordIdx* pRecordIdx)
|
|
{
|
|
NuFileDetails fileDetails;
|
|
|
|
memset(&fileDetails, 0, sizeof(fileDetails));
|
|
fileDetails.storageNameMOR = fileNameMOR;
|
|
fileDetails.fileSysInfo = kLocalFssep;
|
|
fileDetails.access = kNuAccessUnlocked;
|
|
|
|
return NuAddRecord(pArchive, &fileDetails, pRecordIdx);
|
|
}
|
|
|
|
/*
|
|
* Display error messages... or not.
|
|
*/
|
|
NuResult ErrorMessageHandler(NuArchive* pArchive, void* vErrorMessage)
|
|
{
|
|
const NuErrorMessage* pErrorMessage = (const NuErrorMessage*) vErrorMessage;
|
|
|
|
//if (gSuppressError)
|
|
// return kNuOK;
|
|
|
|
if (pErrorMessage->isDebug) {
|
|
fprintf(stderr, "%sNufxLib says: [%s:%d %s] %s\n",
|
|
pArchive == NULL ? "GLOBAL>" : "",
|
|
pErrorMessage->file, pErrorMessage->line, pErrorMessage->function,
|
|
pErrorMessage->message);
|
|
} else {
|
|
fprintf(stderr, "%sNufxLib says: %s\n",
|
|
pArchive == NULL ? "GLOBAL>" : "",
|
|
pErrorMessage->message);
|
|
}
|
|
|
|
return kNuOK;
|
|
}
|
|
|
|
#ifdef USE_UTF16
|
|
TODO - use _waccess, _wunlink, etc.
|
|
#else
|
|
int RemoveTestFile(const char* title, const char* fileName)
|
|
{
|
|
char answer;
|
|
|
|
if (access(fileName, F_OK) == 0) {
|
|
printf("%s '%s' exists, remove (y/n)? ", title, fileName);
|
|
fflush(stdout);
|
|
answer = TGetReplyChar('n');
|
|
if (tolower(answer) != 'y')
|
|
return -1;
|
|
if (unlink(fileName) < 0) {
|
|
perror("unlink");
|
|
return -1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
* Utility function that wraps NuConvertUNIToMOR, allocating a new
|
|
* buffer to hold the converted string. The caller must free the result.
|
|
*/
|
|
char* CopyUNIToMOR(const UNICHAR* stringUNI)
|
|
{
|
|
size_t morLen;
|
|
char* morBuf;
|
|
|
|
morLen = NuConvertUNIToMOR(stringUNI, NULL, 0);
|
|
if (morLen == (size_t) -1) {
|
|
return NULL;
|
|
}
|
|
morBuf = (char*) malloc(morLen);
|
|
(void) NuConvertUNIToMOR(stringUNI, morBuf, morLen);
|
|
return morBuf;
|
|
}
|
|
|
|
/*
|
|
* Utility function that wraps NuConvertMORToUNI, allocating a new
|
|
* buffer to hold the converted string. The caller must free the result.
|
|
*/
|
|
UNICHAR* CopyMORToUNI(const char* stringMOR)
|
|
{
|
|
size_t uniLen;
|
|
char* uniBuf;
|
|
|
|
uniLen = NuConvertMORToUNI(stringMOR, NULL, 0);
|
|
if (uniLen == (size_t) -1) {
|
|
return NULL;
|
|
}
|
|
uniBuf = (UNICHAR*) malloc(uniLen);
|
|
(void) NuConvertMORToUNI(stringMOR, uniBuf, uniLen);
|
|
return uniBuf;
|
|
}
|
|
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* Tests
|
|
* ===========================================================================
|
|
*/
|
|
|
|
void DumpMorString(const char* str)
|
|
{
|
|
printf("(%d) ", (int) strlen(str));
|
|
while (*str != '\0') {
|
|
if (*str >= 0x20 && *str < 0x7f) {
|
|
putchar(*str);
|
|
} else {
|
|
printf("\\x%02x", (uint8_t) *str);
|
|
}
|
|
str++;
|
|
}
|
|
putchar('\n');
|
|
}
|
|
|
|
void DumpUnicharString(const UNICHAR* str)
|
|
{
|
|
printf("(%d) ", (int) strlen(str));
|
|
while (*str != '\0') {
|
|
if (*str >= 0x20 && *str < 0x7f) {
|
|
putchar(*str);
|
|
} else {
|
|
if (sizeof(UNICHAR) == 1) {
|
|
printf("\\x%02x", (uint8_t) *str);
|
|
} else {
|
|
printf("\\u%04x", (uint16_t) *str);
|
|
}
|
|
}
|
|
str++;
|
|
}
|
|
putchar('\n');
|
|
}
|
|
|
|
/*
|
|
* Some basic string conversion unit tests.
|
|
*
|
|
* TODO: test with short buffer, make sure we don't get partial code
|
|
* points when converting to Unicode
|
|
*/
|
|
int TestStringConversion(void)
|
|
{
|
|
static const char kMORTest[] = "test\xe0\xe9\xed\xf3\xfa#\xf0\xb0";
|
|
|
|
size_t outLen;
|
|
char morBuf[512];
|
|
UNICHAR uniBuf[512];
|
|
|
|
// convert test string to Unicode
|
|
memset(uniBuf, 0xcc, sizeof(uniBuf));
|
|
//printf("MOR: "); DumpMorString(kMORTest);
|
|
|
|
outLen = NuConvertMORToUNI(kMORTest, NULL, 0);
|
|
//printf("outLen is %u\n", (unsigned int) outLen);
|
|
if (NuConvertMORToUNI(kMORTest, uniBuf, sizeof(uniBuf)) != outLen) {
|
|
fprintf(stderr, "Inconsistent MORToUNI len\n");
|
|
return -1;
|
|
}
|
|
//printf("UNI: "); DumpUnicharString(uniBuf);
|
|
if (strlen(uniBuf) + 1 != outLen) {
|
|
fprintf(stderr, "Expected length != actual length\n");
|
|
return -1;
|
|
}
|
|
|
|
// convert Unicode back to MOR
|
|
memset(morBuf, 0xcc, sizeof(morBuf));
|
|
|
|
outLen = NuConvertUNIToMOR(uniBuf, NULL, 0);
|
|
//printf("outLen is %u\n", (unsigned int) outLen);
|
|
if (NuConvertUNIToMOR(uniBuf, morBuf, sizeof(morBuf)) != outLen) {
|
|
fprintf(stderr, "Inconsistent UNIToMOR len\n");
|
|
return -1;
|
|
}
|
|
//printf("MOR: "); DumpMorString(morBuf);
|
|
if (strlen(morBuf) + 1 != outLen) {
|
|
fprintf(stderr, "Expected length != actual length\n");
|
|
return -1;
|
|
}
|
|
|
|
// check vs. original
|
|
if (strcmp(kMORTest, morBuf) != 0) {
|
|
fprintf(stderr, "Test string corrupted by double conversion\n");
|
|
return -1;
|
|
}
|
|
|
|
#ifdef USE_UTF16
|
|
static const UNICHAR kNonMorUniStr[] = L"nlTest\u7e2e\u3080.shk";
|
|
static const UNICHAR kBadUniStr[] = L"nlTest\u7e2e\x30";
|
|
#else
|
|
static const UNICHAR kNonMorUniStr[] = "nlTest\xe7\xb8\xae\xe3\x82\x80.shk";
|
|
static const UNICHAR kBadUniStr[] = "nlTest\x81\xe7";
|
|
#endif
|
|
static const char kNonMorExpected[] = "nlTest??.shk";
|
|
static const char kBadExpected[] = "nlTest??";
|
|
|
|
NuConvertUNIToMOR(kNonMorUniStr, morBuf, sizeof(morBuf));
|
|
if (strcmp(morBuf, kNonMorExpected) != 0) {
|
|
fprintf(stderr, "Non-MOR string conversion failed\n");
|
|
return -1;
|
|
}
|
|
|
|
NuConvertUNIToMOR(kBadUniStr, morBuf, sizeof(morBuf));
|
|
if (strcmp(morBuf, kBadExpected) != 0) {
|
|
fprintf(stderr, "Bad UNI string conversion failed\n");
|
|
return -1;
|
|
}
|
|
|
|
printf("... string conversion tests successful\n");
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Create a new entry and give it a trivial data fork.
|
|
*/
|
|
int AddTestEntry(NuArchive* pArchive, const char* entryNameMOR)
|
|
{
|
|
NuDataSource* pDataSource = NULL;
|
|
NuRecordIdx recordIdx;
|
|
static const char* kTestMsg = "Hello, world!\n";
|
|
uint32_t status;
|
|
NuError err;
|
|
|
|
/*
|
|
* Add our test entry.
|
|
*/
|
|
err = AddSimpleRecord(pArchive, entryNameMOR, &recordIdx);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: add record failed (err=%d)\n", err);
|
|
goto failed;
|
|
}
|
|
|
|
err = NuCreateDataSourceForBuffer(kNuThreadFormatUncompressed,
|
|
0, (const uint8_t*)kTestMsg, 0, strlen(kTestMsg), NULL,
|
|
&pDataSource);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: source create failed (err=%d)\n", err);
|
|
goto failed;
|
|
}
|
|
|
|
err = NuAddThread(pArchive, recordIdx, kNuThreadIDDataFork, pDataSource,
|
|
NULL);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: thread add failed (err=%d)\n", err);
|
|
goto failed;
|
|
}
|
|
pDataSource = NULL; /* now owned by library */
|
|
|
|
/*
|
|
* Flush changes.
|
|
*/
|
|
err = NuFlush(pArchive, &status);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: couldn't flush after add (err=%d, status=%u)\n",
|
|
err, status);
|
|
goto failed;
|
|
}
|
|
|
|
return 0;
|
|
failed:
|
|
if (pDataSource != NULL)
|
|
NuFreeDataSource(pDataSource);
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* Extract the file we created.
|
|
*/
|
|
int TestExtract(NuArchive* pArchive, const char* entryNameMOR)
|
|
{
|
|
const NuRecord* pRecord;
|
|
NuRecordIdx recordIdx;
|
|
NuError err;
|
|
|
|
err = NuGetRecordIdxByName(pArchive, entryNameMOR, &recordIdx);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: couldn't find '%s' (err=%d)\n",
|
|
entryNameMOR, err);
|
|
return -1;
|
|
}
|
|
err = NuGetRecord(pArchive, recordIdx, &pRecord);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: couldn't get record index %u (err=%d)\n",
|
|
recordIdx, err);
|
|
return -1;
|
|
}
|
|
assert(pRecord != NULL);
|
|
|
|
const NuThread* pThread = NULL;
|
|
uint32_t idx;
|
|
for (idx = 0; idx < NuRecordGetNumThreads(pRecord); idx++) {
|
|
pThread = NuGetThread(pRecord, idx);
|
|
|
|
if (NuGetThreadID(pThread) == kNuThreadIDDataFork)
|
|
break;
|
|
}
|
|
if (pThread == NULL) {
|
|
fprintf(stderr, "ERROR: no data thread?\n");
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* Prepare the output file.
|
|
*/
|
|
UNICHAR* entryNameUNI = CopyMORToUNI(entryNameMOR);
|
|
NuDataSink* pDataSink = NULL;
|
|
err = NuCreateDataSinkForFile(true, kNuConvertOff, entryNameUNI,
|
|
kLocalFssep, &pDataSink);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: unable to create data sink for file (err=%d)\n",
|
|
err);
|
|
free(entryNameUNI);
|
|
return -1;
|
|
}
|
|
|
|
err = NuExtractThread(pArchive, pThread->threadIdx, pDataSink);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: extract failed (err=%d)\n", err);
|
|
(void) NuFreeDataSink(pDataSink);
|
|
free(entryNameUNI);
|
|
return -1;
|
|
}
|
|
|
|
(void) NuFreeDataSink(pDataSink);
|
|
|
|
printf("... confirming extraction of '%s'\n", entryNameUNI);
|
|
if (access(entryNameUNI, F_OK) != 0) {
|
|
fprintf(stderr, "ERROR: unable to read '%s' (err=%d)\n",
|
|
entryNameUNI, errno);
|
|
free(entryNameUNI);
|
|
return -1;
|
|
}
|
|
|
|
if (unlink(entryNameUNI) < 0) {
|
|
perror("unlink test entry");
|
|
free(entryNameUNI);
|
|
return -1;
|
|
}
|
|
|
|
free(entryNameUNI);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Run some tests.
|
|
*
|
|
* Returns 0 on success, -1 on error.
|
|
*/
|
|
int DoTests(void)
|
|
{
|
|
NuError err;
|
|
NuArchive* pArchive = NULL;
|
|
char* testEntryNameMOR = NULL;
|
|
int result = 0;
|
|
|
|
if (TestStringConversion() < 0) {
|
|
goto failed;
|
|
}
|
|
|
|
/*
|
|
* Make sure we're starting with a clean slate.
|
|
*/
|
|
if (RemoveTestFile("Test archive", kTestArchive) < 0) {
|
|
goto failed;
|
|
}
|
|
if (RemoveTestFile("Test temp file", kTestTempFile) < 0) {
|
|
goto failed;
|
|
}
|
|
if (RemoveTestFile("Test entry", kTestEntryName) < 0) {
|
|
goto failed;
|
|
}
|
|
|
|
testEntryNameMOR = CopyUNIToMOR(kTestEntryName);
|
|
|
|
/*
|
|
* Create a new archive to play with.
|
|
*/
|
|
err = NuOpenRW(kTestArchive, kTestTempFile, kNuOpenCreat|kNuOpenExcl,
|
|
&pArchive);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: NuOpenRW failed (err=%d)\n", err);
|
|
goto failed;
|
|
}
|
|
if (NuSetErrorMessageHandler(pArchive, ErrorMessageHandler) ==
|
|
kNuInvalidCallback)
|
|
{
|
|
fprintf(stderr, "ERROR: couldn't set message handler\n");
|
|
goto failed;
|
|
}
|
|
|
|
/*
|
|
* Add a single entry.
|
|
*/
|
|
if (AddTestEntry(pArchive, testEntryNameMOR) != 0) {
|
|
goto failed;
|
|
}
|
|
|
|
printf("... checking presence of '%s' and '%s'\n",
|
|
kTestArchive, kTestTempFile);
|
|
|
|
if (access(kTestTempFile, F_OK) != 0) {
|
|
/* in theory, NufxLib doesn't need to use the temp file we provide,
|
|
so this test isn't entirely sound */
|
|
fprintf(stderr, "ERROR: did not find %s (err=%d)\n",
|
|
kTestTempFile, err);
|
|
goto failed;
|
|
}
|
|
|
|
/*
|
|
* Close it and confirm that the file has the expected name.
|
|
*/
|
|
err = NuClose(pArchive);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: mid NuClose failed (err=%d)\n", err);
|
|
goto failed;
|
|
}
|
|
pArchive = NULL;
|
|
|
|
if (access(kTestArchive, F_OK) != 0) {
|
|
fprintf(stderr, "ERROR: did not find %s (err=%d)\n", kTestArchive, err);
|
|
goto failed;
|
|
}
|
|
|
|
/*
|
|
* Reopen it read-only.
|
|
*/
|
|
printf("... reopening archive read-only\n");
|
|
err = NuOpenRO(kTestArchive, &pArchive);
|
|
if (err != kNuErrNone) {
|
|
fprintf(stderr, "ERROR: NuOpenRO failed (err=%d)\n", err);
|
|
goto failed;
|
|
}
|
|
if (NuSetErrorMessageHandler(pArchive, ErrorMessageHandler) ==
|
|
kNuInvalidCallback)
|
|
{
|
|
fprintf(stderr, "ERROR: couldn't set message handler\n");
|
|
goto failed;
|
|
}
|
|
|
|
/*
|
|
* Extract the file.
|
|
*/
|
|
if (TestExtract(pArchive, testEntryNameMOR) < 0) {
|
|
goto failed;
|
|
}
|
|
|
|
/*
|
|
* That's all, folks...
|
|
*/
|
|
NuClose(pArchive);
|
|
pArchive = NULL;
|
|
|
|
printf("... removing '%s'\n", kTestArchive);
|
|
if (unlink(kTestArchive) < 0) {
|
|
perror("unlink kTestArchive");
|
|
goto failed;
|
|
}
|
|
|
|
|
|
leave:
|
|
if (pArchive != NULL) {
|
|
NuAbort(pArchive);
|
|
NuClose(pArchive);
|
|
}
|
|
free(testEntryNameMOR);
|
|
return result;
|
|
|
|
failed:
|
|
result = -1;
|
|
goto leave;
|
|
}
|
|
|
|
|
|
/*
|
|
* Start here.
|
|
*/
|
|
int main(void)
|
|
{
|
|
int32_t major, minor, bug;
|
|
const char* pBuildDate;
|
|
const char* pBuildFlags;
|
|
int cc;
|
|
|
|
(void) NuGetVersion(&major, &minor, &bug, &pBuildDate, &pBuildFlags);
|
|
printf("Using NuFX library v%d.%d.%d, built on or after\n"
|
|
" %s with [%s]\n\n",
|
|
major, minor, bug, pBuildDate, pBuildFlags);
|
|
|
|
if (NuSetGlobalErrorMessageHandler(ErrorMessageHandler) ==
|
|
kNuInvalidCallback)
|
|
{
|
|
fprintf(stderr, "ERROR: can't set the global message handler");
|
|
exit(1);
|
|
}
|
|
|
|
printf("... starting tests\n");
|
|
|
|
cc = DoTests();
|
|
|
|
printf("... tests ended, %s\n", cc == 0 ? "SUCCESS" : "FAILURE");
|
|
exit(cc != 0);
|
|
}
|
|
|