ftp.nice.ch/pub/next/unix/network/news/nn.6.4.16.s.tar.gz#/nn/global.c

This is global.c in view mode; [Download] [Up]

/*
 *	(c) Copyright 1990, Kim Fabricius Storm.  All rights reserved.
 *
 *	Global declarations and auxiliary functions.
 */

#include <signal.h>
#include <errno.h>
#include <pwd.h>
#include "config.h"
#include "patchlevel.h"

export char *home_directory;
export char *nn_directory;		/* ~/.nn */
export char *news_directory;		/* /usr/spool/news */
export char *news_lib_directory; 	/* /usr/lib/news */
export char *lib_directory;		/* /usr/local/lib/nn */
export char *master_directory;		/* = lib */
export char *help_directory;		/* = lib/help */
export char *bin_directory = BIN_DIRECTORY;

export char *db_directory;	/* /usr/spool/nn or NEWS_DIR/.nn */
export char *db_data_directory;	/* ..../DATA     or undefined    */
export int db_data_subdirs = 0;	/* set if DATA/[0-9]/ exist	 */

export char *news_active;	/* NLIB/active or DB/ACTIVE */

export char *pager;

export char *log_file;			/* = lib/Log */
export char *log_entry_filter = NULL;

export char *temp_file;

#ifndef TMP_DIRECTORY
#define TMP_DIRECTORY "/usr/tmp"
#endif
export char *tmp_directory = TMP_DIRECTORY;

export char version_id[32];

#ifdef NNTP
export int use_nntp = 0;	/* bool: t iff we use nntp */
#endif

export unsigned short user_eid;
export unsigned short user_id, group_id;
export int process_id;
export int who_am_i;
export int dont_write_console = 0;
export int mail_errors_mode = 2;

#ifdef HAVE_MULTIGROUP
#ifndef NGROUPS
#include <sys/param.h>
#endif
#ifndef GIDSET_TYPE
#define GIDSET_TYPE int
#endif
static int ngroups;
static GIDSET_TYPE gidset[NGROUPS];

static in_grplist(gid)
GIDSET_TYPE gid;
{
    int n;

    if (gid == group_id) return 1;

    for (n = 0; n < ngroups; ++n)
	if (gidset[n] == gid) return 1;

    return 0;
}

#define group_access(gpid)	in_grplist((GIDSET_TYPE)(gpid))
#else
#define group_access(gid)	((gid) == group_id)
#endif

/* signal handler interface */

export int s_hangup		= 0;	/* hangup signal */
export int s_keyboard		= 0;	/* keyboard interrupt */
export int s_pipe		= 0;	/* broken pipe */
export int s_redraw		= 0;	/* redraw signal (if job control) */
#ifdef RESIZING
export int s_resized		= 0;	/* screen resized */
#endif

#ifdef FAKE_INTERRUPT
#include <setjmp.h>

export jmp_buf fake_keyb_sig;
export int arm_fake_keyb_sig = 0;
export char fake_keyb_siglist[NSIG];
#endif

sig_type catch_hangup(n)
{
    s_hangup = 1;
    signal(n, SIG_IGN);

#ifdef FAKE_INTERRUPT
    if (fake_keyb_siglist[n] && arm_fake_keyb_sig)
        longjmp(fake_keyb_sig, 1);
#endif
}

static sig_type catch_keyboard(n)
{
#ifdef RESET_SIGNAL_WHEN_CAUGHT
    signal(n, catch_keyboard);
#endif
#ifdef FAKE_INTERRUPT
    if (fake_keyb_siglist[n] && arm_fake_keyb_sig)
	longjmp(fake_keyb_sig, 1);
#endif
    s_keyboard++;
}

static sig_type catch_pipe(n)
{
    s_pipe++;

#ifdef RESET_SIGNAL_WHEN_CAUGHT
    signal(n, catch_pipe);
#endif
#ifdef FAKE_INTERRUPT
    if (fake_keyb_siglist[n] && arm_fake_keyb_sig)
        longjmp(fake_keyb_sig, 1);
#endif
}

