/* netlib-unshar    http://netlib.bell-labs.com/netlib/access/unshar.c
 *
 * This program unpacks the shell scripts sent by netlib.  More
 * precisely, it attempts to do nothing but create new files in the
 * current directory, taking care not to overwrite existing files, nor
 * to write files elsewhere, nor to execute any commands.
 *
 * This program assumes Standard C.  It does not pretend to interpret
 * all valid shell scripts, only certain ones used in netlib.
 * Send other bug reports to: ehg@research.bell-labs.com.
 *
 * The author of this software is Eric Grosse.  Copyright (c) 1998.
 * Permission to use, copy, modify, and distribute this software for any
 * purpose without fee is hereby granted, provided that this entire notice
 * is included in all copies of any software which is or includes a copy
 * or modification of this software and in all copies of the supporting
 * documentation for such software.
 * THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED
 * WARRANTY.  IN PARTICULAR, NEITHER THE AUTHORS NOR AT&T MAKE ANY
 * REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY
 * OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE.
 */

/* SAMPLE INPUT 1:   (reply)
	From netlibd Mon Apr 10 22:19 EDT 1995
	To: ehg
	Subject: Re: send master/index  1
	
	#!/bin/sh
	# to unpack, sh this message in an empty directory
	PATH=/bin:/usr/bin
	echo "Anything free comes with no guarantee!"
	cat > index <<'CUT HERE............'
	lib     a
	for     approximation algorithms from various sources
	CUT HERE............
	test ! -d linpack && mkdir linpack
	cat > linpack/dgefa.f <<'CUT HERE............'
	      subroutine dgefa(a,lda,n,ipvt,info)
	CUT HERE............
	#define END
*/

/* SAMPLE INPUT 2:   (stree)
	echo unshar.c 1>&2
	sed >unshar.c <<'-------cut here----- unshar.c' 's/^X//'
	X/* netlib-unshar
	X *
	X * This program unpacks the shell scripts sent by netlib.  More
	-------cut here----- unshar.c
*/

/* SAMPLE INPUT 3:   (shar)
	if test -f 'acoshl.c' -a X"$1" != X"-c"; then
		echo 'x - skipping acoshl.c (File already exists)'
	else
	echo 'x - extracting acoshl.c (Text)'
	sed 's/^X//' << 'SHAR_EOF' > 'acoshl.c' &&
		y = acoshl( x );
	SHAR_EOF
	chmod 0666 acoshl.c ||
	echo 'restore of acoshl.c failed'
	Wc_c="`wc -c < 'acoshl.c'`"
	test 2873 -eq "$Wc_c" ||
		echo 'acoshl.c: original size 2873, current size' "$Wc_c"
	fi
	exit 0
*/

/* SAMPLE INPUT 4:   (bigmail)
	cat > netlib <<'CUT HERE............'
	# A good place to put all this is the dumpster.
	CUT HERE............
	cat > netlib <<'CUT HERE............'
	# A good place to put all this is the dumpster.
	CUT HERE............
	test -w netlib &&
	test -r 10832P00 &&
	(
	 cat 10832P00 >> netlib; rm 10832P00
	)
*/
/* SAMPLE INPUT 5:   (shar 3.49)      added 6jun98
	if test ! -d 'lapackpp'; then
	    echo 'x - creating directory lapackpp'
	    mkdir 'lapackpp'
	fi
	if test -f 'lapackpp/arch.h' -a X"$1" != X"-c"; then
		echo 'x - skipping lapackpp/arch.h (File already exists)'
	else
	echo 'x - extracting lapackpp/arch.h (Text)'
	sed 's/^X//' << 'SHAR_EOF' > 'lapackpp/arch.h' &&
	...
	SHAR_EOF
	chmod 0660 lapackpp/arch.h ||
	echo 'restore of lapackpp/arch.h failed'
	Wc_c="`wc -c < 'lapackpp/arch.h'`"
	test 440 -eq "$Wc_c" ||
		echo 'lapackpp/arch.h: original size 4, current size' "$Wc_c"
	fi
*/

