1107 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			C
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			1107 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			C
		
	
	
		
			Executable File
		
	
	
	
	
| /* tar - tape archiver			Author: Michiel Huisjes */
 | |
| 
 | |
| /* Usage: tar [cxt][vo][F][f] tapefile [files]
 | |
|  *
 | |
|  * attempt to make tar to conform to POSIX 1003.1
 | |
|  * disclaimer: based on an old (1986) POSIX draft.
 | |
|  * Klamer Schutte, 20/9/89
 | |
|  *
 | |
|  * Changes:
 | |
|  *  Changed to handle the original minix-tar format.	KS 22/9/89
 | |
|  *  Changed to handle BSD4.3 tar format.		KS 22/9/89
 | |
|  *  Conform to current umask if not super-user.		KS 22/9/89
 | |
|  *  Update usage message to show f option		KS 22/9/89
 | |
|  *
 | |
|  *
 | |
|  * 1)	tar will back itself up, should check archive inode num(&dev) and
 | |
|   then check the target inode number. In verbose mode, issue
 | |
|   warning, in all cases ignore target.
 | |
|   marks@mgse		Mon Sep 25 10:38:58 CDT 1989
 | |
|   	added global varaibles, made changes to main() and add_file();
 | |
|   maks@mgse Mon Sep 25 12:09:20 CDT 1989
 | |
| 
 | |
|    2)	tar will not notice that a file has changed size while it was being
 | |
|   backed up. should issue warning.
 | |
|   marks@mgse		Mon Sep 25 10:38:58 CDT 1989
 | |
| 
 | |
|    3)	the 'f' option was not documented in usage[].
 | |
|   marks@mgse		Mon Sep 25 12:03:20 CDT 1989
 | |
|   	changed both usage[] defines. Why are there two (one is commented out)?
 | |
|   	( deleted by me (was done twice) -- KS, 2/10/89 )
 | |
|  *
 | |
|  *  changed stat on tar_fd to an fstat				KS 2/10/89
 | |
|  *  deleted mkfifo() code -- belongs in libc.a			KS 2/10/89
 | |
|  *  made ar_dev default to -1 : an illegal device		KS 2/10/89
 | |
|  *  made impossible to chown if normal user			KS 2/10/89
 | |
|  *  if names in owner fields not known use numirical values	KS 2/10/89
 | |
|  *  creat with mask 666 -- use umask if to liberal		KS 2/10/89
 | |
|  *  allow to make directories as ../directory			KS 2/10/89
 | |
|  *  allow tmagic field to end with a space (instead of \0)	KS 2/10/89
 | |
|  *  correct usage of tmagic field 				KS 3/10/89
 | |
|  *  made mkdir() to return a value if directory == "."  	KS 3/10/89
 | |
|  *  made lint complains less (On a BSD 4.3 system)		KS 3/10/89
 | |
|  *  use of directory(3) routines				KS 3/10/89
 | |
|  *  deleted use of d_namlen selector of struct dirent		KS 18/10/89
 | |
|  *  support mknod4(2)						EC 7/7/90
 | |
|  *  forget inodes when link count expires			EC 6/4/91
 | |
|  *  don't remember directories *twice*!
 | |
|  *  added 'p' flag to ignore umask for normal user		KJB 6/10/92
 | |
|  *  mknod4(2) out						KJB 30/10/94
 | |
|  *  added 'D' flag to not recurse into directories		KJB 19/12/94
 | |
|  *  status output to stdout unless 'tar cvf -'			KJB 3/5/97
 | |
|  *
 | |
|  * Bugs:
 | |
|  *  verbose mode is not reporting consistent
 | |
|  *  code needs cleanup
 | |
|  *  prefix field is not used
 | |
|  *  timestamp of a directory will not be correct if there are files to be
 | |
|  *  unpacked in the directory
 | |
|  *	(add you favorite bug here (or two (or three (or ...))))
 | |
| */
 | |
| 
 | |
| #include <sys/types.h>
 | |
| #include <sys/stat.h>
 | |
| #include <fcntl.h>
 | |
| #include <pwd.h>
 | |
| #include <grp.h>
 | |
| #include <tar.h>
 | |
| #include <stdarg.h>
 | |
| #include <stdlib.h>
 | |
| #include <string.h>
 | |
| #include <unistd.h>
 | |
| #include <utime.h>
 | |
| #include <sys/wait.h>
 | |
| #include <stdio.h>		/* need NULL */
 | |
| #include <errno.h>
 | |
| 
 | |
| #define	POSIX_COMP		/* POSIX compatible */
 | |
| #define DIRECT_3		/* use directory(3) routines */
 | |
| 
 | |
| #ifdef DIRECT_3
 | |
| #ifndef BSD
 | |
| /* To all minix users: i am sorry, developed this piece of code on a
 | |
|  * BSD system. KS 18/10/89 */
 | |
| #include <dirent.h>
 | |
| #define	direct	dirent		/* stupid BSD non-POSIX compatible name! */
 | |
| #else				/* BSD */
 | |
| #include <sys/dir.h>
 | |
| #include <dir.h>
 | |
| #endif				/* BSD */
 | |
| #endif				/* DIRECT_3 */
 | |
| 
 | |
| #ifdef S_IFIFO
 | |
| #define	HAVE_FIFO		/* have incorporated Simon Pooles' changes */
 | |
| #endif
 | |
| #ifdef S_IFLNK
 | |
| #define HAVE_SYMLINK
 | |
| #endif
 | |
| 
 | |
| typedef char BOOL;
 | |
| #define TRUE	1
 | |
| #define FALSE	0
 | |
| 
 | |
| #define STRING_SIZE	256	/* string buffer size */
 | |
| #define HEADER_SIZE	TBLOCK
 | |
| #define NAME_SIZE	NAMSIZ
 | |
| /* #define BLOCK_BOUNDARY	 20 -- not in POSIX ! */
 | |
| 
 | |
| typedef union hblock HEADER;
 | |
| 
 | |
| /* Make the MINIX member names overlap to the POSIX names */
 | |
| #define	m_name		name
 | |
| #define m_mode		mode
 | |
| #define m_uid		uid
 | |
| #define m_gid		gid
 | |
| #define m_size		size
 | |
| #define	m_time		mtime
 | |
| #define	m_checksum	chksum
 | |
| #define	m_linked	typeflag
 | |
| #define	m_link		linkname
 | |