#ifdef HAVE_JOBCONTROL
static sig_type catch_suspend(n)
{
    s_redraw++;

#ifdef RESET_SIGNAL_WHEN_CAUGHT
    signal(n, catch_suspend);
#endif

    suspend_nn();

#ifdef FAKE_INTERRUPT
    if (fake_keyb_siglist[n] && arm_fake_keyb_sig) {
#ifdef RESIZING
	s_resized++;
#endif
	longjmp(fake_keyb_sig, 1);
    }
#endif
}
#endif


init_global()
{
    unsigned short getuid(), getgid();
    int getpid();
    int suspend_nn();
    int i;

#ifdef FAKE_INTERRUPT
    for (i = 0; i < NSIG; i++) fake_keyb_siglist[i] = i == 2 ? 1 : 0;
#endif

    if (who_am_i != I_AM_NN) {
	signal(SIGINT,  SIG_IGN);
	signal(SIGQUIT, SIG_IGN);
    }
    signal(SIGTERM, catch_hangup);
    signal(SIGHUP,  catch_hangup);
    signal(SIGPIPE, catch_pipe);
    signal(SIGALRM, SIG_IGN);

#ifdef SIGPWR
    signal(SIGPWR, catch_hangup);
#endif

#ifdef CONFIG_NUM_IN_VERSION
    sprintf(version_id, "%s.%d #%d", RELEASE, PATCHLEVEL,
#include "update.h"
	    );
#else
    sprintf(version_id, "%s.%d", RELEASE, PATCHLEVEL);
#endif

    user_id = getuid();

#ifdef HAVE_MULTIGROUP
    ngroups = getgroups(NGROUPS, gidset);	/* Get users's group set */
#endif
    group_id = getegid();
    user_eid = geteuid();

    process_id = getpid();

#ifdef CLIENT_DIRECTORY
    lib_directory = CLIENT_DIRECTORY;
#else
    lib_directory = LIB_DIRECTORY;
#endif

#ifdef NEWS_DIRECTORY
    news_directory = NEWS_DIRECTORY;
#else
    news_directory = "/usr/spool/news";
#endif

#ifdef DB_DIRECTORY
    db_directory = DB_DIRECTORY;
#else
    db_directory = mk_file_name(news_directory, ".nn");
#endif

#ifdef ACCOUNTING
    if (who_am_i == I_AM_ACCT)
	return 0;
#endif

#ifdef DB_DATA_DIRECTORY
    db_data_directory = DB_DATA_DIRECTORY;
#else
#ifdef DB_DIRECTORY
    db_data_directory = mk_file_name(db_directory, "DATA");
#else
    db_data_directory = NULL;
#endif
#endif
#ifndef DB_LONG_NAMES
    if (db_data_directory != NULL)
	db_data_subdirs = file_exist(relative(db_data_directory, "0"), "dx");
#endif

#ifdef NEWS_LIB_DIRECTORY
    news_lib_directory = NEWS_LIB_DIRECTORY;
#else
    news_lib_directory = "/usr/lib/news";
#endif

    /* this may later be changed by nntp_check */
    news_active = mk_file_name(news_lib_directory, "active");

#ifdef MASTER_DIRECTORY
    master_directory = MASTER_DIRECTORY;
#else
    master_directory = LIB_DIRECTORY;
#endif

#ifdef HELP_DIRECTORY
    help_directory = HELP_DIRECTORY;
#else
    help_directory = mk_file_name(lib_directory, "help");
#endif

#ifdef LOG_FILE
    log_file = LOG_FILE;
#else
    log_file = mk_file_name(LIB_DIRECTORY, "Log");
#endif

    if (who_am_i == I_AM_MASTER || who_am_i == I_AM_SPEW)
	return 0;

    signal(SIGINT,  catch_keyboard);
    signal(SIGQUIT, catch_keyboard);
#ifdef HAVE_JOBCONTROL
    signal(SIGTSTP, catch_suspend);
#endif

    if ((home_directory = getenv("HOME")) == NULL)
	user_error("No HOME environment variable");

    if ((pager = getenv("PAGER")) == NULL)
	pager = DEFAULT_PAGER;

    nn_directory = mk_file_name(home_directory, ".nn");

    if (who_am_i != I_AM_ADMIN && !file_exist(nn_directory, "drwx")) {
	if (who_am_i != I_AM_NN) return -1;
	if (mkdir(nn_directory, 0755) < 0)
	    user_error("Cannot create %s directory", nn_directory);
	return 1;
    }

    return 0;
}

