NetBSD/sbin/restore/interactive.c

795 lines
18 KiB
C
Raw Normal View History

/* $NetBSD: interactive.c,v 1.19 2003/08/07 10:04:37 agc Exp $ */
1993-03-21 12:45:37 +03:00
/*
* Copyright (c) 1985, 1993
* The Regents of the University of California. All rights reserved.
1993-03-21 12:45:37 +03:00
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the University nor the names of its contributors
1993-03-21 12:45:37 +03:00
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
1997-09-15 12:04:28 +04:00
#include <sys/cdefs.h>
1993-03-21 12:45:37 +03:00
#ifndef lint
#if 0
1997-09-16 17:44:12 +04:00
static char sccsid[] = "@(#)interactive.c 8.5 (Berkeley) 5/1/95";
#else
__RCSID("$NetBSD: interactive.c,v 1.19 2003/08/07 10:04:37 agc Exp $");
#endif
1993-03-21 12:45:37 +03:00
#endif /* not lint */
#include <sys/param.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <ufs/ufs/dinode.h>
#include <ufs/ufs/dir.h>
1997-09-16 17:44:12 +04:00
#include <ufs/ffs/fs.h>
1993-03-21 12:45:37 +03:00
#include <protocols/dumprestore.h>
1993-03-21 12:45:37 +03:00
#include <setjmp.h>
#include <glob.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "restore.h"
#include "extern.h"
1993-03-21 12:45:37 +03:00
#define round(a, b) (((a) + (b) - 1) / (b) * (b))
/*
* Things to handle interruptions.
*/
static int runshell;
1993-03-21 12:45:37 +03:00
static jmp_buf reset;
static char *nextarg = NULL;
/*
* Structure and routines associated with listing directories.
*/
struct afile {
ino_t fnum; /* inode number of file */
char *fname; /* file name */
short len; /* name length */
char prefix; /* prefix character */
char postfix; /* postfix character */
1993-03-21 12:45:37 +03:00
};
struct arglist {
int freeglob; /* glob structure needs to be freed */
int argcnt; /* next globbed argument to return */
glob_t glob; /* globbing information */
char *cmd; /* the current command */
1993-03-21 12:45:37 +03:00
};
static char *copynext __P((char *, char *));
static int fcmp __P((const void *, const void *));
static void formatf __P((struct afile *, int));
static void getcmd __P((char *, char *, char *, struct arglist *));
struct dirent *glob_readdir __P((RST_DIR *dirp));
static int glob_stat __P((const char *, struct stat *));
1994-12-28 05:21:43 +03:00
static void mkentry __P((char *, struct direct *, struct afile *));
static void printlist __P((char *, char *));
1993-03-21 12:45:37 +03:00
/*
* Read and execute commands from the terminal.
*/
void
1993-03-21 12:45:37 +03:00
runcmdshell()
{
struct entry *np;
1993-03-21 12:45:37 +03:00
ino_t ino;
struct arglist arglist;
1993-03-21 12:45:37 +03:00
char curdir[MAXPATHLEN];
char name[MAXPATHLEN];
char cmd[BUFSIZ];
arglist.freeglob = 0;
arglist.argcnt = 0;
arglist.glob.gl_flags = GLOB_ALTDIRFUNC;
arglist.glob.gl_opendir = (void *)rst_opendir;
arglist.glob.gl_readdir = (void *)glob_readdir;
arglist.glob.gl_closedir = (void *)rst_closedir;
arglist.glob.gl_lstat = glob_stat;
arglist.glob.gl_stat = glob_stat;
1993-03-21 12:45:37 +03:00
canon("/", curdir);
loop:
if (setjmp(reset) != 0) {
if (arglist.freeglob != 0) {
arglist.freeglob = 0;
arglist.argcnt = 0;
globfree(&arglist.glob);
}
1993-03-21 12:45:37 +03:00
nextarg = NULL;
volno = 0;
}
runshell = 1;
getcmd(curdir, cmd, name, &arglist);
1993-03-21 12:45:37 +03:00
switch (cmd[0]) {
/*
* Add elements to the extraction list.
*/
case 'a':
if (strncmp(cmd, "add", strlen(cmd)) != 0)
goto bad;
ino = dirlookup(name);
if (ino == 0)
break;
if (mflag)
pathcheck(name);
treescan(name, ino, addfile);
break;
/*
* Change working directory.
*/
case 'c':
if (strncmp(cmd, "cd", strlen(cmd)) != 0)
goto bad;
ino = dirlookup(name);
if (ino == 0)
break;
if (inodetype(ino) == LEAF) {
fprintf(stderr, "%s: not a directory\n", name);
break;
}
(void) strcpy(curdir, name);
break;
/*
* Delete elements from the extraction list.
*/
case 'd':
if (strncmp(cmd, "delete", strlen(cmd)) != 0)
goto bad;
np = lookupname(name);
if (np == NULL || (np->e_flags & NEW) == 0) {
1993-03-21 12:45:37 +03:00
fprintf(stderr, "%s: not on extraction list\n", name);
break;
}
treescan(name, np->e_ino, deletefile);
break;
/*
* Extract the requested list.
*/
case 'e':
if (strncmp(cmd, "extract", strlen(cmd)) != 0)
goto bad;
createfiles();
createlinks();
setdirmodes(0);
1993-03-21 12:45:37 +03:00
if (dflag)
checkrestore();
volno = 0;
break;
/*
* List available commands.
*/
case 'h':
if (strncmp(cmd, "help", strlen(cmd)) != 0)
goto bad;
case '?':
fprintf(stderr, "%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s",
1993-03-21 12:45:37 +03:00
"Available commands are:\n",
"\tls [arg] - list directory\n",
"\tcd arg - change directory\n",
"\tpwd - print current directory\n",
"\tadd [arg] - add `arg' to list of",
" files to be extracted\n",
"\tdelete [arg] - delete `arg' from",
" list of files to be extracted\n",
"\textract - extract requested files\n",
"\tsetmodes - set modes of requested directories\n",
"\tquit or xit - immediately exit program\n",
1993-03-21 12:45:37 +03:00
"\twhat - list dump header information\n",
"\tverbose - toggle verbose flag",
" (useful with ``ls'')\n",
"\thelp or `?' - print this list\n",
"\tDebug - turn on debugging\n",
1993-03-21 12:45:37 +03:00
"If no `arg' is supplied, the current",
" directory is used\n");
break;
/*
* List a directory.
*/
case 'l':
if (strncmp(cmd, "ls", strlen(cmd)) != 0)
goto bad;
printlist(name, curdir);
1993-03-21 12:45:37 +03:00
break;
/*
* Print current directory.
*/
case 'p':
if (strncmp(cmd, "pwd", strlen(cmd)) != 0)
goto bad;
if (curdir[1] == '\0')
fprintf(stderr, "/\n");
else
fprintf(stderr, "%s\n", &curdir[1]);
break;
/*
* Quit.
*/
case 'q':
if (strncmp(cmd, "quit", strlen(cmd)) != 0)
goto bad;
return;
case 'x':
if (strncmp(cmd, "xit", strlen(cmd)) != 0)
goto bad;
return;
/*
* Toggle verbose mode.
*/
case 'v':
if (strncmp(cmd, "verbose", strlen(cmd)) != 0)
goto bad;
if (vflag) {
fprintf(stderr, "verbose mode off\n");
vflag = 0;
break;
}
fprintf(stderr, "verbose mode on\n");
vflag++;
break;
/*
* Just restore requested directory modes.
*/
case 's':
if (strncmp(cmd, "setmodes", strlen(cmd)) != 0)
goto bad;
setdirmodes(FORCE);
1993-03-21 12:45:37 +03:00
break;
/*
* Print out dump header information.
*/
case 'w':
if (strncmp(cmd, "what", strlen(cmd)) != 0)
goto bad;
printdumpinfo();
break;
/*
* Turn on debugging.
*/
case 'D':
if (strncmp(cmd, "Debug", strlen(cmd)) != 0)
goto bad;
if (dflag) {
fprintf(stderr, "debugging mode off\n");
dflag = 0;
break;
}
fprintf(stderr, "debugging mode on\n");
dflag++;
break;
/*
* Unknown command.
*/
default:
bad:
fprintf(stderr, "%s: unknown command; type ? for help\n", cmd);
break;
}
goto loop;
}
/*
* Read and parse an interactive command.
* The first word on the line is assigned to "cmd". If
* there are no arguments on the command line, then "curdir"
* is returned as the argument. If there are arguments
* on the line they are returned one at a time on each
* successive call to getcmd. Each argument is first assigned
* to "name". If it does not start with "/" the pathname in
* "curdir" is prepended to it. Finally "canon" is called to
* eliminate any embedded ".." components.
*/
static void
1993-03-21 12:45:37 +03:00
getcmd(curdir, cmd, name, ap)
char *curdir, *cmd, *name;
struct arglist *ap;
{
char *cp;
1993-03-21 12:45:37 +03:00
static char input[BUFSIZ];
char output[BUFSIZ];
int globretval;
1993-03-21 12:45:37 +03:00
# define rawname input /* save space by reusing input buffer */
/*
* Check to see if still processing arguments.
*/
if (ap->argcnt > 0)
goto retnext;
1993-03-21 12:45:37 +03:00
if (nextarg != NULL)
goto getnext;
/*
* Read a command line and trim off trailing white space.
*/
do {
2001-02-20 01:48:57 +03:00
fprintf(stderr, "%s > ", getprogname());
1993-03-21 12:45:37 +03:00
(void) fflush(stderr);
(void) fgets(input, BUFSIZ, terminal);
} while (!feof(terminal) && input[0] == '\n');
if (feof(terminal)) {
(void) strcpy(cmd, "quit");
return;
}
for (cp = &input[strlen(input) - 2]; *cp == ' ' || *cp == '\t'; cp--)
/* trim off trailing white space and newline */;
*++cp = '\0';
/*
* Copy the command into "cmd".
*/
cp = copynext(input, cmd);
ap->cmd = cmd;
/*
* If no argument, use curdir as the default.
*/
if (*cp == '\0') {
(void) strcpy(name, curdir);
return;
}
nextarg = cp;
/*
* Find the next argument.
*/
getnext:
cp = copynext(nextarg, rawname);
if (*cp == '\0')
nextarg = NULL;
else
nextarg = cp;
/*
* If it is an absolute pathname, canonicalize it and return it.
1993-03-21 12:45:37 +03:00
*/
if (rawname[0] == '/') {
canon(rawname, name);
} else {
/*
* For relative pathnames, prepend the current directory to
* it then canonicalize and return it.
*/
(void) strcpy(output, curdir);
(void) strcat(output, "/");
(void) strcat(output, rawname);
canon(output, name);
}
if ((globretval = glob(name, GLOB_ALTDIRFUNC, NULL, &ap->glob)) < 0) {
fprintf(stderr, "%s: %s: ", ap->cmd, name);
switch (globretval) {
case GLOB_NOSPACE:
fprintf(stderr, "out of memory\n");
break;
case GLOB_NOMATCH:
fprintf(stderr, "no filename match.\n");
break;
case GLOB_ABORTED:
fprintf(stderr, "glob() aborted.\n");
break;
default:
fprintf(stderr, "unknown error!\n");
break;
}
}
if (ap->glob.gl_pathc == 0)
return;
ap->freeglob = 1;
ap->argcnt = ap->glob.gl_pathc;
retnext:
strcpy(name, ap->glob.gl_pathv[ap->glob.gl_pathc - ap->argcnt]);
if (--ap->argcnt == 0) {
ap->freeglob = 0;
globfree(&ap->glob);
}
1993-03-21 12:45:37 +03:00
# undef rawname
}
/*
* Strip off the next token of the input.
*/
static char *
1993-03-21 12:45:37 +03:00
copynext(input, output)
char *input, *output;
{
char *cp, *bp;
1993-03-21 12:45:37 +03:00
char quote;
for (cp = input; *cp == ' ' || *cp == '\t'; cp++)
/* skip to argument */;
bp = output;
while (*cp != ' ' && *cp != '\t' && *cp != '\0') {
/*
* Handle back slashes.
*/
if (*cp == '\\') {
if (*++cp == '\0') {
fprintf(stderr,
"command lines cannot be continued\n");
continue;
}
*bp++ = *cp++;
continue;
}
/*
* The usual unquoted case.
*/
if (*cp != '\'' && *cp != '"') {
*bp++ = *cp++;
continue;
}
/*
* Handle single and double quotes.
*/
quote = *cp++;
while (*cp != quote && *cp != '\0')
*bp++ = *cp++;
1993-03-21 12:45:37 +03:00
if (*cp++ == '\0') {
fprintf(stderr, "missing %c\n", quote);
cp--;
continue;
}
}
*bp = '\0';
return (cp);
}
/*
* Canonicalize file names to always start with ``./'' and
* remove any imbedded "." and ".." components.
*/
void
1993-03-21 12:45:37 +03:00
canon(rawname, canonname)
char *rawname, *canonname;
{
char *cp, *np;
1993-03-21 12:45:37 +03:00
if (strcmp(rawname, ".") == 0 || strncmp(rawname, "./", 2) == 0)
(void) strcpy(canonname, "");
else if (rawname[0] == '/')
(void) strcpy(canonname, ".");
else
(void) strcpy(canonname, "./");
(void) strcat(canonname, rawname);
/*
* Eliminate multiple and trailing '/'s
*/
for (cp = np = canonname; *np != '\0'; cp++) {
*cp = *np++;
while (*cp == '/' && *np == '/')
np++;
}
*cp = '\0';
if (*--cp == '/')
*cp = '\0';
/*
* Eliminate extraneous "." and ".." from pathnames.
*/
for (np = canonname; *np != '\0'; ) {
np++;
cp = np;
while (*np != '/' && *np != '\0')
np++;
if (np - cp == 1 && *cp == '.') {
cp--;
(void) strcpy(cp, np);
np = cp;
}
if (np - cp == 2 && strncmp(cp, "..", 2) == 0) {
cp--;
while (cp > &canonname[1] && *--cp != '/')
/* find beginning of name */;
(void) strcpy(cp, np);
np = cp;
}
}
}
/*
* Do an "ls" style listing of a directory
*/
static void
printlist(name, basename)
1993-03-21 12:45:37 +03:00
char *name;
char *basename;
{
struct afile *fp, *list, *listp;
struct direct *dp;
1993-03-21 12:45:37 +03:00
struct afile single;
RST_DIR *dirp;
1994-12-28 05:21:43 +03:00
int entries, len, namelen;
char locname[MAXPATHLEN + 1];
1993-03-21 12:45:37 +03:00
dp = pathsearch(name);
listp = NULL;
1994-12-28 05:21:43 +03:00
if (dp == NULL || (!dflag && TSTINO(dp->d_ino, dumpmap) == 0) ||
(!vflag && dp->d_ino == WINO))
return;
1993-03-21 12:45:37 +03:00
if ((dirp = rst_opendir(name)) == NULL) {
entries = 1;
list = &single;
1994-12-28 05:21:43 +03:00
mkentry(name, dp, list);
len = strlen(basename) + 1;
if (strlen(name) - len > single.len) {
freename(single.fname);
single.fname = savename(&name[len]);
single.len = strlen(single.fname);
}
1993-03-21 12:45:37 +03:00
} else {
entries = 0;
while ((dp = rst_readdir(dirp)) != NULL)
entries++;
rst_closedir(dirp);
list = (struct afile *)malloc(entries * sizeof(struct afile));
if (list == NULL) {
fprintf(stderr, "ls: out of memory\n");
return;
}
if ((dirp = rst_opendir(name)) == NULL)
panic("directory reopen failed\n");
1993-03-21 12:45:37 +03:00
fprintf(stderr, "%s:\n", name);
entries = 0;
listp = list;
1994-12-28 05:21:43 +03:00
(void) strncpy(locname, name, MAXPATHLEN);
(void) strncat(locname, "/", MAXPATHLEN);
namelen = strlen(locname);
while ((dp = rst_readdir(dirp)) != NULL) {
if (!dflag && TSTINO(dp->d_ino, dumpmap) == 0)
1993-03-21 12:45:37 +03:00
continue;
1994-12-28 05:21:43 +03:00
if (!vflag && (dp->d_ino == WINO ||
strcmp(dp->d_name, ".") == 0 ||
1993-03-21 12:45:37 +03:00
strcmp(dp->d_name, "..") == 0))
continue;
1994-12-28 05:21:43 +03:00
locname[namelen] = '\0';
if (namelen + dp->d_namlen >= MAXPATHLEN) {
fprintf(stderr, "%s%s: name exceeds %d char\n",
locname, dp->d_name, MAXPATHLEN);
} else {
(void) strncat(locname, dp->d_name,
(int)dp->d_namlen);
mkentry(locname, dp, listp++);
entries++;
}
}
rst_closedir(dirp);
if (entries == 0) {
fprintf(stderr, "\n");
free(list);
return;
1993-03-21 12:45:37 +03:00
}
qsort((char *)list, entries, sizeof(struct afile), fcmp);
1993-03-21 12:45:37 +03:00
}
formatf(list, entries);
if (dirp != NULL) {
for (fp = listp - 1; fp >= list; fp--)
1993-03-21 12:45:37 +03:00
freename(fp->fname);
fprintf(stderr, "\n");
free(list);
}
1993-03-21 12:45:37 +03:00
}
/*
* Read the contents of a directory.
*/
static void
1994-12-28 05:21:43 +03:00
mkentry(name, dp, fp)
char *name;
struct direct *dp;
struct afile *fp;
{
char *cp;
struct entry *np;
1993-03-21 12:45:37 +03:00
fp->fnum = dp->d_ino;
fp->fname = savename(dp->d_name);
for (cp = fp->fname; *cp; cp++)
if (!vflag && (*cp < ' ' || *cp >= 0177))
*cp = '?';
fp->len = cp - fp->fname;
if (dflag && TSTINO(fp->fnum, dumpmap) == 0)
fp->prefix = '^';
1994-12-28 05:21:43 +03:00
else if ((np = lookupname(name)) != NULL && (np->e_flags & NEW))
fp->prefix = '*';
else
fp->prefix = ' ';
switch(dp->d_type) {
default:
fprintf(stderr, "Warning: undefined file type %d\n",
dp->d_type);
/* fall through */
case DT_REG:
fp->postfix = ' ';
break;
case DT_LNK:
fp->postfix = '@';
break;
case DT_FIFO:
case DT_SOCK:
fp->postfix = '=';
break;
case DT_CHR:
case DT_BLK:
fp->postfix = '#';
break;
1994-12-28 05:21:43 +03:00
case DT_WHT:
fp->postfix = '%';
break;
case DT_UNKNOWN:
case DT_DIR:
if (inodetype(dp->d_ino) == NODE)
fp->postfix = '/';
else
fp->postfix = ' ';
break;
1993-03-21 12:45:37 +03:00
}
return;
1993-03-21 12:45:37 +03:00
}
/*
* Print out a pretty listing of a directory
*/
static void
formatf(list, nentry)
struct afile *list;
int nentry;
1993-03-21 12:45:37 +03:00
{
struct afile *fp, *endlist;
int width, bigino, haveprefix, havepostfix;
int i, j, w, precision, columns, lines;
width = 0;
haveprefix = 0;
havepostfix = 0;
precision = 0;
bigino = ROOTINO;
endlist = &list[nentry];
for (fp = &list[0]; fp < endlist; fp++) {
if (bigino < fp->fnum)
bigino = fp->fnum;
if (width < fp->len)
width = fp->len;
if (fp->prefix != ' ')
haveprefix = 1;
if (fp->postfix != ' ')
havepostfix = 1;
}
if (haveprefix)
width++;
if (havepostfix)
width++;
if (vflag) {
for (precision = 0, i = bigino; i > 0; i /= 10)
precision++;
width += precision + 1;
1993-03-21 12:45:37 +03:00
}
width++;
columns = 81 / width;
1993-03-21 12:45:37 +03:00
if (columns == 0)
columns = 1;
lines = (nentry + columns - 1) / columns;
for (i = 0; i < lines; i++) {
for (j = 0; j < columns; j++) {
fp = &list[j * lines + i];
if (vflag) {
fprintf(stderr, "%*d ", precision, fp->fnum);
fp->len += precision + 1;
}
if (haveprefix) {
putc(fp->prefix, stderr);
fp->len++;
}
fprintf(stderr, "%s", fp->fname);
if (havepostfix) {
putc(fp->postfix, stderr);
fp->len++;
}
if (fp + lines >= endlist) {
1993-03-21 12:45:37 +03:00
fprintf(stderr, "\n");
break;
}
for (w = fp->len; w < width; w++)
putc(' ', stderr);
1993-03-21 12:45:37 +03:00
}
}
}
/*
* Skip over directory entries that are not on the tape
*
* First have to get definition of a dirent.
1993-03-21 12:45:37 +03:00
*/
#undef DIRBLKSIZ
#include <dirent.h>
#undef d_ino
struct dirent *
glob_readdir(dirp)
RST_DIR *dirp;
1993-03-21 12:45:37 +03:00
{
struct direct *dp;
static struct dirent adirent;
1993-03-21 12:45:37 +03:00
while ((dp = rst_readdir(dirp)) != NULL) {
1994-12-28 05:21:43 +03:00
if (!vflag && dp->d_ino == WINO)
continue;
if (dflag || TSTINO(dp->d_ino, dumpmap))
break;
}
if (dp == NULL)
return (NULL);
adirent.d_fileno = dp->d_ino;
adirent.d_namlen = dp->d_namlen;
1997-09-16 17:44:12 +04:00
memmove(adirent.d_name, dp->d_name, dp->d_namlen + 1);
return (&adirent);
1993-03-21 12:45:37 +03:00
}
/*
* Return st_mode information in response to stat or lstat calls
1993-03-21 12:45:37 +03:00
*/
static int
glob_stat(name, stp)
const char *name;
struct stat *stp;
1993-03-21 12:45:37 +03:00
{
struct direct *dp;
1993-03-21 12:45:37 +03:00
dp = pathsearch(name);
1994-12-28 05:21:43 +03:00
if (dp == NULL || (!dflag && TSTINO(dp->d_ino, dumpmap) == 0) ||
(!vflag && dp->d_ino == WINO))
return (-1);
if (inodetype(dp->d_ino) == NODE)
1995-01-30 23:32:01 +03:00
stp->st_mode = S_IFDIR;
1993-03-21 12:45:37 +03:00
else
1995-01-30 23:32:01 +03:00
stp->st_mode = S_IFREG;
return (0);
}
/*
* Comparison routine for qsort.
*/
static int
fcmp(f1, f2)
const void *f1, *f2;
{
return (strcmp(((struct afile *)f1)->fname,
((struct afile *)f2)->fname));
1993-03-21 12:45:37 +03:00
}
/*
* respond to interrupts
*/
void
onintr(signo)
int signo;
1993-03-21 12:45:37 +03:00
{
if (command == 'i' && runshell)
1993-03-21 12:45:37 +03:00
longjmp(reset, 1);
if (reply("restore interrupted, continue") == FAIL)
exit(1);
1993-03-21 12:45:37 +03:00
}