| #define	hdr_block	dummy
 | |
| #define	m		header
 | |
| #define	member		dbuf
 | |
| 
 | |
| #if 0				/* original structure -- see tar.h for new
 | |
| 			 * structure */
 | |
| typedef union {
 | |
|   char hdr_block[HEADER_SIZE];
 | |
|   struct m {
 | |
| 	char m_name[NAME_SIZE];
 | |
| 	char m_mode[8];
 | |
| 	char m_uid[8];
 | |
| 	char m_gid[8];
 | |
| 	char m_size[12];
 | |
| 	char m_time[12];
 | |
| 	char m_checksum[8];
 | |
| 	char m_linked;
 | |
| 	char m_link[NAME_SIZE];
 | |
|   } member;
 | |
| } HEADER;
 | |
| 
 | |
| #endif
 | |
| 
 | |
| /* Structure used to note links */
 | |
| struct link {
 | |
|   ino_t ino;
 | |
|   dev_t dev;
 | |
|   nlink_t nlink;
 | |
|   struct link *next;
 | |
|   char name[1];
 | |
| } *link_top = NULL;
 | |
| 
 | |
| HEADER header;
 | |
| 
 | |
| #define INT_TYPE	(sizeof(header.member.m_uid))
 | |
| #define LONG_TYPE	(sizeof(header.member.m_size))
 | |
| 
 | |
| #define NIL_HEADER	((HEADER *) 0)
 | |
| #define NIL_PTR		((char *) 0)
 | |
| #define TBLOCK_SIZE	TBLOCK
 | |
| 
 | |
| #define flush()		print(NIL_PTR)
 | |
| 
 | |
| BOOL show_fl, creat_fl, ext_fl;
 | |
| 
 | |
| int tar_fd;
 | |
| /* Char usage[] = "Usage: tar [cxt] tarfile [files]."; */
 | |
| char usage[] = "Usage: tar [cxt][vo][F][f] tarfile [files].";
 | |
| char io_buffer[TBLOCK_SIZE];
 | |
| char path[NAME_SIZE];
 | |
| char pathname[NAME_SIZE];
 | |
| int force_flag = 0;
 | |
| #ifdef ORIGINAL_DEFAULTS
 | |
| int chown_flag = 1;
 | |
| int verbose_flag = 1;
 | |
| #else
 | |
| int chown_flag = 0;
 | |
| int verbose_flag = 0;
 | |
| #endif
 | |
| int norec_flag = 0;
 | |
| 
 | |
| /* Make sure we don't tar ourselves. marks@mgse Mon Sep 25 12:06:28 CDT 1989 */
 | |
| ino_t ar_inode;			/* archive inode number	 */
 | |
| dev_t ar_dev;			/* archive device number */
 | |
| 
 | |
| int total_blocks;
 | |
| int u_mask;			/* one's complement of current umask */
 | |
| 
 | |
| #define block_size()	(int) ((convert(header.member.m_size, LONG_TYPE) \
 | |
|   + (long) TBLOCK_SIZE - 1) / (long) TBLOCK_SIZE)
 | |
| 
 | |
| _PROTOTYPE(int main, (int argc, char **argv));
 | |
| _PROTOTYPE(void error, (char *s1, char *s2));
 | |
| _PROTOTYPE(BOOL get_header, (void));
 | |
| _PROTOTYPE(void tarfile, (void));
 | |
| _PROTOTYPE(void skip_entry, (void));
 | |
| _PROTOTYPE(void extract, (char *file));
 | |
| _PROTOTYPE(void delete, (char *file));
 | |
| _PROTOTYPE(void do_chown, (char *file));
 | |
| _PROTOTYPE(void timestamp, (char *file));
 | |
| _PROTOTYPE(void copy, (char *file, int from, int to, long bytes));
 | |
| _PROTOTYPE(long convert, (char str[], int type));
 | |
| _PROTOTYPE(int checksum, (void));
 | |
| _PROTOTYPE(int is_dir, (char *file));
 | |
| _PROTOTYPE(char *path_name, (char *file));
 | |
| _PROTOTYPE(void add_path, (char *name));
 | |
| _PROTOTYPE(void add_file, (char *file));
 | |
| _PROTOTYPE(void verb_print, (char *s1, char *s2));
 | |
| _PROTOTYPE(void add_close, (int fd));
 | |
| _PROTOTYPE(int add_open, (char *file, struct stat * st));
 | |
| _PROTOTYPE(void make_header, (char *file, struct stat * st));
 | |
| _PROTOTYPE(void is_added, (struct stat * st, char *file));
 | |
| _PROTOTYPE(void is_deleted, (struct stat * st));
 | |
| _PROTOTYPE(char *is_linked, (struct stat * st));
 | |
| _PROTOTYPE(void clear_header, (void));
 | |
| _PROTOTYPE(void adjust_boundary, (void));
 | |
| _PROTOTYPE(void mread, (int fd, char *address, int bytes));
 | |
| _PROTOTYPE(void mwrite, (int fd, char *address, int bytes));
 | |
| _PROTOTYPE(int bread, (int fd, char *address, int bytes));
 | |
| _PROTOTYPE(int bwrite, (int fd, char *address, int bytes));
 | |
| _PROTOTYPE(void print, (char *str));
 | |
| _PROTOTYPE(char *num_out, (long number));
 | |
| _PROTOTYPE(void string_print, (char *buffer, char *fmt,...));
 | |
| 
 | |
| void error(s1, s2)
 | |
| char *s1, *s2;
 | |
| {
 | |
|   string_print(NIL_PTR, "%s %s\n", s1, s2 ? s2 : "");
 | |
|   flush();
 | |
|   exit(1);
 | |
| }
 | |
| 
 | |
| int main(argc, argv)
 | |
| int argc;
 | |
| register char *argv[];
 | |