/* Implementation Note.  Obviously, this code is less elegant and less
general than just invoking the shell.  Its reason for being is purely
to protect against malicious input.  (That's also the reason I wasn't
satisfied with the unshar programs I found on the net.)  If you find a
security hole here, please alert me immediately!  ehg@research.bell-labs.com
   If you make small edits to let this program cope with other, widely
used shar formats, I'm also happy to absorb those.  I made a conscious
decision not to interpret chmod.  Frequently these set directories to be
read-only, then try to create files there.  More seriously, if chmod +x
were interpreted, then we'd need to watch out for creating "mkdir" in
the local directory.
*/


#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#ifdef DOS
#define SLASH '\\'
#else
#define SLASH '/'
#endif

int verbose = 1;

void
quit(void)
{
	fprintf(stderr,"Sorry.\n");
	if(getenv("Errordump")) abort();
	exit(1);
}

/* fgets() but strip \r, for Unix processes reading Windows files */
char*
fgetsr(char *s, int n, FILE *stream)
{
	char *r, *line = fgets(s,n,stream);
	if(line==NULL)
		return NULL;
	r = strrchr(line,'\r');
	if(r!=NULL && r[1]=='\n' && r[2]=='\0'){
		r[0] = '\n';
		r[1] = '\0';
	}
	return line;
}


/* parses line==" > pathname <<'CUT HERE' 's/^X//'\n" */
/* Note: a newline is added to cut, to simplify later matching. */
void
setcut(char *line, char *path, int npath, char *cut, int ncut,
	unsigned int *drop1p)
{
	char *s = line;
	int i, n;
	*path = 0;
	*cut = 0;
	*drop1p = 0;
	while(*s){
	  s += strspn(s," \t");
	  switch(*s){
	    case '\'': case '\"':
		if(s[1]=='s' && s[3]=='^' &&
			s[2]==s[5] && s[5]==s[6] && s[0]==s[7]){
			*drop1p = (unsigned char)(s[4]);
			s += 8;
		}else{
			fprintf(stderr,"couldn't parse %s\n",s);
			quit();
		}
		break;
	    case '>':
		s += 1+strspn(s+1," \t");
		n = strcspn(s," \t\n<");
		if(n>=npath){
			fprintf(stderr,"pathname too long in:\n%s",line);
			quit();
		}
		strncpy(path,s,n);
		path[n] = 0;
		s += n;
		if(path[0]=='\'' && path[n-1]=='\''){
			/* strip quotes */
			/* memmove is missing from some otherwise usable
				compilers like gcc, so do it inline...
				memmove(path,path+1,n-2);  */
			for(i = 0; i<n-2; i++)
				path[i] = path[i+1];
			path[n-2] = 0;
		}
		break;
	    case '<':
		if(strncmp(s,"<<",2)!=0){
			fprintf(stderr,"expected <<' in:\n%s",line);
			quit();
		}
		s += 2;
		s += strspn(s," \t\n");
		if(*s=='\''){
			s++;  /* advance to start of cut string */
			n = strcspn(s,"'");
		}else if(*s=='\\'){
			s++;  /* advance to start of cut string */
			n = strcspn(s," ");
		}else{
			n = strcspn(s," ");
		}
		if(n+1>=ncut){
			fprintf(stderr,"cutstring too long in:\n%s",line);
			quit();
		}
		strncpy(cut,s,n);
		cut[n] = '\n';
		cut[n+1] = 0;
		s += n+1;
		break;
	    case '&':
		if(strncmp(s,"&&",2)==0){
			s += 2;
		}else{
			fprintf(stderr,"unrecognized: %s",s);
			fprintf(stderr,"   in: %s",line);
			quit();
		}
		break;
	    case '\n':
	    case '\0':
		return;
		break;
	    default:
		fprintf(stderr,"unrecognized: %s",s);
		fprintf(stderr,"   in: %s",line);
		quit();
		break;
	  }
	}
}