new_temp_file()
{
    static char buf[FILENAME];
    static char *temp_dir = NULL;

    if (temp_dir == NULL)
	if ((temp_dir = getenv("TMPDIR")) == NULL)
	    temp_dir = tmp_directory; /* just to make test above false */
	else
	    tmp_directory = temp_dir;

    sprintf(buf, "%s/nn.XXXXXX", tmp_directory);
    mktemp(buf);
    temp_file = buf;
}


FILE *open_file(name, mode)
char *name;
int mode;
{
    FILE *f;
    int fd;
    extern int errno;

    if ((mode & DONT_CREATE) && !file_exist(name, (char *)NULL))
	return NULL;

    switch (mode & 0x0f) {

     case OPEN_READ:

	f = fopen(name, "r");
	break;

     case OPEN_UPDATE:

/*	f = fopen(name, "r+"); 	-- not reliable on many systems (sigh) */

	if ((fd = open(name, O_WRONLY)) >= 0) {
	    if ((f = fdopen(fd, "w")) != NULL) return f;
	    close(fd);
	}

	/* fall thru */

     case OPEN_CREATE:

	f = fopen(name, "w");
	break;

     case OPEN_APPEND:

	f = fopen(name, "a");
	break;

     case OPEN_CREATE_RW:

	f = fopen(name, "w+");	/* not safe on all systems -- beware */
	break;

     default:

	sys_error("Illegal mode: open_file(%s, 0x%x)", name, mode);
    }

    if (f) {
	if (mode & OPEN_UNLINK) unlink(name);
	return f;
    }

    if ((mode & MUST_EXIST) == 0) return NULL;

    sys_error("Cannot open file %s, mode=0x%x, errno=%d", name, mode, errno);

    return NULL;
}

FILE *open_file_search_path(name, mode)
char *name;
int mode;
{
    FILE *f;

    if (name == NULL) return NULL;

    if (*name == '/') return open_file(name, mode);
    
    f = NULL;
    if (!use_nntp)
	f = open_file(relative(news_lib_directory, name), OPEN_READ);
    if (f == NULL)
	f= open_file(relative(lib_directory, name), OPEN_READ);
    if (f == NULL)
	f = open_file(relative(db_directory, name), OPEN_READ);

    return f;
}
    
fgets_multi(buf, size, f)
char *buf;
int size;
register FILE *f;
{
    register char *s = buf;
    register int c, n = size;
    
    while (--n > 0) {
	c = getc(f);
	if (c == '\\') {
	    if ((c = getc(f)) == NL) continue;
	    *s++ = '\\';
	    if (--n < 0) break;
	}
	if (c == EOF) {
	    *s = NUL;
	    return s != buf;
	}
	if (c == NL) {
	    *s = NUL;
	    return 1;
	}
	*s++ = c;
    }
    buf[30] = NUL;
    sys_error("Line too long \"%s...\" (max %d)", buf, size);
}
	
/*
 * 	relative -- concat directory name and file name
 */

char *relative(dir, name)
char *dir, *name;
{
    static char concat_path[FILENAME];

    sprintf(concat_path, "%s/%s", dir, name);
    return concat_path;
}


char *mk_file_name(dir, name)
char *dir, *name;
{
    char *buf;

    buf = newstr(strlen(dir) + strlen(name) + 2);
    sprintf(buf, "%s/%s", dir, name);

    return buf;
}


char *home_relative(dir)
char *dir;
{
    if (dir) {
	if (*dir == '/')
	    return copy_str(dir);
	else {
	    if (*dir == '~' && *++dir == '/') dir++;
	    return mk_file_name(home_directory, dir);
	}
    }
    return NULL;
}


char *substchr(str, c1, c2)
char *str, c1, c2;
{
    char *p;

    if ((p = strchr(str, c1)) != NULL) *p = c2;
    return p;
}

char *copy_str(str)
char *str;
{
    char *new;

    new = newstr(strlen(str) + 1);
    if (new) strcpy(new, str);

    return new;
}

time_t m_time(f)
FILE *f;
{
    struct stat st;

    if (fstat(fileno(f), &st) < 0) return 0;
    return st.st_mtime;
}