| {
 | |
|   register char *mem_name;
 | |
|   register char *ptr;
 | |
|   struct stat st;
 | |
|   int i;
 | |
| 
 | |
|   if (argc < 3) error(usage, NIL_PTR);
 | |
| 
 | |
|   for (ptr = argv[1]; *ptr; ptr++) {
 | |
| 	switch (*ptr) {
 | |
| 	    case 'c':	creat_fl = TRUE;	break;
 | |
| 	    case 'x':	ext_fl = TRUE;	break;
 | |
| 	    case 't':	show_fl = TRUE;	break;
 | |
| 	    case 'v':		/* verbose output  -Dal */
 | |
| 		verbose_flag = !verbose_flag;
 | |
| 		break;
 | |
| 	    case 'o':		/* chown/chgrp files  -Dal */
 | |
| 		chown_flag = TRUE;
 | |
| 		break;
 | |
| 	    case 'F':		/* IGNORE ERRORS  -Dal */
 | |
| 		force_flag = TRUE;
 | |
| 		break;
 | |
| 	    case 'f':		/* standard U*IX usage -KS */
 | |
| 		break;
 | |
| 	    case 'p':		/* restore file modes right, ignore umask. */
 | |
| 		(void) umask(0);
 | |
| 		break;
 | |
| 	    case 'D':		/* do not recursively add directories. */
 | |
| 		norec_flag = TRUE;
 | |
| 		break;
 | |
| 	    default:	error(usage, NIL_PTR);
 | |
| 	}
 | |
|   }
 | |
| 
 | |
|   if (creat_fl + ext_fl + show_fl != 1) error(usage, NIL_PTR);
 | |
| 
 | |
|   if (strcmp(argv[2], "-") == 0)/* only - means stdin/stdout - KS */
 | |
| 	tar_fd = creat_fl ? 1 : 0;	/* '-' means used
 | |
| 					 * stdin/stdout  -Dal */
 | |
|   else
 | |
| 	tar_fd = creat_fl ? creat(argv[2], 0666) : open(argv[2], O_RDONLY);
 | |
| 
 | |
|   if (tar_fd < 0) error("Cannot open ", argv[2]);
 | |
| 
 | |
|   if (geteuid()) {		/* check if super-user */
 | |
| 	int save_umask;
 | |
| 	save_umask = umask(0);
 | |
| 	u_mask = ~save_umask;
 | |
| 	umask(save_umask);
 | |
| 	chown_flag = TRUE;	/* normal user can't chown */
 | |
|   } else
 | |
| 	u_mask = ~0;		/* don't restrict if 'privileged utility' */
 | |
| 
 | |
|   ar_dev = -1;			/* impossible device nr */
 | |
|   if (creat_fl) {
 | |
| 	if (tar_fd > 1 && fstat(tar_fd, &st) < 0)
 | |
| 		error("Can't stat ", argv[2]);	/* will never be here,
 | |
| 						 * right? */
 | |
| 	else {			/* get archive inode & device	 */
 | |
| 		ar_inode = st.st_ino;	/* save files inode	 */
 | |
| 		ar_dev = st.st_dev;	/* save files device	 */
 | |
| 	}			/* marks@mgse Mon Sep 25 11:30:45 CDT 1989 */
 | |
| 
 | |
| 	for (i = 3; i < argc; i++) {
 | |
| 		add_file(argv[i]);
 | |
| 		path[0] = '\0';
 | |
| 	}
 | |
| 	adjust_boundary();
 | |
|   } else if (ext_fl) {
 | |
| 	/* Extraction code moved here from tarfile() MSP */
 | |
| 	while (get_header()) {
 | |
| 		mem_name = header.member.m_name;
 | |
| 		if (is_dir(mem_name)) {
 | |
| 			for (ptr = mem_name; *ptr; ptr++);
 | |
| 			*(ptr - 1) = '\0';
 | |
| 			header.dbuf.typeflag = '5';
 | |
| 		}
 | |
| 		for (i = 3; i < argc; i++)
 | |
| 			if (!strncmp(argv[i], mem_name, strlen(argv[i])))
 | |
| 				break;
 | |
| 		if (argc == 3 || (i < argc)) {
 | |
| 			extract(mem_name);
 | |
| 		} else if (header.dbuf.typeflag == '0' ||
 | |
| 			   header.dbuf.typeflag == 0 ||
 | |
| 			   header.dbuf.typeflag == ' ')
 | |
| 			skip_entry();
 | |
| 		flush();
 | |
| 	}
 | |
|   } else
 | |
| 	tarfile();		/* tarfile() justs prints info. now MSP */
 | |
| 
 | |
|   flush();
 | |
|   return(0);
 | |
| }
 | |
| 
 | |
| BOOL get_header()
 | |
| {
 | |
|   register int check;
 | |
| 
 | |
|   mread(tar_fd, (char *) &header, sizeof(header));
 | |
|   if (header.member.m_name[0] == '\0') return FALSE;
 | |
| 
 | |
|   if (force_flag)		/* skip checksum verification  -Dal */
 | |
| 	return TRUE;
 | |
| 
 | |
|   check = (int) convert(header.member.m_checksum, INT_TYPE);
 | |
| 
 | |
|   if (check != checksum()) error("Tar: header checksum error.", NIL_PTR);
 | |
| 
 | |
|   return TRUE;
 | |
| }
 | |
| 
 | |
| /* Tarfile() just lists info about archive now; as of the t flag. */
 | |
| /* Extraction has been moved into main() as that needs access to argv[] */
 | |
| 
 | |
| void tarfile()
 | |
| {
 | |
|   register char *mem_name;
 | |
| 
 | |
|   while (get_header()) {
 | |
| 	mem_name = header.member.m_name;
 | |
| 	string_print(NIL_PTR, "%s%s", mem_name,
 | |
| 		     (verbose_flag ? " " : "\n"));
 | |
| 	switch (header.dbuf.typeflag) {
 | |
| 	    case '1':
 | |
| 		verb_print("linked to", header.dbuf.linkname);
 | |
| 		break;
 | |
| 	    case '2':
 | |
| 		verb_print("symbolic link to", header.dbuf.linkname);
 | |
| 		break;
 | |
| 	    case '6':	verb_print("", "fifo");	break;
 | |
| 	    case '3':
 | |
| 	    case '4':
 | |
| 		if (verbose_flag) {
 | |
| 			char sizebuf[TSIZLEN + 1];
 | |
| 
 | |
| 			strncpy(sizebuf, header.dbuf.size, (size_t) TSIZLEN);
 | |
| 			sizebuf[TSIZLEN] = 0;
 | |
| 			string_print(NIL_PTR,
 | |
| 			      "%s special file major %s minor %s\n",
 | |
| 				     (header.dbuf.typeflag == '3' ?
 | |
| 				      "character" : "block"),
 | |
| 				     header.dbuf.devmajor,
 | |
| 				     header.dbuf.devminor,
 | |
| 				     sizebuf);
 | |
| 		}
 | |
| 		break;
 | |
| 	    case '0':		/* official POSIX */
 | |
| 	    case 0:		/* also mentioned in POSIX */
 | |
| 	    case ' ':		/* ofetn used */
 | |
| 		if (!is_dir(mem_name)) {
 | |
| 			if (verbose_flag)
 | |
| 				string_print(NIL_PTR, "%d tape blocks\n",
 | |
| 					     block_size());
 | |
| 			skip_entry();
 | |
| 			break;
 | |
| 		} else		/* FALL TROUGH */
 | |
| 	    case '5':
 | |
| 			verb_print("", "directory");
 | |
| 		break;
 | |
| 	    default:
 | |
| 		string_print(NIL_PTR, "not recogised item %d\n",
 | |
| 			     header.dbuf.typeflag);
 | |
| 	}
 | |
| 	flush();
 | |
|   }
 | |
| }
 | |