int
unsafe(char *s)
{
        char c, *r = s;
        /* check for characters interpreted by the shell (by which nefarious
           users might otherwise break into the system) */
        for (c = *s; c != '\0'; c = *++s) {
                if ( strchr(" \"'`$\n;&|^<>()\\", c) ||
				(c & 0200) ||
				!isprint(c) ){
			if(verbose)
				fprintf(stderr,"char %c = %u\n",c,c);
                        return(1);
		}
        }
	for(; r=strchr(r,'.'); r++){
		if(r[1]!='.')
			continue;
		if(r>s && r[-1]!=SLASH)
			continue;
		if(r[2]!=0 && r[2]!=SLASH)
			continue;
		fprintf(stderr,"no .. path components allowed!\n");
		fprintf(stderr,"   %s\n",s);
		quit();
	}
        return(0);
}

/* like fopen(,"w") but restricts to local directory tree and
   avoids overwriting existing file */
FILE*
safeopen(char*path)
{
	FILE *f;
	if(unsafe(path)){
		fprintf(stderr,"filename contains dangerous chars\n");
		quit();
	}
	f = fopen(path,"r");
	if(f){
		fprintf(stderr,"filename %s collision.\n",path);
		fprintf(stderr,"unshar in empty directory.\n");
		quit();
	}
	f = fopen(path,"w");
	if(!f){
		fprintf(stderr,"can't open %s\n",path);
		quit();
	}
	return(f);
}

/* copy disclaimers, etc. onto standard error */
void
echo(char *mess)
{
	int n = strlen(mess);
	if(strcmp(mess+n-6," 1>&2\n")==0){
		mess[n-6] = '\n';
		mess[n-5] = 0;
	}
	while(*mess){
		if(isprint(*mess) || *mess=='\n')
			fputc(*mess,stderr);
		mess++;
	}
}

/* return nonzero if line ends in && or ||, and if so chop it */
int
shellbool(char *line)
{
	int n;
	char *s;
	n = strlen(line);
	s = line+n-3;
	if(strcmp(s,"&&\n")==0 || strcmp(s,"||\n")==0){
		*s = '\n';
		return(1);
	}
	return(0);
}

/* Terminator2
	test -w netlib &&
	test -r 10832P00 &&
	test -r 10832P01 &&
	(
	 cat 10832P00 >> netlib; rm 10832P00
	 cat 10832P01 >> netlib; rm 10832P01
	)
*/
void
bigmail(char *r, FILE *in)
{
	char line[5000], assembly[1000], piece[30], *s;
	int n;
	FILE *f, *af;

	s = strchr(r,' ');
	if(!s || strcmp(s," &&\n")!=0 || s<=r || s-r>=sizeof(assembly))
		return;
	*s = 0;
	strcpy(assembly,r);
	if(unsafe(assembly))
		return;
	while(1){
		if(!fgetsr(line,sizeof(line),in))
			return;
		if(strcmp(line,"(\n")==0)
			break;
		if(strncmp(line,"test -r ",8)!=0)
			return;
		r = line+8;
		s = strchr(r,' ');
		if(!s || strcmp(s," &&\n")!=0 || s<=r || s-r>=sizeof(piece))
			return;
		*s = 0;
		strcpy(piece,r);
		if(unsafe(piece))
			return;
		f = fopen(piece,"r");
		if(!f)
			return;
		fclose(f);
	}
	if(verbose)
		fprintf(stderr,"reassembling %s\n",assembly);
	af = fopen(assembly,"a");
	while(1){
		if(!fgetsr(line,sizeof(line),in)){
			fprintf(stderr,"unexpected EOF\n");
			return;
		}
		if(strcmp(line,")\n")==0)
			break;
		if(strncmp(line," cat ",5)!=0){
			fprintf(stderr,"contact ehg@research.bell-labs.com");
			quit();
		}
		r = line+5;
		s = strchr(r,' ');
		if(!s || s<=r || s-r>=sizeof(piece))
			return;
		*s++ = 0;
		strcpy(piece,r);
		if(unsafe(piece))
			return;
		n = strlen(assembly);
		if( strncmp(s,">> ",3)!=0 ||
		    strncmp(s+3,assembly,n)!=0 ||
		    strncmp(s+3+n,"; rm ",5)!=0)
			return;
		f = fopen(piece,"r");
		if(!f){
			fprintf(stderr,"couldn't reopen %s",piece);
			quit();
		}
		if(verbose)
			fprintf(stderr,"   %s\n",piece);
		while((n = fread(line,1,sizeof(line),f))>0){
			if(fwrite(line,1,n,af)!=n){
				fprintf(stderr,"write error\n");
				quit();
			}
		}
		fclose(f);
		remove(piece);
	}
	fclose(af);
}