time_t file_exist(name, mode)
char *name;
char *mode;
{
    static struct stat statb;
    extern int errno;
    int mask;

    if (name != NULL && stat(name, &statb)) return 0;

    if (mode == NULL) return statb.st_mtime;

    if (statb.st_uid == user_eid)
	mask = 0700;
    else if (group_access(statb.st_gid))
	mask = 0070;
    else
	mask = 0007;

    while (*mode) {
	switch (*mode++) {
	case 'd':
	    if ((statb.st_mode & S_IFMT) == S_IFDIR) continue;
	    errno = ENOTDIR;
	    return 0;
	case 'f':
	    if ((statb.st_mode & S_IFMT) == S_IFREG) continue;
	    if ((statb.st_mode & S_IFMT) == 0000000) continue;
	    if ((statb.st_mode & S_IFMT) == S_IFDIR) {
		errno = EISDIR;
		return 0;
	    }
	    break;
	case 'r':
	    if (statb.st_mode & mask & 0444) continue;
	    break;
	case 'w':
	    if (statb.st_mode & mask & 0222) continue;
	    break;
	case 'x':
	    if (statb.st_mode & mask & 0111) continue;
	    break;
	}
	errno = EACCES;
	return 0;
    }

    /* all modes are ok */
    return statb.st_mtime;
}

/*
 * copy_file: copy (or append) src file to dest file.
 *
 * Returns number of characters copied or an error code:
 *  -1: source file not found
 *  -2: cannot create destination
 *  -3: write error
 */

int32 copy_file(src, dest, append)
char *src, *dest;
int append;
{
    register FILE *s, *d;
    register int32 n = 0;
    register int c;

    s = open_file(src, OPEN_READ);
    if (s == NULL) return -1;

    d = open_file(dest, append ? OPEN_APPEND : OPEN_CREATE);
    if (d == NULL) {
	fclose(s);
	return -2;
    }

    n = 0;
    while ((c = getc(s)) != EOF) {
	putc(c, d);
	n++;
    }

    fclose(s);
    if (fclose(d) == EOF) {
	if (!append) unlink(dest);
	return -3;
    }
    return n;
}

/*
 * move_file: move old file to new file, linking if possible.
 *
 * The third arg determines what is acceptable if the old file cannot be
 * removed after copying to the new file:
 *   0: must remove old, else remove new and fail,
 *   1: must remove or truncate old, else remove new and fail,
 *   2: just leave old if it cannot be removed or truncated.
 *	
 * Returns positive value for success, negative for failure:
 *   0: file renamed (link)
 *   1: file copied, old removed
 *   2: file copied, but old file is only truncated.
 *   3: file copied, but old file still exist.
 *  -1: source file not found
 *  -2: cannot create destination
 *  -3: write error
 *  -4: cannot unlink/truncate old
 *  -5: cannot unlink new
 *  -6: cannot link old to new
 *  -9: messy situation: old and new linked on return (cannot happen?)
 */

move_file(old, new, may_keep_old)
char *old, *new;
int may_keep_old;
{
    int32 n;

    if (file_exist(new, (char *)NULL)) {
	if (file_exist((char *)NULL, "d"))
	    return -5;
	if (unlink(new) < 0)	/* careful - new may be directory ? */
	    switch (errno) {
	     case ENOENT:
		break;
	     case EACCES:
		if (file_exist((char *)NULL, "w")) goto do_copy;
	     default:
		return -5;
	    }
    }
    
    if (link(old, new) < 0)
	switch (errno) {
	 case EACCES:	/* can just as well try to copy */
	 case EXDEV:
	    goto do_copy;
	 default:
	    return -6;
	}
    
    if (unlink(old) == 0)
	return 0;

    /* we were able to link but not unlink old	*/
    /* remove new, and attempt a copy instead	*/
    if (unlink(new) < 0) return -9; /* cannot happen? */

 do_copy:
    if ((n = copy_file(old, new, 0)) < 0) return n;
    if (unlink(old) == 0) return 1;
    if (may_keep_old)
	if (n == 0 || truncate(old, (off_t)0) == 0) return 2;
    if (may_keep_old == 2) return 3;
    unlink(new);
    return -4;
}

save_old_file(name, suffix)
char *name, *suffix;
{
    char buf[FILENAME];
    sprintf(buf, "%s%s", name, suffix);
    return move_file(name, buf, 0);
}

#ifdef HAVE_SYSLOG
#include <syslog.h>
#endif /* HAVE_SYSLOG */