| 
 | |
| void skip_entry()
 | |
| {
 | |
|   register int blocks = block_size();
 | |
| 
 | |
|   while (blocks--) (void) bread(tar_fd, io_buffer, TBLOCK_SIZE);
 | |
| }
 | |
| 
 | |
| void extract(file)
 | |
| register char *file;
 | |
| {
 | |
|   register int fd, r;
 | |
|   char *pd1, *pd2;		/* walk thru failed directory path */
 | |
| 
 | |
|   switch (header.dbuf.typeflag) {
 | |
|       case '1':			/* Link */
 | |
| 	delete(file);
 | |
| 	if (link(header.member.m_link, file) < 0)
 | |
| 		string_print(NIL_PTR, "Cannot link %s to %s: %s\n",
 | |
| 			     header.member.m_link, file, strerror(errno));
 | |
| 	else if (verbose_flag)
 | |
| 		string_print(NIL_PTR, "Linked %s to %s\n",
 | |
| 			     header.member.m_link, file);
 | |
| 	return;
 | |
|       case '5':			/* directory */
 | |
| 	if (!(file[0] == '.' && file[1] == '\0')) delete(file);
 | |
| 	if ((file[0] == '.' && file[1] == '\0') || mkdir(file, 0700) == 0) {
 | |
| 		do_chown(file);
 | |
| 		verb_print("created directory", file);
 | |
| 	} else {
 | |
| 		string_print(NIL_PTR, "Can't make directory %s: %s\n",
 | |
| 				file, strerror(errno));
 | |
| 	}
 | |
| 	return;
 | |
|       case '3':			/* character special */
 | |
|       case '4':			/* block special */
 | |
| 	{
 | |
| 		int dmajor, dminor, mode;
 | |
| 
 | |
| 		dmajor = (int) convert(header.dbuf.devmajor, INT_TYPE);
 | |
| 		dminor = (int) convert(header.dbuf.devminor, INT_TYPE);
 | |
| 		mode = (header.dbuf.typeflag == '3' ? S_IFCHR : S_IFBLK);
 | |
| 		delete(file);
 | |
| 		if (mknod(file, mode, (dmajor << 8 | dminor)) == 0) {
 | |
| 			if (verbose_flag) string_print(NIL_PTR,
 | |
| 				     "made %s special file major %s minor %s\n",
 | |
| 				      (header.dbuf.typeflag == '3' ?
 | |
| 				       "character" : "block"),
 | |
| 					     header.dbuf.devmajor,
 | |
| 					     header.dbuf.devminor);
 | |
| 			do_chown(file);
 | |
| 		}
 | |
| 		else 
 | |
| 		{
 | |
| 			string_print(NIL_PTR,
 | |
| 				     "cannot make %s special file major %s minor %s: %s\n",
 | |
| 				      (header.dbuf.typeflag == '3' ?
 | |
| 				       "character" : "block"),
 | |
| 					     header.dbuf.devmajor,
 | |
| 					     header.dbuf.devminor,
 | |
| 					     strerror(errno));
 | |
| 		}
 | |
| 		return;
 | |
| 	}
 | |
|       case '2':			/* symbolic link */
 | |
| #ifdef HAVE_SYMLINK
 | |
| 	delete(file);
 | |
| 	if (symlink(header.member.m_link, file) < 0)
 | |
| 		string_print(NIL_PTR, "Cannot make symbolic link %s to %s: %s\n",
 | |
| 			     header.member.m_link, file, strerror(errno));
 | |
| 	else if (verbose_flag)
 | |
| 		string_print(NIL_PTR, "Symbolic link %s to %s\n",
 | |
| 			     header.member.m_link, file);
 | |
| 	return;
 | |
| #endif
 | |
|       case '7':			/* contiguous file -- what is this (KS) */
 | |
| 	print("Not implemented file type\n");
 | |
| 	return;			/* not implemented, but break out */
 | |
| #ifdef HAVE_FIFO
 | |
|       case '6':			/* fifo */
 | |
| 	delete(file);
 | |
| 	if (mkfifo(file, 0) == 0) {	/* is chmod'ed in do_chown */
 | |
| 		do_chown(file);
 | |
| 		verb_print("made fifo", file);
 | |
| 	} else
 | |
| 		string_print(NIL_PTR, "Can't make fifo %s: %s\n",
 | |
| 			file, strerror(errno));
 | |
| 	return;
 | |
| #endif
 | |
|   }
 | |
| 
 | |
|   /* Create regular file.  If failure, try to make missing directories. */
 | |
|   if ((fd = creat(file, 0600)) < 0) {
 | |
| 	pd1 = file;
 | |
| 	while ((pd2 = index(pd1, '/')) > (char *) 0) {
 | |
| 		*pd2 = '\0';
 | |
| 		if (access(file, 1) < 0)
 | |
| 			if (mkdir(file, 0777) < 0) {
 | |
| 				string_print(NIL_PTR, "Cannot mkdir %s: %s\n",
 | |
| 					file, strerror(errno));
 | |
| 				return;
 | |
| 			} else
 | |
| 				string_print(NIL_PTR, "Made directory %s\n", file);
 | |
| 		*pd2 = '/';
 | |
| 		pd1 = ++pd2;
 | |
| 	}
 | |
| 	if ((fd = creat(file, 0600)) < 0) {
 | |
| 		string_print(NIL_PTR, "Cannot create %s: %s\n",
 | |
| 			file, strerror(errno));
 | |
| 		return;
 | |
| 	}
 | |
|   }
 | |
|   copy(file, tar_fd, fd, convert(header.member.m_size, LONG_TYPE));
 | |
|   (void) close(fd);
 | |
| 
 | |
|   do_chown(file);
 | |
| }
 | |
