1999-10-05 21:54:54 +05:30
|
|
|
/*
|
|
|
|
* Copyright (c) 1999 by David I. Bell
|
|
|
|
* Permission is granted to use, distribute, or modify this source,
|
|
|
|
* provided that this copyright notice remains intact.
|
|
|
|
*
|
|
|
|
* The "tar" command, taken from sash.
|
|
|
|
* This allows creation, extraction, and listing of tar files.
|
|
|
|
*
|
|
|
|
* Permission to distribute this code under the GPL has been granted.
|
|
|
|
* Modified for busybox by Erik Andersen <andersee@debian.org> <andersen@lineo.com>
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
#include "internal.h"
|
|
|
|
|
|
|
|
#ifdef BB_TAR
|
|
|
|
|
|
|
|
const char tar_usage[] =
|
|
|
|
"Create, extract, or list files from a TAR file\n\n"
|
|
|
|
"usage: tar -[cxtvOf] [tarFileName] [FILE] ...\n"
|
|
|
|
"\tc=create, x=extract, t=list contents, v=verbose,\n"
|
|
|
|
"\tO=extract to stdout, f=tarfile or \"-\" for stdin\n";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#include <stdio.h>
|
|
|
|
#include <dirent.h>
|
|
|
|
#include <errno.h>
|
|
|
|
#include <fcntl.h>
|
|
|
|
#include <signal.h>
|
|
|
|
#include <time.h>
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Tar file constants.
|
|
|
|
*/
|
|
|
|
#define TAR_BLOCK_SIZE 512
|
|
|
|
#define TAR_NAME_SIZE 100
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* The POSIX (and basic GNU) tar header format.
|
|
|
|
* This structure is always embedded in a TAR_BLOCK_SIZE sized block
|
|
|
|
* with zero padding. We only process this information minimally.
|
|
|
|
*/
|
|
|
|
typedef struct
|
|
|
|
{
|
|
|
|
char name[TAR_NAME_SIZE];
|
|
|
|
char mode[8];
|
|
|
|
char uid[8];
|
|
|
|
char gid[8];
|
|
|
|
char size[12];
|
|
|
|
char mtime[12];
|
|
|
|
char checkSum[8];
|
|
|
|
char typeFlag;
|
|
|
|
char linkName[TAR_NAME_SIZE];
|
|
|
|
char magic[6];
|
|
|
|
char version[2];
|
|
|
|
char uname[32];
|
|
|
|
char gname[32];
|
|
|
|
char devMajor[8];
|
|
|
|
char devMinor[8];
|
|
|
|
char prefix[155];
|
|
|
|
} TarHeader;
|
|
|
|
|
|
|
|
#define TAR_MAGIC "ustar"
|
|
|
|
#define TAR_VERSION "00"
|
|
|
|
|
|
|
|
#define TAR_TYPE_REGULAR '0'
|
|
|
|
#define TAR_TYPE_HARD_LINK '1'
|
|
|
|
#define TAR_TYPE_SOFT_LINK '2'
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Static data.
|
|
|
|
*/
|
1999-10-09 05:55:00 +05:30
|
|
|
static int listFlag;
|
|
|
|
static int extractFlag;
|
|
|
|
static int createFlag;
|
|
|
|
static int verboseFlag;
|
|
|
|
static int tostdoutFlag;
|
|
|
|
|
|
|
|
static int inHeader;
|
|
|
|
static int badHeader;
|
|
|
|
static int errorFlag;
|
|
|
|
static int skipFileFlag;
|
|
|
|
static int warnedRoot;
|
|
|
|
static int eofFlag;
|
1999-10-05 21:54:54 +05:30
|
|
|
static long dataCc;
|
|
|
|
static int outFd;
|
|
|
|
static char outName[TAR_NAME_SIZE];
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Static data associated with the tar file.
|
|
|
|
*/
|
|
|
|
static const char * tarName;
|
|
|
|
static int tarFd;
|
|
|
|
static dev_t tarDev;
|
|
|
|
static ino_t tarInode;
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Local procedures to restore files from a tar file.
|
|
|
|
*/
|
|
|
|
static void readTarFile(int fileCount, char ** fileTable);
|
|
|
|
static void readData(const char * cp, int count);
|
|
|
|
static void createPath(const char * name, int mode);
|
|
|
|
static long getOctal(const char * cp, int len);
|
|
|
|
|
|
|
|
static void readHeader(const TarHeader * hp,
|
|
|
|
int fileCount, char ** fileTable);
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Local procedures to save files into a tar file.
|
|
|
|
*/
|
1999-10-09 05:55:00 +05:30
|
|
|
static void saveFile(const char * fileName, int seeLinks);
|
1999-10-05 21:54:54 +05:30
|
|
|
|
|
|
|
static void saveRegularFile(const char * fileName,
|
|
|
|
const struct stat * statbuf);
|
|
|
|
|
|
|
|
static void saveDirectory(const char * fileName,
|
|
|
|
const struct stat * statbuf);
|
|
|
|
|
1999-10-09 05:55:00 +05:30
|
|
|
static int wantFileName(const char * fileName,
|
1999-10-05 21:54:54 +05:30
|
|
|
int fileCount, char ** fileTable);
|
|
|
|
|
|
|
|
static void writeHeader(const char * fileName,
|
|
|
|
const struct stat * statbuf);
|
|
|
|
|
|
|
|
static void writeTarFile(int fileCount, char ** fileTable);
|
|
|
|
static void writeTarBlock(const char * buf, int len);
|
1999-10-09 05:55:00 +05:30
|
|
|
static int putOctal(char * cp, int len, long value);
|
1999-10-05 21:54:54 +05:30
|
|
|
|
|
|
|
|
|
|
|
extern int
|
1999-10-07 01:55:32 +05:30
|
|
|
tar_main(int argc, char ** argv)
|
1999-10-05 21:54:54 +05:30
|
|
|
{
|
|
|
|
const char * options;
|
|
|
|
|
|
|
|
argc--;
|
|
|
|
argv++;
|
|
|
|
|
|
|
|
if (argc < 1)
|
|
|
|
{
|
|
|
|
fprintf(stderr, "%s", tar_usage);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
errorFlag = FALSE;
|
|
|
|
extractFlag = FALSE;
|
|
|
|
createFlag = FALSE;
|
|
|
|
listFlag = FALSE;
|
|
|
|
verboseFlag = FALSE;
|
|
|
|
tostdoutFlag = FALSE;
|
|
|
|
tarName = NULL;
|
|
|
|
tarDev = 0;
|
|
|
|
tarInode = 0;
|
|
|
|
tarFd = -1;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Parse the options.
|
|
|
|
*/
|
|
|
|
options = *argv++;
|
|
|
|
argc--;
|
|
|
|
|
|
|
|
if (**argv == '-') {
|
|
|
|
for (; *options; options++)
|
|
|
|
{
|
|
|
|
switch (*options)
|
|
|
|
{
|
|
|
|
case 'f':
|
|
|
|
if (tarName != NULL)
|
|
|
|
{
|
|
|
|
fprintf(stderr, "Only one 'f' option allowed\n");
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
tarName = *argv++;
|
|
|
|
argc--;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 't':
|
|
|
|
listFlag = TRUE;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'x':
|
|
|
|
extractFlag = TRUE;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'c':
|
|
|
|
createFlag = TRUE;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'v':
|
|
|
|
verboseFlag = TRUE;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'O':
|
|
|
|
tostdoutFlag = TRUE;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case '-':
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
fprintf(stderr, "Unknown tar flag '%c'\n", *options);
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Validate the options.
|
|
|
|
*/
|
|
|
|
if (extractFlag + listFlag + createFlag != 1)
|
|
|
|
{
|
|
|
|
fprintf(stderr, "Exactly one of 'c', 'x' or 't' must be specified\n");
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Do the correct type of action supplying the rest of the
|
|
|
|
* command line arguments as the list of files to process.
|
|
|
|
*/
|
|
|
|
if (createFlag)
|
|
|
|
writeTarFile(argc, argv);
|
|
|
|
else
|
|
|
|
readTarFile(argc, argv);
|
|
|
|
if (errorFlag)
|
|
|
|
fprintf(stderr, "\n");
|
|
|
|
return( errorFlag);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Read a tar file and extract or list the specified files within it.
|
|
|
|
* If the list is empty than all files are extracted or listed.
|
|
|
|
*/
|
|
|
|
static void
|
|
|
|
readTarFile(int fileCount, char ** fileTable)
|
|
|
|
{
|
|
|
|
const char * cp;
|
|
|
|
int cc;
|
|
|
|
int inCc;
|
|
|
|
int blockSize;
|
|
|
|
char buf[BUF_SIZE];
|
|
|
|
|
|
|
|
skipFileFlag = FALSE;
|
|
|
|
badHeader = FALSE;
|
|
|
|
warnedRoot = FALSE;
|
|
|
|
eofFlag = FALSE;
|
|
|
|
inHeader = TRUE;
|
|
|
|
inCc = 0;
|
|
|
|
dataCc = 0;
|
|
|
|
outFd = -1;
|
|
|
|
blockSize = sizeof(buf);
|
|
|
|
cp = buf;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Open the tar file for reading.
|
|
|
|
*/
|
|
|
|
if ( (tarName==NULL) || !strcmp( tarName, "-") ) {
|
|
|
|
tarFd = STDIN;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
tarFd = open(tarName, O_RDONLY);
|
|
|
|
|
|
|
|
if (tarFd < 0)
|
|
|
|
{
|
|
|
|
perror(tarName);
|
|
|
|
errorFlag = TRUE;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Read blocks from the file until an end of file header block
|
|
|
|
* has been seen. (A real end of file from a read is an error.)
|
|
|
|
*/
|
|
|
|
while (!eofFlag)
|
|
|
|
{
|
|
|
|
/*
|
|
|
|
* Read the next block of data if necessary.
|
|
|
|
* This will be a large block if possible, which we will
|
|
|
|
* then process in the small tar blocks.
|
|
|
|
*/
|
|
|
|
if (inCc <= 0)
|
|
|
|
{
|
|
|
|
cp = buf;
|
|
|
|
inCc = fullRead(tarFd, buf, blockSize);
|
|
|
|
|
|
|
|
if (inCc < 0)
|
|
|
|
{
|
|
|
|
perror(tarName);
|
|
|
|
errorFlag=TRUE;
|
|
|
|
goto done;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inCc == 0)
|
|
|
|
{
|
|
|
|
fprintf(stderr,
|
|
|
|
"Unexpected end of file from \"%s\"",
|
|
|
|
tarName);
|
|
|
|
errorFlag=TRUE;
|
|
|
|
goto done;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If we are expecting a header block then examine it.
|
|
|
|
*/
|
|
|
|
if (inHeader)
|
|
|
|
{
|
|
|
|
readHeader((const TarHeader *) cp, fileCount, fileTable);
|
|
|
|
|
|
|
|
cp += TAR_BLOCK_SIZE;
|
|
|
|
inCc -= TAR_BLOCK_SIZE;
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* We are currently handling the data for a file.
|
|
|
|
* Process the minimum of the amount of data we have available
|
|
|
|
* and the amount left to be processed for the file.
|
|
|
|
*/
|
|
|
|
cc = inCc;
|
|
|
|
|
|
|
|
if (cc > dataCc)
|
|
|
|
cc = dataCc;
|
|
|
|
|
|
|
|
readData(cp, cc);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If the amount left isn't an exact multiple of the tar block
|
|
|
|
* size then round it up to the next block boundary since there
|
|
|
|
* is padding at the end of the file.
|
|
|
|
*/
|
|
|
|
if (cc % TAR_BLOCK_SIZE)
|
|
|
|
cc += TAR_BLOCK_SIZE - (cc % TAR_BLOCK_SIZE);
|
|
|
|
|
|
|
|
cp += cc;
|
|
|
|
inCc -= cc;
|
|
|
|
}
|
|
|
|
|
|
|
|
done:
|
|
|
|
/*
|
|
|
|
* Close the tar file if needed.
|
|
|
|
*/
|
|
|
|
if ((tarFd >= 0) && (close(tarFd) < 0))
|
|
|
|
perror(tarName);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Close the output file if needed.
|
|
|
|
* This is only done here on a previous error and so no
|
|
|
|
* message is required on errors.
|
|
|
|
*/
|
|
|
|
if (tostdoutFlag==FALSE) {
|
|
|
|
if (outFd >= 0)
|
|
|
|
(void) close(outFd);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Examine the header block that was just read.
|
|
|
|
* This can specify the information for another file, or it can mark
|
|
|
|
* the end of the tar file.
|
|
|
|
*/
|
|
|
|
static void
|
|
|
|
readHeader(const TarHeader * hp, int fileCount, char ** fileTable)
|
|
|
|
{
|
|
|
|
int mode;
|
|
|
|
int uid;
|
|
|
|
int gid;
|
|
|
|
int checkSum;
|
|
|
|
long size;
|
|
|
|
time_t mtime;
|
|
|
|
const char * name;
|
|
|
|
int cc;
|
1999-10-09 05:55:00 +05:30
|
|
|
int hardLink;
|
|
|
|
int softLink;
|
1999-10-05 21:54:54 +05:30
|
|
|
|
|
|
|
/*
|
|
|
|
* If the block is completely empty, then this is the end of the
|
|
|
|
* archive file. If the name is null, then just skip this header.
|
|
|
|
*/
|
|
|
|
name = hp->name;
|
|
|
|
|
|
|
|
if (*name == '\0')
|
|
|
|
{
|
|
|
|
for (cc = TAR_BLOCK_SIZE; cc > 0; cc--)
|
|
|
|
{
|
|
|
|
if (*name++)
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
eofFlag = TRUE;
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* There is another file in the archive to examine.
|
|
|
|
* Extract the encoded information and check it.
|
|
|
|
*/
|
|
|
|
mode = getOctal(hp->mode, sizeof(hp->mode));
|
|
|
|
uid = getOctal(hp->uid, sizeof(hp->uid));
|
|
|
|
gid = getOctal(hp->gid, sizeof(hp->gid));
|
|
|
|
size = getOctal(hp->size, sizeof(hp->size));
|
|
|
|
mtime = getOctal(hp->mtime, sizeof(hp->mtime));
|
|
|
|
checkSum = getOctal(hp->checkSum, sizeof(hp->checkSum));
|
|
|
|
|
|
|
|
if ((mode < 0) || (uid < 0) || (gid < 0) || (size < 0))
|
|
|
|
{
|
|
|
|
if (!badHeader)
|
|
|
|
fprintf(stderr, "Bad tar header, skipping\n");
|
|
|
|
|
|
|
|
badHeader = TRUE;
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
badHeader = FALSE;
|
|
|
|
skipFileFlag = FALSE;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Check for the file modes.
|
|
|
|
*/
|
|
|
|
hardLink = ((hp->typeFlag == TAR_TYPE_HARD_LINK) ||
|
|
|
|
(hp->typeFlag == TAR_TYPE_HARD_LINK - '0'));
|
|
|
|
|
|
|
|
softLink = ((hp->typeFlag == TAR_TYPE_SOFT_LINK) ||
|
|
|
|
(hp->typeFlag == TAR_TYPE_SOFT_LINK - '0'));
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Check for a directory or a regular file.
|
|
|
|
*/
|
|
|
|
if (name[strlen(name) - 1] == '/')
|
|
|
|
mode |= S_IFDIR;
|
|
|
|
else if ((mode & S_IFMT) == 0)
|
|
|
|
mode |= S_IFREG;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Check for absolute paths in the file.
|
|
|
|
* If we find any, then warn the user and make them relative.
|
|
|
|
*/
|
|
|
|
if (*name == '/')
|
|
|
|
{
|
|
|
|
while (*name == '/')
|
|
|
|
name++;
|
|
|
|
|
|
|
|
if (!warnedRoot)
|
|
|
|
{
|
|
|
|
fprintf(stderr,
|
|
|
|
"Absolute path detected, removing leading slashes\n");
|
|
|
|
}
|
|
|
|
|
|
|
|
warnedRoot = TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* See if we want this file to be restored.
|
|
|
|
* If not, then set up to skip it.
|
|
|
|
*/
|
|
|
|
if (!wantFileName(name, fileCount, fileTable))
|
|
|
|
{
|
|
|
|
if (!hardLink && !softLink && S_ISREG(mode))
|
|
|
|
{
|
|
|
|
inHeader = (size == 0);
|
|
|
|
dataCc = size;
|
|
|
|
}
|
|
|
|
|
|
|
|
skipFileFlag = TRUE;
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* This file is to be handled.
|
|
|
|
* If we aren't extracting then just list information about the file.
|
|
|
|
*/
|
|
|
|
if (!extractFlag)
|
|
|
|
{
|
|
|
|
if (verboseFlag)
|
|
|
|
{
|
|
|
|
printf("%s %3d/%-d %9ld %s %s", modeString(mode),
|
|
|
|
uid, gid, size, timeString(mtime), name);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
printf("%s", name);
|
|
|
|
|
|
|
|
if (hardLink)
|
|
|
|
printf(" (link to \"%s\")", hp->linkName);
|
|
|
|
else if (softLink)
|
|
|
|
printf(" (symlink to \"%s\")", hp->linkName);
|
|
|
|
else if (S_ISREG(mode))
|
|
|
|
{
|
|
|
|
inHeader = (size == 0);
|
|
|
|
dataCc = size;
|
|
|
|
}
|
|
|
|
|
|
|
|
printf("\n");
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* We really want to extract the file.
|
|
|
|
*/
|
|
|
|
if (verboseFlag)
|
|
|
|
printf("x %s\n", name);
|
|
|
|
|
|
|
|
if (hardLink)
|
|
|
|
{
|
|
|
|
if (link(hp->linkName, name) < 0)
|
|
|
|
perror(name);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (softLink)
|
|
|
|
{
|
|
|
|
#ifdef S_ISLNK
|
|
|
|
if (symlink(hp->linkName, name) < 0)
|
|
|
|
perror(name);
|
|
|
|
#else
|
|
|
|
fprintf(stderr, "Cannot create symbolic links\n");
|
|
|
|
#endif
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If the file is a directory, then just create the path.
|
|
|
|
*/
|
|
|
|
if (S_ISDIR(mode))
|
|
|
|
{
|
|
|
|
createPath(name, mode);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* There is a file to write.
|
|
|
|
* First create the path to it if necessary with a default permission.
|
|
|
|
*/
|
|
|
|
createPath(name, 0777);
|
|
|
|
|
|
|
|
inHeader = (size == 0);
|
|
|
|
dataCc = size;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Start the output file.
|
|
|
|
*/
|
|
|
|
if (tostdoutFlag==TRUE)
|
|
|
|
outFd = STDOUT;
|
|
|
|
else
|
|
|
|
outFd = open(name, O_WRONLY | O_CREAT | O_TRUNC, mode);
|
|
|
|
|
|
|
|
if (outFd < 0)
|
|
|
|
{
|
|
|
|
perror(name);
|
|
|
|
skipFileFlag = TRUE;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If the file is empty, then that's all we need to do.
|
|
|
|
*/
|
|
|
|
if (size == 0 && tostdoutFlag == FALSE)
|
|
|
|
{
|
|
|
|
(void) close(outFd);
|
|
|
|
outFd = -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Handle a data block of some specified size that was read.
|
|
|
|
*/
|
|
|
|
static void
|
|
|
|
readData(const char * cp, int count)
|
|
|
|
{
|
|
|
|
/*
|
|
|
|
* Reduce the amount of data left in this file.
|
|
|
|
* If there is no more data left, then we need to read
|
|
|
|
* the header again.
|
|
|
|
*/
|
|
|
|
dataCc -= count;
|
|
|
|
|
|
|
|
if (dataCc <= 0)
|
|
|
|
inHeader = TRUE;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If we aren't extracting files or this file is being
|
|
|
|
* skipped then do nothing more.
|
|
|
|
*/
|
|
|
|
if (!extractFlag || skipFileFlag)
|
|
|
|
return;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Write the data to the output file.
|
|
|
|
*/
|
|
|
|
if (fullWrite(outFd, cp, count) < 0)
|
|
|
|
{
|
|
|
|
perror(outName);
|
|
|
|
if (tostdoutFlag==FALSE) {
|
|
|
|
(void) close(outFd);
|
|
|
|
outFd = -1;
|
|
|
|
}
|
|
|
|
skipFileFlag = TRUE;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If the write failed, close the file and disable further
|
|
|
|
* writes to this file.
|
|
|
|
*/
|
|
|
|
if (dataCc <= 0 && tostdoutFlag==FALSE)
|
|
|
|
{
|
|
|
|
if (close(outFd))
|
|
|
|
perror(outName);
|
|
|
|
|
|
|
|
outFd = -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Write a tar file containing the specified files.
|
|
|
|
*/
|
|
|
|
static void
|
|
|
|
writeTarFile(int fileCount, char ** fileTable)
|
|
|
|
{
|
|
|
|
struct stat statbuf;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Make sure there is at least one file specified.
|
|
|
|
*/
|
|
|
|
if (fileCount <= 0)
|
|
|
|
{
|
|
|
|
fprintf(stderr, "No files specified to be saved\n");
|
|
|
|
errorFlag=TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Create the tar file for writing.
|
|
|
|
*/
|
|
|
|
if ( (tarName==NULL) || !strcmp( tarName, "-") ) {
|
|
|
|
tostdoutFlag = TRUE;
|
|
|
|
tarFd = STDOUT;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
tarFd = open(tarName, O_WRONLY | O_CREAT | O_TRUNC, 0666);
|
|
|
|
|
|
|
|
if (tarFd < 0)
|
|
|
|
{
|
|
|
|
perror(tarName);
|
|
|
|
errorFlag=TRUE;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Get the device and inode of the tar file for checking later.
|
|
|
|
*/
|
|
|
|
if (fstat(tarFd, &statbuf) < 0)
|
|
|
|
{
|
|
|
|
perror(tarName);
|
|
|
|
errorFlag = TRUE;
|
|
|
|
goto done;
|
|
|
|
}
|
|
|
|
|
|
|
|
tarDev = statbuf.st_dev;
|
|
|
|
tarInode = statbuf.st_ino;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Append each file name into the archive file.
|
|
|
|
* Follow symbolic links for these top level file names.
|
|
|
|
*/
|
|
|
|
while (!errorFlag && (fileCount-- > 0))
|
|
|
|
{
|
|
|
|
saveFile(*fileTable++, FALSE);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Now write an empty block of zeroes to end the archive.
|
|
|
|
*/
|
|
|
|
writeTarBlock("", 1);
|
|
|
|
|
|
|
|
|
|
|
|
done:
|
|
|
|
/*
|
|
|
|
* Close the tar file and check for errors if it was opened.
|
|
|
|
*/
|
|
|
|
if ( (tostdoutFlag==FALSE) && (tarFd >= 0) && (close(tarFd) < 0))
|
|
|
|
perror(tarName);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Save one file into the tar file.
|
|
|
|
* If the file is a directory, then this will recursively save all of
|
|
|
|
* the files and directories within the directory. The seeLinks
|
|
|
|
* flag indicates whether or not we want to see symbolic links as
|
|
|
|
* they really are, instead of blindly following them.
|
|
|
|
*/
|
|
|
|
static void
|
1999-10-09 05:55:00 +05:30
|
|
|
saveFile(const char * fileName, int seeLinks)
|
1999-10-05 21:54:54 +05:30
|
|
|
{
|
|
|
|
int status;
|
|
|
|
int mode;
|
|
|
|
struct stat statbuf;
|
|
|
|
|
|
|
|
if (verboseFlag)
|
|
|
|
printf("a %s\n", fileName);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Check that the file name will fit in the header.
|
|
|
|
*/
|
|
|
|
if (strlen(fileName) >= TAR_NAME_SIZE)
|
|
|
|
{
|
|
|
|
fprintf(stderr, "%s: File name is too long\n", fileName);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Find out about the file.
|
|
|
|
*/
|
|
|
|
#ifdef S_ISLNK
|
|
|
|
if (seeLinks)
|
|
|
|
status = lstat(fileName, &statbuf);
|
|
|
|
else
|
|
|
|
#endif
|
|
|
|
status = stat(fileName, &statbuf);
|
|
|
|
|
|
|
|
if (status < 0)
|
|
|
|
{
|
|
|
|
perror(fileName);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Make sure we aren't trying to save our file into itself.
|
|
|
|
*/
|
|
|
|
if ((statbuf.st_dev == tarDev) && (statbuf.st_ino == tarInode))
|
|
|
|
{
|
|
|
|
fprintf(stderr, "Skipping saving of archive file itself\n");
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Check the type of file.
|
|
|
|
*/
|
|
|
|
mode = statbuf.st_mode;
|
|
|
|
|
|
|
|
if (S_ISDIR(mode))
|
|
|
|
{
|
|
|
|
saveDirectory(fileName, &statbuf);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (S_ISREG(mode))
|
|
|
|
{
|
|
|
|
saveRegularFile(fileName, &statbuf);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* The file is a strange type of file, ignore it.
|
|
|
|
*/
|
|
|
|
fprintf(stderr, "%s: not a directory or regular file\n", fileName);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Save a regular file to the tar file.
|
|
|
|
*/
|
|
|
|
static void
|
|
|
|
saveRegularFile(const char * fileName, const struct stat * statbuf)
|
|
|
|
{
|
1999-10-09 05:55:00 +05:30
|
|
|
int sawEof;
|
1999-10-05 21:54:54 +05:30
|
|
|
int fileFd;
|
|
|
|
int cc;
|
|
|
|
int dataCount;
|
|
|
|
long fullDataCount;
|
|
|
|
char data[TAR_BLOCK_SIZE * 16];
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Open the file for reading.
|
|
|
|
*/
|
|
|
|
fileFd = open(fileName, O_RDONLY);
|
|
|
|
|
|
|
|
if (fileFd < 0)
|
|
|
|
{
|
|
|
|
perror(fileName);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Write out the header for the file.
|
|
|
|
*/
|
|
|
|
writeHeader(fileName, statbuf);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Write the data blocks of the file.
|
|
|
|
* We must be careful to write the amount of data that the stat
|
|
|
|
* buffer indicated, even if the file has changed size. Otherwise
|
|
|
|
* the tar file will be incorrect.
|
|
|
|
*/
|
|
|
|
fullDataCount = statbuf->st_size;
|
|
|
|
sawEof = FALSE;
|
|
|
|
|
|
|
|
while (fullDataCount > 0)
|
|
|
|
{
|
|
|
|
/*
|
|
|
|
* Get the amount to write this iteration which is
|
|
|
|
* the minumum of the amount left to write and the
|
|
|
|
* buffer size.
|
|
|
|
*/
|
|
|
|
dataCount = sizeof(data);
|
|
|
|
|
|
|
|
if (dataCount > fullDataCount)
|
|
|
|
dataCount = (int) fullDataCount;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Read the data from the file if we haven't seen the
|
|
|
|
* end of file yet.
|
|
|
|
*/
|
|
|
|
cc = 0;
|
|
|
|
|
|
|
|
if (!sawEof)
|
|
|
|
{
|
|
|
|
cc = fullRead(fileFd, data, dataCount);
|
|
|
|
|
|
|
|
if (cc < 0)
|
|
|
|
{
|
|
|
|
perror(fileName);
|
|
|
|
|
|
|
|
(void) close(fileFd);
|
|
|
|
errorFlag = TRUE;
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If the file ended too soon, complain and set
|
|
|
|
* a flag so we will zero fill the rest of it.
|
|
|
|
*/
|
|
|
|
if (cc < dataCount)
|
|
|
|
{
|
|
|
|
fprintf(stderr,
|
|
|
|
"%s: Short read - zero filling",
|
|
|
|
fileName);
|
|
|
|
|
|
|
|
sawEof = TRUE;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Zero fill the rest of the data if necessary.
|
|
|
|
*/
|
|
|
|
if (cc < dataCount)
|
|
|
|
memset(data + cc, 0, dataCount - cc);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Write the buffer to the TAR file.
|
|
|
|
*/
|
|
|
|
writeTarBlock(data, dataCount);
|
|
|
|
|
|
|
|
fullDataCount -= dataCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Close the file.
|
|
|
|
*/
|
|
|
|
if ( (tostdoutFlag==FALSE) && close(fileFd) < 0)
|
|
|
|
fprintf(stderr, "%s: close: %s\n", fileName, strerror(errno));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Save a directory and all of its files to the tar file.
|
|
|
|
*/
|
|
|
|
static void
|
|
|
|
saveDirectory(const char * dirName, const struct stat * statbuf)
|
|
|
|
{
|
|
|
|
DIR * dir;
|
|
|
|
struct dirent * entry;
|
1999-10-09 05:55:00 +05:30
|
|
|
int needSlash;
|
1999-10-05 21:54:54 +05:30
|
|
|
char fullName[PATH_LEN];
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Construct the directory name as used in the tar file by appending
|
|
|
|
* a slash character to it.
|
|
|
|
*/
|
|
|
|
strcpy(fullName, dirName);
|
|
|
|
strcat(fullName, "/");
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Write out the header for the directory entry.
|
|
|
|
*/
|
|
|
|
writeHeader(fullName, statbuf);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Open the directory.
|
|
|
|
*/
|
|
|
|
dir = opendir(dirName);
|
|
|
|
|
|
|
|
if (dir == NULL)
|
|
|
|
{
|
|
|
|
fprintf(stderr, "Cannot read directory \"%s\": %s\n",
|
|
|
|
dirName, strerror(errno));
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* See if a slash is needed.
|
|
|
|
*/
|
|
|
|
needSlash = (*dirName && (dirName[strlen(dirName) - 1] != '/'));
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Read all of the directory entries and check them,
|
|
|
|
* except for the current and parent directory entries.
|
|
|
|
*/
|
|
|
|
while (!errorFlag && ((entry = readdir(dir)) != NULL))
|
|
|
|
{
|
|
|
|
if ((strcmp(entry->d_name, ".") == 0) ||
|
|
|
|
(strcmp(entry->d_name, "..") == 0))
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Build the full path name to the file.
|
|
|
|
*/
|
|
|
|
strcpy(fullName, dirName);
|
|
|
|
|
|
|
|
if (needSlash)
|
|
|
|
strcat(fullName, "/");
|
|
|
|
|
|
|
|
strcat(fullName, entry->d_name);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Write this file to the tar file, noticing whether or not
|
|
|
|
* the file is a symbolic link.
|
|
|
|
*/
|
|
|
|
saveFile(fullName, TRUE);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* All done, close the directory.
|
|
|
|
*/
|
|
|
|
closedir(dir);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Write a tar header for the specified file name and status.
|
|
|
|
* It is assumed that the file name fits.
|
|
|
|
*/
|
|
|
|
static void
|
|
|
|
writeHeader(const char * fileName, const struct stat * statbuf)
|
|
|
|
{
|
|
|
|
long checkSum;
|
|
|
|
const unsigned char * cp;
|
|
|
|
int len;
|
|
|
|
TarHeader header;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Zero the header block in preparation for filling it in.
|
|
|
|
*/
|
|
|
|
memset((char *) &header, 0, sizeof(header));
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Fill in the header.
|
|
|
|
*/
|
|
|
|
strcpy(header.name, fileName);
|
|
|
|
|
|
|
|
strncpy(header.magic, TAR_MAGIC, sizeof(header.magic));
|
|
|
|
strncpy(header.version, TAR_VERSION, sizeof(header.version));
|
|
|
|
|
|
|
|
putOctal(header.mode, sizeof(header.mode), statbuf->st_mode & 0777);
|
|
|
|
putOctal(header.uid, sizeof(header.uid), statbuf->st_uid);
|
|
|
|
putOctal(header.gid, sizeof(header.gid), statbuf->st_gid);
|
|
|
|
putOctal(header.size, sizeof(header.size), statbuf->st_size);
|
|
|
|
putOctal(header.mtime, sizeof(header.mtime), statbuf->st_mtime);
|
|
|
|
|
|
|
|
header.typeFlag = TAR_TYPE_REGULAR;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Calculate and store the checksum.
|
|
|
|
* This is the sum of all of the bytes of the header,
|
|
|
|
* with the checksum field itself treated as blanks.
|
|
|
|
*/
|
|
|
|
memset(header.checkSum, ' ', sizeof(header.checkSum));
|
|
|
|
|
|
|
|
cp = (const unsigned char *) &header;
|
|
|
|
len = sizeof(header);
|
|
|
|
checkSum = 0;
|
|
|
|
|
|
|
|
while (len-- > 0)
|
|
|
|
checkSum += *cp++;
|
|
|
|
|
|
|
|
putOctal(header.checkSum, sizeof(header.checkSum), checkSum);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Write the tar header.
|
|
|
|
*/
|
|
|
|
writeTarBlock((const char *) &header, sizeof(header));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Write data to one or more blocks of the tar file.
|
|
|
|
* The data is always padded out to a multiple of TAR_BLOCK_SIZE.
|
|
|
|
* The errorFlag static variable is set on an error.
|
|
|
|
*/
|
|
|
|
static void
|
|
|
|
writeTarBlock(const char * buf, int len)
|
|
|
|
{
|
|
|
|
int partialLength;
|
|
|
|
int completeLength;
|
|
|
|
char fullBlock[TAR_BLOCK_SIZE];
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If we had a write error before, then do nothing more.
|
|
|
|
*/
|
|
|
|
if (errorFlag)
|
|
|
|
return;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Get the amount of complete and partial blocks.
|
|
|
|
*/
|
|
|
|
partialLength = len % TAR_BLOCK_SIZE;
|
|
|
|
completeLength = len - partialLength;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Write all of the complete blocks.
|
|
|
|
*/
|
|
|
|
if ((completeLength > 0) && !fullWrite(tarFd, buf, completeLength))
|
|
|
|
{
|
|
|
|
perror(tarName);
|
|
|
|
|
|
|
|
errorFlag = TRUE;
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If there are no partial blocks left, we are done.
|
|
|
|
*/
|
|
|
|
if (partialLength == 0)
|
|
|
|
return;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Copy the partial data into a complete block, and pad the rest
|
|
|
|
* of it with zeroes.
|
|
|
|
*/
|
|
|
|
memcpy(fullBlock, buf + completeLength, partialLength);
|
|
|
|
memset(fullBlock + partialLength, 0, TAR_BLOCK_SIZE - partialLength);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Write the last complete block.
|
|
|
|
*/
|
|
|
|
if (!fullWrite(tarFd, fullBlock, TAR_BLOCK_SIZE))
|
|
|
|
{
|
|
|
|
perror(tarName);
|
|
|
|
|
|
|
|
errorFlag = TRUE;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Attempt to create the directories along the specified path, except for
|
|
|
|
* the final component. The mode is given for the final directory only,
|
|
|
|
* while all previous ones get default protections. Errors are not reported
|
|
|
|
* here, as failures to restore files can be reported later.
|
|
|
|
*/
|
|
|
|
static void
|
|
|
|
createPath(const char * name, int mode)
|
|
|
|
{
|
|
|
|
char * cp;
|
|
|
|
char * cpOld;
|
|
|
|
char buf[TAR_NAME_SIZE];
|
|
|
|
|
|
|
|
strcpy(buf, name);
|
|
|
|
|
|
|
|
cp = strchr(buf, '/');
|
|
|
|
|
|
|
|
while (cp)
|
|
|
|
{
|
|
|
|
cpOld = cp;
|
|
|
|
cp = strchr(cp + 1, '/');
|
|
|
|
|
|
|
|
*cpOld = '\0';
|
|
|
|
|
|
|
|
if (mkdir(buf, cp ? 0777 : mode) == 0)
|
|
|
|
printf("Directory \"%s\" created\n", buf);
|
|
|
|
|
|
|
|
*cpOld = '/';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Read an octal value in a field of the specified width, with optional
|
|
|
|
* spaces on both sides of the number and with an optional null character
|
|
|
|
* at the end. Returns -1 on an illegal format.
|
|
|
|
*/
|
|
|
|
static long
|
|
|
|
getOctal(const char * cp, int len)
|
|
|
|
{
|
|
|
|
long val;
|
|
|
|
|
|
|
|
while ((len > 0) && (*cp == ' '))
|
|
|
|
{
|
|
|
|
cp++;
|
|
|
|
len--;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((len == 0) || !isOctal(*cp))
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
val = 0;
|
|
|
|
|
|
|
|
while ((len > 0) && isOctal(*cp))
|
|
|
|
{
|
|
|
|
val = val * 8 + *cp++ - '0';
|
|
|
|
len--;
|
|
|
|
}
|
|
|
|
|
|
|
|
while ((len > 0) && (*cp == ' '))
|
|
|
|
{
|
|
|
|
cp++;
|
|
|
|
len--;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((len > 0) && *cp)
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
return val;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Put an octal string into the specified buffer.
|
|
|
|
* The number is zero and space padded and possibly null padded.
|
|
|
|
* Returns TRUE if successful.
|
|
|
|
*/
|
1999-10-09 05:55:00 +05:30
|
|
|
static int
|
1999-10-05 21:54:54 +05:30
|
|
|
putOctal(char * cp, int len, long value)
|
|
|
|
{
|
|
|
|
int tempLength;
|
|
|
|
char * tempString;
|
|
|
|
char tempBuffer[32];
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Create a string of the specified length with an initial space,
|
|
|
|
* leading zeroes and the octal number, and a trailing null.
|
|
|
|
*/
|
|
|
|
tempString = tempBuffer;
|
|
|
|
|
|
|
|
sprintf(tempString, " %0*lo", len - 2, value);
|
|
|
|
|
|
|
|
tempLength = strlen(tempString) + 1;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If the string is too large, suppress the leading space.
|
|
|
|
*/
|
|
|
|
if (tempLength > len)
|
|
|
|
{
|
|
|
|
tempLength--;
|
|
|
|
tempString++;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If the string is still too large, suppress the trailing null.
|
|
|
|
*/
|
|
|
|
if (tempLength > len)
|
|
|
|
tempLength--;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If the string is still too large, fail.
|
|
|
|
*/
|
|
|
|
if (tempLength > len)
|
|
|
|
return FALSE;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Copy the string to the field.
|
|
|
|
*/
|
|
|
|
memcpy(cp, tempString, len);
|
|
|
|
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* See if the specified file name belongs to one of the specified list
|
|
|
|
* of path prefixes. An empty list implies that all files are wanted.
|
|
|
|
* Returns TRUE if the file is selected.
|
|
|
|
*/
|
1999-10-09 05:55:00 +05:30
|
|
|
static int
|
1999-10-05 21:54:54 +05:30
|
|
|
wantFileName(const char * fileName, int fileCount, char ** fileTable)
|
|
|
|
{
|
|
|
|
const char * pathName;
|
|
|
|
int fileLength;
|
|
|
|
int pathLength;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If there are no files in the list, then the file is wanted.
|
|
|
|
*/
|
|
|
|
if (fileCount == 0)
|
|
|
|
return TRUE;
|
|
|
|
|
|
|
|
fileLength = strlen(fileName);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Check each of the test paths.
|
|
|
|
*/
|
|
|
|
while (fileCount-- > 0)
|
|
|
|
{
|
|
|
|
pathName = *fileTable++;
|
|
|
|
|
|
|
|
pathLength = strlen(pathName);
|
|
|
|
|
|
|
|
if (fileLength < pathLength)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if (memcmp(fileName, pathName, pathLength) != 0)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if ((fileLength == pathLength) ||
|
|
|
|
(fileName[pathLength] == '/'))
|
|
|
|
{
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#endif
|
|
|
|
/* END CODE */
|
|
|
|
|
|
|
|
|