static enter_log(type, va_tail)
char type;
va_tdcl
{
    FILE *log;
    char *msg, buf[512];

    if (log_entry_filter != NULL)
	for (msg = log_entry_filter; *msg; msg++)
	    if (*msg == type) return 1;

    msg  = va_arg1(char *);
    vsprintf(buf, msg, va_args2toN);

    /* cannot use relative: one of the args may be generated by it */

    log = open_file(log_file, OPEN_APPEND);
    if (log == NULL) return 0;

    fprintf(log, "%c: %s (%s): %s\n", type,
	    date_time((time_t)0), user_name(), buf);

    fclose(log);
    return 1;
}

static mail_sys_error(err, isfatal)
char *err;
int isfatal;
{
    FILE *f;
    char cmd[FILENAME*2];

    switch (mail_errors_mode) {
     case 0:
	return;
     case 1:
	if (!isfatal) return;
     default:
	break;
    }
    
#ifdef FATAL_ERROR_MAIL_CMD
    strcpy(cmd, FATAL_ERROR_MAIL_CMD);
#else
#ifdef MAILX
    sprintf(cmd, "%s -s 'nnmaster %s' %s", MAILX,
	    isfatal ? "fatal error" : "warning", OWNER);
#else
    sprintf(cmd, "mail %s", OWNER);
#endif
#endif
    if ((f = popen(cmd, "w")) == NULL) return;
    fprintf(f, "nnmaster %s\n\n%system error:\n%s\n",
	    isfatal ? "terminated" : "warning",
	    isfatal ? "Fatal s" : "S", err);
    pclose(f);
}

/*VARARGS*/
sys_error(va_alist)
va_dcl
{
    char buf[512];
    char *fmt;
    FILE *f;
    use_vararg;

    start_vararg;
    enter_log('E', va_args1toN);
    end_vararg;

    start_vararg;
    fmt = va_arg1(char *);
    vsprintf(buf, fmt, va_args2toN);
    end_vararg;

    if (who_am_i == I_AM_MASTER) {
	mail_sys_error(buf, 1);
	if (dont_write_console) nn_exit(7);
#ifndef HAVE_SYSLOG
	f = open_file("/dev/console", OPEN_CREATE);
	if (f == NULL) nn_exit(8);
	fprintf(f, "\n\rNNMASTER FATAL ERROR\n\r%s\n\n\r", buf);
	fclose(f);
#else /* HAVE_SYSLOG */
	openlog("nnmaster", LOG_CONS, LOG_DAEMON);
	syslog(LOG_ALERT, "%s", buf);
	closelog();
#endif /* HAVE_SYSLOG */
	nn_exit(7);
    }
    user_error("%s", buf);
}

/*
 *	sys_warning: like sys_error but MASTER will return with -1
 *	instead of exit.  Clients still terminate!
 */

/*VARARGS*/
sys_warning(va_alist)
va_dcl
{
    char buf[512];
    char *fmt;
    FILE *f;
    static char *last_err = NULL;
    use_vararg;

    start_vararg;
    fmt = va_arg1(char *);
    vsprintf(buf, fmt, va_args2toN);
    end_vararg;

    if (last_err != NULL) {
	if (strcmp(last_err, buf) == 0) return;
	free(last_err);
    }
    last_err = copy_str(buf);

    start_vararg;
    enter_log('R', va_args1toN);
    end_vararg;

    if (who_am_i != I_AM_MASTER)
	user_error("%s", buf);

    mail_sys_error(buf, 0);
    if (dont_write_console) return -1;
#ifndef HAVE_SYSLOG
    if((f = open_file("/dev/console", OPEN_CREATE)) != NULL) {
	fprintf(f, "\n\rNNMASTER WARNING\n\r%s\n\n\r", buf);
	fclose(f);
    }
#else /* HAVE_SYSLOG */
    openlog("nnmaster", LOG_CONS, LOG_DAEMON);
    syslog(LOG_ALERT, "%s", buf);
    closelog();
#endif /* HAVE_SYSLOG */
    return -1;
}

/*VARARGS*/
log_entry(va_alist)
va_dcl
{
    int type, rval;
    use_vararg;

    start_vararg;
    type = va_arg1(int);
    rval = enter_log(type, va_args2toN);
    end_vararg;

    return rval;
}