| 
 | |
| void delete(file)
 | |
| char *file;
 | |
| {
 | |
|   /* remove a file or an empty directory */
 | |
|   struct stat stbuf;
 | |
| 
 | |
|   if (stat(file, &stbuf) < 0) return;
 | |
| 
 | |
|   if (S_ISDIR(stbuf.st_mode)) (void) rmdir(file); else (void) unlink(file);
 | |
|   /* leave error reporting to the create following soon. */
 | |
| }
 | |
| 
 | |
| void do_chown(file)
 | |
| char *file;
 | |
| {
 | |
|   int uid = -1, gid = -1;	/* these are illegal ??? -- KS */
 | |
| 
 | |
|   if (!chown_flag) {		/* set correct owner and group  -Dal */
 | |
| 	if (header.dbuf.magic[TMAGLEN] == ' ')
 | |
| 		header.dbuf.magic[TMAGLEN] = '\0';	/* some tars out there
 | |
| 							 * ... */
 | |
| 	if (strncmp(TMAGIC, header.dbuf.magic, (size_t) TMAGLEN)) {
 | |
| 		struct passwd *pwd;
 | |
| 		struct group *grp;
 | |
| 
 | |
| 		pwd = getpwnam(header.dbuf.uname);
 | |
| 		if (pwd != NULL) uid = pwd->pw_uid;
 | |
| 		grp = getgrnam(header.dbuf.gname);
 | |
| 		if (grp != NULL) gid = grp->gr_gid;
 | |
| 	}
 | |
| 	if (uid == -1) uid = (int) convert(header.member.m_uid, INT_TYPE);
 | |
| 	if (gid == -1) gid = (int) convert(header.member.m_gid, INT_TYPE);
 | |
| 	chown(file, uid, gid);
 | |
|   }
 | |
|   chmod(file, u_mask & (int) convert(header.member.m_mode, INT_TYPE));
 | |
| 
 | |
|   /* Should there be a timestamp if the chown failes? -- KS */
 | |
|   timestamp(file);
 | |
| 
 | |
| }
 | |
| 
 | |
| void timestamp(file)
 | |
| char *file;
 | |
| {
 | |
|   struct utimbuf buf;
 | |
| 
 | |
|   buf.modtime = buf.actime = convert(header.dbuf.mtime, LONG_TYPE);
 | |
|   utime(file, &buf);
 | |
| }
 | |
| 
 | |
| void copy(file, from, to, bytes)
 | |
| char *file;
 | |
| int from, to;
 | |
| register long bytes;
 | |
| {
 | |
|   register int rest;
 | |
|   int blocks = (int) ((bytes + (long) TBLOCK_SIZE - 1) / (long) TBLOCK_SIZE);
 | |
| 
 | |
|   if (verbose_flag)
 | |
| 	string_print(NIL_PTR, "%s, %d tape blocks\n", file, blocks);
 | |
| 
 | |
|   while (blocks--) {
 | |
| 	(void) bread(from, io_buffer, TBLOCK_SIZE);
 | |
| 	rest = (bytes > (long) TBLOCK_SIZE) ? TBLOCK_SIZE : (int) bytes;
 | |
| 	mwrite(to, io_buffer, (to == tar_fd) ? TBLOCK_SIZE : rest);
 | |
| 	bytes -= (long) rest;
 | |
|   }
 | |
| }
 | |
| 
 | |
| long convert(str, type)
 | |
| char str[];
 | |
| int type;
 | |
| {
 | |
|   register long ac = 0L;
 | |
|   register int i;
 | |
| 
 | |
|   for (i = 0; i < type; i++) {
 | |
| 	if (str[i] >= '0' && str[i] <= '7') {
 | |
| 		ac <<= 3;
 | |
| 		ac += (long) (str[i] - '0');
 | |
| 	}
 | |
|   }
 | |
| 
 | |
|   return ac;
 | |
| }
 | |
| 
 | |
| int checksum()
 | |
| {
 | |
|   register char *ptr = header.member.m_checksum;
 | |
|   register int ac = 0;
 | |
| 
 | |
|   while (ptr < &header.member.m_checksum[INT_TYPE]) *ptr++ = ' ';
 | |
| 
 | |
|   ptr = header.hdr_block;
 | |
|   while (ptr < &header.hdr_block[TBLOCK_SIZE]) ac += *ptr++;
 | |
| 
 | |
|   return ac;
 | |
| }
 | |
| 
 | |
| int is_dir(file)
 | |
| register char *file;
 | |
| {
 | |
|   while (*file++ != '\0');
 | |
| 
 | |
|   return(*(file - 2) == '/');
 | |
| }
 | |
| 
 | |
| 
 | |
| char *path_name(file)
 | |
| register char *file;
 | |
| {
 | |
| 
 | |
|   string_print(pathname, "%s%s", path, file);
 | |
|   return pathname;
 | |
| }
 | |
| 
 | |
| void add_path(name)
 | |
| register char *name;
 | |
| {
 | |
|   register char *path_ptr = path;
 | |
| 
 | |
|   while (*path_ptr) path_ptr++;
 | |
| 
 | |
|   if (name == NIL_PTR) {
 | |
| 	while (*path_ptr-- != '/');
 | |
| 	while (*path_ptr != '/' && path_ptr != path) path_ptr--;
 | |
| 	if (*path_ptr == '/') path_ptr++;
 | |
| 	*path_ptr = '\0';
 | |
|   } else {
 | |
| 	while (*name) {
 | |
| 		if (path_ptr == &path[NAME_SIZE])
 | |
| 			error("Pathname too long", NIL_PTR);
 | |
| 		*path_ptr++ = *name++;
 | |
| 	}
 | |
| 	*path_ptr++ = '/';
 | |
| 	*path_ptr = '\0';
 | |
|   }
 | |
| }
 | |
| 
 | |
| /*
 | |
|  *	add a file to the archive
 | |
| */
 | |
| void add_file(file)
 | |
| register char *file;
 | |