void
mkdir(char *cmd)
{
	int n = strlen(cmd);
	if(n>1000)  /* don't stress system() */
		return;
	if(cmd[n-1]=='\n')
		cmd[n-1] = 0;
	if(strncmp(cmd,"mkdir ",6)!=0 || unsafe(cmd+6))
		return;
	if(verbose)
		fprintf(stderr,"%s\n",cmd);
	system(cmd);
}

/* unpack one input file into (possibly many) output files */
void
unshar(FILE *in)
{
	char line[5000], *line0, cut[100], path[1000];
		/* plenty big enough for netlib shar files */
	FILE *out = 0;
	unsigned int drop1;  /* drop first char on line, if it equals drop1 */
	int skipnext = 0;

	while(fgetsr(line,sizeof(line),in)){
		/* newline retained in line */
		if(out){
			if(strcmp(line,cut)==0){
				fclose(out);
				out = 0;
				continue;
			}
			if(drop1 && drop1==(unsigned char)line[0])
				line0 = line+1;
			else
				line0 = line;
			if(fputs(line0,out)==EOF){
				fprintf(stderr,"write error\n");
				quit();
			}
			continue;
		}
		if(skipnext){
			skipnext = shellbool(line);
			continue;
		}
		fprintf(stderr, ">>>>> %s\n", line);
		line0 = line;
		while(isspace(*line0))
			line0++;
		if(strncmp(line0,"echo ",5)==0){
			echo(line0+5);
		}else if(strncmp(line0,"exit ",5)==0){
			return;
		}else if(strncmp(line0,"cat ",4)==0 ||
			 strncmp(line0,"sed ",4)==0){
			setcut(line0+4,path,sizeof(path),cut,sizeof(cut),&drop1);
			out = safeopen(path);
			if(verbose)
				fprintf(stderr,"extracting %s\n",path);
		}else if(strncmp(line,"if test -f '",12)==0){
			while(fgetsr(line,sizeof(line),in))
				if(strcmp(line,"else\n")==0)
					break;
		}else if(strncmp(line,"if test ! -d '",14)==0){
			line0 = strchr(line+14,'\'');
			if(!line0){fprintf(stderr,"missing '\n");quit();}
			strcpy(line+8,"mkdir");
			line[13] = ' ';
			*line0 = 0;
			mkdir(line+8);
			while(fgetsr(line,sizeof(line),in))
				if(strcmp(line,"fi\n")==0)
					break;
		}else if(strncmp(line,"test ! -d ",10)==0){
			line0 = strchr(line+10,' ');
			if(!line0){fprintf(stderr,"unrec test -d\n");quit();}
			if(strncmp(line0," && mkdir ",10)!=0)
				continue;
			mkdir(line0+4);
		}else if(strncmp(line,"mkdir ",6)==0){
			mkdir(line);
		}else if(strncmp(line,"test -w ",8)==0){
			bigmail(line+8,in);
		}else{
			skipnext = shellbool(line);
			/* and otherwise ignore */
		}
	}
}

void
main(int argc, char**argv)
{
	FILE *in;

	if(argc<=1){
		unshar(stdin);
	}else{
		while(--argc>0){
			if(strcmp(*++argv,"-v")==0){
				verbose = 1 - verbose;
				continue;
			}
			in = fopen(*argv,"r");
			if(!in){
				fprintf(stderr,"can't open %s\n",*argv);
				quit();
			}
			if(verbose)
				fprintf(stderr,"======= %s =======\n",*argv);
			unshar(in);
		}
	}
	exit(0);
}