char *user_name()
{
    static char *user = NULL;
    struct passwd *pw, *getpwuid();
    extern char *getlogin(), *getenv();

    if (who_am_i == I_AM_MASTER) return "M";
    if (who_am_i == I_AM_EXPIRE) return "X";

    if (user == NULL) {
	user = getlogin();
	if (user != NULL && *user != NUL) goto out;

	pw = getpwuid((int)user_id);
	if (pw != NULL && pw->pw_name[0] != NUL) {
	    user = copy_str(pw->pw_name);
	    goto out;
	}

	user = getenv("LOGNAME");
	if (user != NULL && *user != NUL) goto out;
	user = getenv("USER");
	if (user != NULL && *user != NUL) goto out;
	user = "?";
    }

 out:
    return user;
}

time_t cur_time()
{
    time_t t;

    time(&t);
    return t;
}

char *date_time(t)
time_t t;
{
    char *str;

    if (t == (time_t)0) t = cur_time();
    str = ctime(&t);

    str[16] = 0;
    return str+4;
}

char *plural(n)
long n;
{
    return n != 1 ? "s" : "";
}

/*
 *	memory management
 */

/* #define MEM_DEBUG		/* trace memory usage */

static mem_error(t, bytes)
int t;
int32 bytes;
{
    char buf[200];

    if (t == 1) {
	sprintf(buf, "Alloc failed: unsigned too short to represent %ld bytes",
		(long)bytes);
    } else {
	sprintf(buf, "Out of memory - cannot allocate %ld bytes",
		(long)bytes);
    }

    sys_error(buf);
}

char *mem_obj(size, nelt)
unsigned size;
int32 nelt;
{
    unsigned n;
    char *obj, *calloc();

    n = nelt;
    if (n != nelt) mem_error(1, nelt);

    obj = calloc(n, size);
#ifdef MEM_DEBUG
    printf("CALLOC(%u,%u) => %lx\n", n, size, (long)obj);
#endif
    if (obj == NULL) mem_error(2, (int32)(size * nelt));
    return obj;
}

char *mem_str(nelt)
int32 nelt;
{
    unsigned n;
    char *obj, *malloc();

    n = nelt;
    if (n != nelt) mem_error(1, nelt);

    obj = malloc(n);
#ifdef MEM_DEBUG
    printf("MALLOC(%u) => %lx\n", n, (long)obj);
#endif
    if (obj == NULL) mem_error(2, nelt);
    return obj;
}

char *mem_resize(obj, size, nelt)
char *obj;
unsigned size;
int32 nelt;
{
    unsigned n;
    char *realloc(), *obj1;

    if (obj == NULL)
	return mem_obj(size, nelt);

    nelt *= size;

    n = nelt;
    if (n != nelt) mem_error(1, nelt);

    obj1 = realloc(obj, n);
#ifdef MEM_DEBUG
    printf("REALLOC(%lx, %u) => %lx\n", (long)obj, n, (long)obj1);
#endif
    if (obj1 == NULL) mem_error(2, (int32)size);
    return obj1;
}

char *mem_clear(obj, size, nelt)
register char *obj;
unsigned size;
register int32 nelt;
{
    nelt *= size;
    while (--nelt >= 0) *obj++ = NUL;
}

mem_free(obj)
char *obj;
{
#ifdef MEM_DEBUG
    printf("FREE(%lx)\n", (long)obj);
#endif
    if (obj != NULL) free(obj);
}

#ifndef HAVE_MKDIR

mkdir(path, mode)
char *path;
int mode;
{
    char command[FILENAME*2 + 20];

    sprintf(command, "{ mkdir %s && chmod %o %s ; } > /dev/null 2>&1",
	    path, mode, path);
    return system(command) != 0 ? -1 : 0;
}
#endif

#ifndef HAVE_TRUNCATE

truncate(path, len)
char *path;
off_t len;
{
    int fd;
    struct stat st;

    if (len != 0)
	sys_error("truncate(%s,%ld): non-zero length", path, (long)len);

#ifdef O_TRUNC
    fd = open(path, O_WRONLY | O_TRUNC);
#else
    if (stat(path, &st) < 0) return -1;
    fd = creat(path, st.st_mode & 07777);
#endif    
    if (fd < 0) return -1;
    close(fd);
    return 0;
}

#endif

These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.