| {
 | |
|   struct stat st;
 | |
|   char *linkname;
 | |
|   register int fd = -1;
 | |
|   char namebuf[16];		/* -Dal */
 | |
|   char cwd[129];		/* -KS */
 | |
| 
 | |
| #ifdef HAVE_SYMLINK
 | |
|   if (lstat(file, &st) < 0) {
 | |
| #else
 | |
|   if (stat(file, &st) < 0) {
 | |
| #endif
 | |
| 	string_print(NIL_PTR, "%s: %s\n", file, strerror(errno));
 | |
| 	return;
 | |
|   }
 | |
|   if (st.st_dev == ar_dev && st.st_ino == ar_inode) {
 | |
| 	string_print(NIL_PTR, "Cannot tar current archive file (%s)\n", file);
 | |
| 	return;
 | |
|   }				/* marks@mgse Mon Sep 25 12:06:28 CDT 1989 */
 | |
|   if ((fd = add_open(file, &st)) < 0) {
 | |
| 	string_print(NIL_PTR, "Cannot open %s\n", file);
 | |
| 	return;
 | |
|   }
 | |
|   make_header(path_name(file), &st);
 | |
|   if ((linkname = is_linked(&st)) != NULL) {
 | |
| 	strncpy(header.dbuf.linkname, linkname, (size_t) NAMSIZ);
 | |
| 	header.dbuf.typeflag = '1';
 | |
| 	if (verbose_flag) string_print(NIL_PTR, "linked %s to %s\n",
 | |
| 			     header.dbuf.linkname, file);
 | |
| 	string_print(header.member.m_checksum, "%I ", checksum());
 | |
| 	mwrite(tar_fd, (char *) &header, sizeof(header));
 | |
|   } else {
 | |
| 	is_added(&st, file);
 | |
| 	switch (st.st_mode & S_IFMT) {
 | |
| 	    case S_IFREG:
 | |
| 		header.dbuf.typeflag = '0';
 | |
| 		string_print(header.member.m_checksum, "%I ", checksum());
 | |
| 		mwrite(tar_fd, (char *) &header, sizeof(header));
 | |
| 		copy(path_name(file), fd, tar_fd, (long) st.st_size);
 | |
| 		break;
 | |
| 	    case S_IFDIR:
 | |
| 		header.dbuf.typeflag = '5';
 | |
| 		string_print(header.member.m_checksum, "%I ", checksum());
 | |
| 		mwrite(tar_fd, (char *) &header, sizeof(header));
 | |
| 		verb_print("read directory", file);
 | |
| 		if (norec_flag) break;
 | |
| 		if (NULL == getcwd(cwd, (int) sizeof cwd))
 | |
| 			string_print(NIL_PTR, "Error: cannot getcwd()\n");
 | |
| 		else if (chdir(file) < 0)
 | |
| 			string_print(NIL_PTR, "Cannot chdir to %s: %s\n",
 | |
| 				file, strerror(errno));
 | |
| 		else {
 | |
| 			add_path(file);
 | |
| #ifdef	DIRECT_3
 | |
| 			{
 | |
| 				DIR *dirp;
 | |
| 				struct direct *dp;
 | |
| 				struct stat dst;
 | |
| 
 | |
| 				add_close(fd);
 | |
| 				fd= 0;
 | |
| 				dirp = opendir(".");
 | |
| 				while (NULL != (dp = readdir(dirp)))
 | |
| 					if (strcmp(dp->d_name, ".") == 0)
 | |
| 						is_linked(&st);
 | |
| 					else if (strcmp(dp->d_name, "..") == 0) {
 | |
| 						if (stat("..", &dst) == 0)
 | |
| 							is_linked(&dst);
 | |
| 					} else {
 | |
| 						strcpy(namebuf, dp->d_name);
 | |
| 						add_file(namebuf);
 | |
| 					}
 | |
| 				closedir(dirp);
 | |
| 			}
 | |
| #else
 | |
| 			{
 | |
| 				int i;
 | |
| 				struct direct dir;
 | |
| 				struct stat dst;
 | |
| 
 | |
| 				for (i = 0; i < 2; i++) {	/* . and .. */
 | |
| 					mread(fd, &dir, sizeof(dir));
 | |
| 					if (strcmp(dir.d_name, ".") == 0)
 | |
| 						is_linked(&st);
 | |
| 					else if (strcmp(dir.d_name, "..") == 0) {
 | |
| 						if (stat("..", &dst) == 0)
 | |
| 							is_linked(&dst);
 | |
| 					} else
 | |
| 						break;
 | |
| 				}
 | |
| 				while (bread(fd, &dir, sizeof(dir)) == sizeof(dir))
 | |
| 					if (dir.d_ino) {
 | |
| 						strncpy(namebuf, dir.d_name,
 | |
| 						   (size_t) DIRSIZ);
 | |
| 						namebuf[DIRSIZ] = '\0';
 | |
| 						add_file(namebuf);
 | |
| 					}
 | |
| 			}
 | |
| #endif
 | |
| 			chdir(cwd);
 | |
| 			add_path(NIL_PTR);
 | |
| 			*file = 0;
 | |
| 		}
 | |
| 		break;
 | |
| #ifdef HAVE_SYMLINK
 | |
| 	    case S_IFLNK:
 | |
| 		{
 | |
| 			int i;
 | |
| 
 | |
| 			header.dbuf.typeflag = '2';
 | |
| 			verb_print("read symlink", file);
 | |
| 			i = readlink(file,
 | |
| 				     header.dbuf.linkname,
 | |
| 				  sizeof(header.dbuf.linkname) - 1);
 | |
| 			if (i < 0) {
 | |
| 				string_print(NIL_PTR,
 | |
| 					"Cannot read symbolic link %s: %s\n",
 | |
| 					file, strerror(errno));
 | |
| 				return;
 | |
| 			}
 | |
| 			header.dbuf.linkname[i] = 0;
 | |
| 			string_print(header.member.m_checksum, "%I ", checksum());
 | |
| 			mwrite(tar_fd, (char *) &header, sizeof(header));
 | |
| 			break;
 | |
| 		}
 | |
| #endif
 | |
| #ifdef HAVE_FIFO
 | |
| 	    case S_IFIFO:
 | |
| 		header.dbuf.typeflag = '6';
 | |
| 		verb_print("read fifo", file);
 | |
| 		string_print(header.member.m_checksum, "%I ", checksum());
 | |
| 		mwrite(tar_fd, (char *) &header, sizeof(header));
 | |
| 		break;
 | |
| #endif
 | |
| 	    case S_IFBLK:
 | |
| 		header.dbuf.typeflag = '4';
 | |
| 		if (verbose_flag) {
 | |
| 			char sizebuf[TSIZLEN + 1];
 | |
| 
 | |
| 			strncpy(sizebuf, header.dbuf.size, (size_t) TSIZLEN);
 | |
| 			sizebuf[TSIZLEN] = 0;
 | |
| 			string_print(NIL_PTR,
 | |
| 			 "read block device %s major %s minor %s\n",
 | |
| 				     file, header.dbuf.devmajor, header.dbuf.devminor, sizebuf);
 | |
| 		}
 | |
| 		string_print(header.member.m_checksum, "%I ", checksum());
 | |
| 		mwrite(tar_fd, (char *) &header, sizeof(header));
 | |
| 		break;
 | |
| 	    case S_IFCHR:
 | |
| 		header.dbuf.typeflag = '3';
 | |
| 		if (verbose_flag) string_print(NIL_PTR,
 | |
| 				     "read character device %s major %s minor %s\n",
 | |
| 				     file, header.dbuf.devmajor, header.dbuf.devminor);
 | |
| 		string_print(header.member.m_checksum, "%I ", checksum());
 | |
| 		mwrite(tar_fd, (char *) &header, sizeof(header));
 | |
| 		break;
 | |
| 	    default:
 | |
| 		is_deleted(&st);
 | |
| 		string_print(NIL_PTR, "Tar: %s unknown file type. Not added.\n", file);
 | |
| 		*file = 0;
 | |
| 	}
 | |
|   }
 | |
| 
 | |
|   flush();
 | |
|   add_close(fd);
 | |
| }
 | |
| 
 | |
| void verb_print(s1, s2)
 | |
| char *s1, *s2;
 | |
| {
 | |
|   if (verbose_flag) string_print(NIL_PTR, "%s: %s\n", s1, s2);
 | |
| }
 | |
| 
 | |
| void add_close(fd)
 | |
| int fd;
 | |
| {
 | |
|   if (fd != 0) close(fd);
 | |
| }
 | |
| 
 | |
| /*
 | |
|  *	open file 'file' to be added to archive, return file descriptor
 | |
| */
 | |
| int add_open(file, st)
 | |
| char *file;
 | |
| struct stat *st;
 | |
| {
 | |
|   int fd;
 | |
|   if (((st->st_mode & S_IFMT) != S_IFREG) &&
 | |
|       ((st->st_mode & S_IFMT) != S_IFDIR))
 | |
| 	return 0;
 | |
|   fd = open(file, O_RDONLY);
 | |
|   if (fd == -1)
 | |
|   	fprintf(stderr, "open failed: %s\n", strerror(errno));
 | |
|   return fd;
 | |
| }
 | |
| 
 | |
| void make_header(file, st)
 | |
| char *file;
 | |
| register struct stat *st;
 | |
| {
 | |
|   register char *ptr = header.member.m_name;
 | |
|   struct passwd *pwd;
 | |
|   struct group *grp;
 | |
| 
 | |
|   clear_header();
 | |
| 
 | |
|   while (*ptr++ = *file++);
 | |
| 
 | |
|   if ((st->st_mode & S_IFMT) == S_IFDIR) {	/* fixed test  -Dal */
 | |
| 	*(ptr - 1) = '/';
 | |
|   }
 | |
|   string_print(header.member.m_mode, "%I ", st->st_mode & 07777);
 | |
|   string_print(header.member.m_uid, "%I ", st->st_uid);
 | |
|   string_print(header.member.m_gid, "%I ", st->st_gid);
 | |
|   if ((st->st_mode & S_IFMT) == S_IFREG)
 | |
| 	string_print(header.member.m_size, "%L ", st->st_size);
 | |
|   else
 | |
| 	strncpy(header.dbuf.size, "0", (size_t) TSIZLEN);
 | |
|   string_print(header.member.m_time, "%L ", st->st_mtime);
 | |
|   strncpy(header.dbuf.magic, TMAGIC, (size_t) TMAGLEN);
 | |
|   header.dbuf.version[0] = 0;
 | |
|   header.dbuf.version[1] = 0;
 | |
|   pwd = getpwuid(st->st_uid);
 | |
|   strncpy(header.dbuf.uname,
 | |
| 	(pwd != NULL ? pwd->pw_name : "nobody"), TUNMLEN);
 | |
|   grp = getgrgid(st->st_gid);
 | |
|   strncpy(header.dbuf.gname,
 | |
| 	(grp != NULL ? grp->gr_name : "nobody"), TGNMLEN);
 | |
|   if (st->st_mode & (S_IFBLK | S_IFCHR)) {
 | |
| 	string_print(header.dbuf.devmajor, "%I ", (st->st_rdev >> 8));
 | |
| 	string_print(header.dbuf.devminor, "%I ", (st->st_rdev & 0xFF));
 | |
|   }
 | |
|   header.dbuf.prefix[0] = 0;
 | |
| }
 | |
| 
 | |
| void is_added(st, file)
 | |
| struct stat *st;
 | |
| char *file;
 | |
| {
 | |
|   struct link *new;
 | |
|   char *name;
 | |
| 
 | |
|   if ((*file == 0) || (st->st_nlink == 1)) return;
 | |
|   name = path_name(file);
 | |
|   new = (struct link *) malloc(sizeof(struct link) + strlen(name));
 | |
|   if (new == NULL) {
 | |
| 	print("Out of memory\n");
 | |
| 	return;
 | |
|   }
 | |
|   new->next = link_top;
 | |
|   new->dev = st->st_dev;
 | |
|   new->ino = st->st_ino;
 | |
|   new->nlink = st->st_nlink - 1;
 | |
|   strcpy(new->name, name);
 | |
|   link_top = new;
 | |
| }
 | |
| 
 | |
| void is_deleted(st)
 | |
| struct stat *st;
 | |
| {
 | |
|   struct link *old;
 | |
| 
 | |
|   if ((old = link_top) != NULL) {
 | |
| 	link_top = old->next;
 | |
| 	free(old);
 | |
|   }
 | |
| }
 | |
| 
 | |
| char *is_linked(st)
 | |
| struct stat *st;
 | |
| {
 | |
|   struct link *cur = link_top;
 | |
|   struct link **pre = &link_top;
 | |
|   static char name[NAMSIZ];
 | |
| 
 | |
|   while (cur != NULL)
 | |
| 	if ((cur->dev != st->st_dev) || (cur->ino != st->st_ino)) {
 | |
| 		pre = &cur->next;
 | |
| 		cur = cur->next;
 | |
| 	} else {
 | |
| 		if (--cur->nlink == 0) {
 | |
| 			*pre = cur->next;
 | |
| 			strncpy(name, cur->name, NAMSIZ);
 | |
| 			return name;
 | |
| 		}
 | |
| 		return cur->name;
 | |
| 	}
 | |
|   return NULL;
 | |
| }
 | |
| 
 | |
| void clear_header()
 | |
| {
 | |
|   register char *ptr = header.hdr_block;
 | |
| 
 | |
|   while (ptr < &header.hdr_block[TBLOCK_SIZE]) *ptr++ = '\0';
 | |
| }
 | |
| 
 | |
| void adjust_boundary()
 | |
| {
 | |
|   clear_header();
 | |
|   mwrite(tar_fd, (char *) &header, sizeof(header));
 | |
| #ifndef POSIX_COMP
 | |
|   while (total_blocks++ < BLOCK_BOUNDARY)
 | |
| 	mwrite(tar_fd, (char *) &header, sizeof(header));
 | |
| #else
 | |
|   mwrite(tar_fd, (char *) &header, sizeof(header));
 | |
| #endif
 | |
|   (void) close(tar_fd);
 | |
| }
 | |
| 
 | |
| void mread(fd, address, bytes)
 | |
| int fd, bytes;
 | |
| char *address;
 | |
| {
 | |
|   if (bread(fd, address, bytes) != bytes) error("Tar: read error.", NIL_PTR);
 | |
| }
 | |
| 
 | |
| void mwrite(fd, address, bytes)
 | |
| int fd, bytes;
 | |
| char *address;
 | |
| {
 | |
|   if (bwrite(fd, address, bytes) != bytes) error("Tar: write error.", NIL_PTR);
 | |
| 
 | |
|   total_blocks++;
 | |
| }
 | |
| 
 | |
| int bread(fd, address, bytes)
 | |
| int fd, bytes;
 | |
| char *address;
 | |
| {
 | |
|   int n = 0, r;
 | |
| 
 | |
|   while (n < bytes) {
 | |
| 	if ((r = read(fd, address + n, bytes - n)) <= 0) {
 | |
| 		if (r < 0) return r;
 | |
| 		break;
 | |
| 	}
 | |
| 	n += r;
 | |
|   }
 | |
|   return n;
 | |
| }
 | |
| 
 | |
| int bwrite(fd, address, bytes)
 | |
| int fd, bytes;
 | |
| char *address;
 | |
| {
 | |
|   int n = 0, r;
 | |
| 
 | |
|   while (n < bytes) {
 | |
| 	if ((r = write(fd, address + n, bytes - n)) <= 0) {
 | |
| 		if (r < 0) return r;
 | |
| 		break;
 | |
| 	}
 | |
| 	n += r;
 | |
|   }
 | |
|   return n;
 | |
| }
 | |
| 
 | |
| char output[TBLOCK_SIZE];
 | |
| void print(str)
 | |
| register char *str;
 | |
| {
 | |
|   int fd = (tar_fd == 1 ? 2 : 1);
 | |
|   static int indx = 0;
 | |
| 
 | |
|   if (str == NIL_PTR) {
 | |
| 	write(fd, output, indx);
 | |
| 	indx = 0;
 | |
| 	return;
 | |
|   }
 | |
|   while (*str) {
 | |
| 	output[indx++] = *str++;
 | |
| 	if (indx == TBLOCK_SIZE) {
 | |
| 		write(fd, output, TBLOCK_SIZE);
 | |
| 		indx = 0;
 | |
| 	}
 | |
|   }
 | |
| }
 | |
| 
 | |
| char *num_out(number)
 | |
| register long number;
 | |
| {
 | |
|   static char num_buf[12];
 | |
|   register int i;
 | |
| 
 | |
|   for (i = 11; i--;) {
 | |
| 	num_buf[i] = (number & 07) + '0';
 | |
| 	number >>= 3;
 | |
|   }
 | |
| 
 | |
|   return num_buf;
 | |
| }
 | |
| 
 | |
| /*VARARGS2*/
 | |
| #if __STDC__
 | |
| void string_print(char *buffer, char *fmt,...)
 | |
| #else
 | |
| void string_print(buffer, fmt)
 | |
| char *buffer;
 | |
| char *fmt;
 | |
| #endif
 | |
| {
 | |
|   va_list args;
 | |
|   register char *buf_ptr;
 | |
|   char *scan_ptr;
 | |
|   char buf[STRING_SIZE];
 | |
|   BOOL pr_fl, i;
 | |
| 
 | |
|   if (pr_fl = (buffer == NIL_PTR)) buffer = buf;
 | |
| 
 | |
|   va_start(args, fmt);
 | |
|   buf_ptr = buffer;
 | |
|   while (*fmt) {
 | |
| 	if (*fmt == '%') {
 | |
| 		fmt++;
 | |
| 		switch (*fmt++) {
 | |
| 		    case 's':
 | |
| 			scan_ptr = (char *) (va_arg(args, char *));
 | |
| 			break;
 | |
| 		    case 'I':
 | |
| 			scan_ptr = num_out((long) (va_arg(args, int)));
 | |
| 			for (i = 0; i < 5; i++) scan_ptr++;
 | |
| 			break;
 | |
| 		    case 'L':
 | |
| 			scan_ptr = num_out((long) va_arg(args, long));
 | |
| 			break;
 | |
| 		    case 'd':
 | |
| 			scan_ptr = num_out((long) va_arg(args, int));
 | |
| 			while (*scan_ptr == '0') scan_ptr++;
 | |
| 			scan_ptr--;
 | |
| 			break;
 | |
| 		    default:	scan_ptr = "";
 | |
| 		}
 | |
| 		while (*buf_ptr++ = *scan_ptr++);
 | |
| 		buf_ptr--;
 | |
| 	} else
 | |
| 		*buf_ptr++ = *fmt++;
 | |
|   }
 | |
|   *buf_ptr = '\0';
 | |
| 
 | |
|   if (pr_fl) print(buffer);
 | |
|   va_end(args);
 | |
| }
 | 
