/** Encapper - Version 1.3
* *
* * Silent (without -v) and Deadly (with -f) Encap Util
* *
* * Copyright 1996, Brian J. Swetland.  <swetland@uiuc.edu>
* * Free for non-commercial use.  Share and Enjoy.
* *
* * Bug fixes, -c option, and exclude code by Jason Hoos <jhoos@uiuc.edu>
* * Logfile and -m option by Brandon Long <blong@uiuc.edu>
**/  

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <dirent.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>

#if 0
#define DEFAULT_ENCAPDIR "/local/encap"
#define DEFAULT_LOCALDIR "/local"
#define DEFAULT_TRACKDIR "/local/encap/encapper/EncapDirs"
#define DEFAULT_LOGFILE  "/local/encap/INSTALLED"
#endif
#define DEFAULT_ENCAPDIR "/home/blong/bsd/encap"
#define DEFAULT_LOCALDIR "/home/blong/bsd"
#define DEFAULT_TRACKDIR "/home/blong/bsd/encapper/EncapDirs"
#define DEFAULT_LOGFILE  "/home/blong/bsd/encap/INSTALLED"
#define MAX_PATH_LEN 8192

/* operational flags & such*/
char *encapdir=DEFAULT_ENCAPDIR;
char *localdir=DEFAULT_LOCALDIR;
char *trackdir=DEFAULT_TRACKDIR;
char *logfile=DEFAULT_LOGFILE;
int edlen;

/* directories within packages to permanently ignore; this can be
   augmented with an encap.exclude file in the package directory */
char *ignoredirs[] = {
    "Source",
    "EncapDirs",
    "encap.exclude",
    NULL
};

/* save on a ton of mallocs */
char src[MAX_PATH_LEN],dst[MAX_PATH_LEN],trk[MAX_PATH_LEN];
char buf[MAX_PATH_LEN*3];
char errmsg[MAX_PATH_LEN*3];
char package[256];
char command[256];

/* operational flags */
#define OP_INIT    (1<<0)
#define OP_MESG    (1<<1)
#define OP_INSTALL (1<<2)
#define OP_REMOVE  (1<<3)

int Options = 0;
int Verbose = 0;
int Force = 0; 
int Error = 0;

void usage(char *name)
{
    fprintf(stderr,"\nEncapper v1.3, Copyright 1997, Brian J. Swetland. Share and Enjoy.\n");
    fprintf(stderr,"usage: %s [-v] [-c] [-f] {-[arim] module module ...}*\n",
	name);
    fprintf(stderr,"flags: -f   force\n");
    fprintf(stderr,"       -v   verbose\n");
    fprintf(stderr,"       -a   add modules (default)\n");
    fprintf(stderr,"       -r   remove modules\n");
    fprintf(stderr,"       -i   init encap tree for module\n");
    fprintf(stderr,"       -c   clean out old links\n");
    fprintf(stderr,"       -m   prompt for log message\n");
    exit(1);
}

void encap_log(char *pack, char *cmd, char *msg)
{
  FILE *fp;
  char date[256];
  time_t clock;
  struct tm *tms;

  if ((fp = fopen(logfile, "a")) == NULL)
  {
    sprintf(errmsg, "fopen: Error openning log file\n");
    perror(errmsg);
    Error = 1;
    return;
  }
  clock = time(NULL);
  tms = localtime(&clock);
  strftime(date, sizeof(date), "%D %T", tms);

  fprintf(fp, "%-20s %-10s %-20s %s\n", date,cmd,pack,msg);
  fclose(fp);
}

void take_message()
{
  int x;
  char msg[256];

  fprintf(stderr, "Log message for package %s\n>", package);
  fgets(msg, sizeof(msg), stdin);
  x = strlen(msg) - 1;
  while (x && (msg[x] == '\n' || msg[x] == '\r')) 
    msg[x--] = '\0';
  encap_log(package, "message", msg);
}

char *subdirs[] = {
    "/bin",
    "/lib",
    "/man",
    "/man/man1",
    "/man/man2",
    "/man/man3",
    "/man/man4",
    "/man/man5",
    "/man/man6",
    "/man/man7",
    "/man/man8",
    "/info",
    "/etc",
    NULL
};

void make_encap_dirs(char *top)
{
    int i;
    char *path;

    path = malloc(strlen(top)+strlen(encapdir)+32);
    
    sprintf(path,"%s/%s",encapdir,top);
    if (Verbose) fprintf(stderr,"+ %s\n",path);
    if(mkdir(path,0755)){
	sprintf(errmsg,"E Could not mkdir(\"%s\",0755)",path);
	perror(errmsg);
	Error = 1;
    }
    for(i=0;subdirs[i];i++){
	sprintf(path,"%s/%s%s",encapdir,top,subdirs[i]);
	if(Verbose) fprintf(stderr,"+ %s\n",path);
	if(mkdir(path,0755)){
            sprintf(errmsg,"E Could not mkdir(\"%s\",0755)",path);
            perror(errmsg);
	    Error = 1;
	}
    }
}

void do_link(char *src, char *dst)
{                
    struct stat sinfo;
    int isourlink = 0;
    int l;

    if(!lstat(dst,&sinfo)){
	if (sinfo.st_mode & S_IFLNK) {
                /* its a link; see if it's ours */
            if ((l=readlink(dst, buf, MAX_PATH_LEN))>-1) {
		buf[l] = '\0';
		isourlink = (strncmp(buf, encapdir, edlen)==0);
		if (isourlink && strcmp(buf, src)==0)
                        /* already linked */
                    return;
            }
	}
            /* destination exists */
	if(!Force && !isourlink) {
                /* if we are not forceful,
                   we cannot toast it unless its our link */
            sprintf(errmsg, "E existing destination \"%s\" (try -f)",dst);
	    fprintf(stderr, "%s\n", errmsg);
	    Error = 1;
	} else {
                /* we are forceful -- try to delete */
                /* check if it's a directory and bitch if it isn't empty */
            if(sinfo.st_mode & S_IFDIR) {
		if (rmdir(dst)) {
                    sprintf(errmsg, "E rmdir(\"%s\")",dst);
                    perror(errmsg);
		    Error = 1;
		} else {
                    if(symlink(src,dst)){
                        sprintf(errmsg, "E symlink(\"%s\",\"%s\")",src,dst);
                        perror(errmsg);
			Error = 1;
                    }
		}
            } else {
                if(unlink(dst)){
                    sprintf(errmsg,"E unlink(\"%s\")",dst);
                    perror(errmsg);
		    Error = 1;
                } else {
                    if(symlink(src,dst)){
                        sprintf(errmsg, "E symlink(\"%s\",\"%s\")",src,dst);
                        perror(errmsg);
			Error = 1;
                    }
                    else
                        if (Verbose) fprintf(stderr, "+ %s -> %s\n", dst, src);
                }
            }
	}
    } else {
            /* nothing in the way. go for it */
	if(symlink(src,dst)){
            sprintf(errmsg, "E symlink(\"%s\",\"%s\")",src,dst);
            perror(errmsg);
	    Error = 1;
	}
	else
            if (Verbose) fprintf(stderr, "+ %s -> %s\n", dst, src);
    }
}

void do_unlink(char *src, char *dst)
{
    struct stat sinfo;
    int l;
    
    if(!stat(dst,&sinfo)){
            /* destination exists */
	if(Force){
            if(unlink(dst)){
		sprintf(errmsg, "E unlink(\"%s\")", dst);
		perror(errmsg);
		Error = 1;
            } 
            else
		if (Verbose) fprintf(stderr,"- %s -> %s\n", dst, src);
	} else {
            if(sinfo.st_mode & S_IFLNK){
                    /* and it's a link */
		if((l = readlink(dst,buf,MAX_PATH_LEN*3-1)) != -1){
                    buf[l]=0;
                    if(!strcmp(src,buf)){
                            /* they match */
			if(unlink(dst)){
                            sprintf(errmsg, "E unlink(\"%s\")", dst);
                            perror(errmsg);
			    Error = 1;
			} else {                            
                            if (Verbose)
                                fprintf(stderr,"- %s -> %s\n", dst, src);
                        }
                    } else {
			sprintf(errmsg,"E (%s) is not in this package (try -f)",
			    dst);
			fprintf(stderr, "%s\n", errmsg);
			Error = 1;
                    }
		} else {
                    sprintf(errmsg,"E readlink(\"%s\")",dst);
                    perror(errmsg);
		    Error = 1;
		}
            } else {
		sprintf(errmsg, "E not a link \"%s\" (try -f)", dst);
		fprintf(stderr, "%s\n", errmsg);
		Error = 1;
            }
	}
    } else {
	if(Verbose) {
            fprintf(stderr, "? destination already gone \"%s\"\n", dst);
	}
    }
}

void load_excludes(char *src, char ***excludes, char **buffer)
{
    struct stat sinfo;
    int ex_fd,bytes,cnt,st;
    
        /* first, see if there's an encap.exclude file, and read it */
    strcpy(buf, src);
    strcat(buf, "/encap.exclude");
    
    *buffer = NULL;
    *excludes = NULL;
    
    if (stat(buf, &sinfo)==0) {
            /* Read the file */
	ex_fd = open(buf, O_RDONLY);
	if (ex_fd > -1) {
            *buffer = malloc(sinfo.st_size+1);
            if (*buffer) {
		bytes = 0;
		cnt = 1;
		while (cnt > 0 && bytes<sinfo.st_size) {
                    cnt = read(ex_fd, *buffer+bytes, sinfo.st_size-bytes);
                    if (cnt > 0) 
			bytes+=cnt;
		}
            }
            close(ex_fd);
            *buffer[sinfo.st_size] = '\0';
	}
            /* Now split it */
	if (*buffer) {
                /* Count the lines */
            for (cnt=0, bytes=0; bytes<sinfo.st_size; bytes++)
		if (*buffer[bytes] == '\n')  
                    cnt++;
                /* Allocate the pointers and split the buffer */
            *excludes = malloc((cnt+1)*sizeof(char*));
            if (*excludes) {
		for (cnt=0, bytes=0, st=0; bytes<sinfo.st_size; bytes++) {
                    if (*buffer[bytes] == '\n') {
			*excludes[cnt] = *buffer+st;
			*buffer[bytes] = '\0';
			st = bytes+1;
			cnt++;
                    }
		}
		*excludes[cnt] = NULL;
            }
	}
    }
}

void free_excludes(char ***excludes, char **buffer)
{    
    /* deallocate the excludes list */
    if (*excludes)
	free(*excludes);
    if (*buffer)
	free(*buffer);
}


/* recursively symlink from destpath to path. build dirs as needed */
void encap(void)
{
    struct stat sinfo;
    struct dirent *dinfo;
    DIR *dp;
    int s,d;
    int ignore, i;
    
    char *buffer;
    char **excludes;

    if(Verbose)
	printf("%s:\n",src);

    /* does our destination exist? */
    if(stat(dst,&sinfo)){
	if(mkdir(dst,0755)){
            sprintf(errmsg,"E mkdir(\"%s\",0755)",dst);
            perror(errmsg);
	    Error = 1;
            return;
	}
    } else if(!(sinfo.st_mode & S_IFDIR)){
	sprintf(errmsg,"E \"%s\" is not a directory",dst);
	fprintf(stderr,"%s\n", errmsg);
	Error = 1;
	return;
    }

    /* so far so good.  got a source and dest dir. let's link 'em */

    load_excludes(src, &excludes, &buffer);
    
        /* walk the directory */
    if(!(dp = opendir(src))){
	sprintf(errmsg,"E Cannot opendir(\"%s\")",src);
	perror(errmsg);
	Error = 1;
	return;
    }

        /* save the name of this dir in our list of dirs */
    strcpy(buf, trackdir);
    strcat(buf, "/");
    strcat(buf, dst+strlen(localdir));
    mkdir(buf, 0755);

        /* tack on a '/' */
    strcat(dst,"/");
    strcat(src,"/");

    while((dinfo = readdir(dp))){

      /* as long as it's not "." or ".." ... */
	ignore = 0;
	if(strcmp(dinfo->d_name,".")==0 || strcmp(dinfo->d_name,"..")==0)
            ignore=1;

            /* ... or something in ignoredirs ... */
	for(i=0; ignoredirs[i]!=NULL; i++) {            
            if (strcmp(dinfo->d_name,ignoredirs[i])==0) {
		if (Verbose) 
		  fprintf(stderr,"Ignoring directory %s\n", dinfo->d_name);
		ignore = 1;
            }
        }

            /* ... or something in excludes ... */
	if (excludes) {            
            for(i=0; excludes[i]!=NULL; i++) {                
		if (strcmp(dinfo->d_name,excludes[i])==0) {
                    if (Verbose) 
			fprintf(stderr,"Excluding directory %s\n", 
			    dinfo->d_name);
                    ignore = 1;
		}
            }
        }

            /* ... then do the linking. */
	if (!ignore) {
                /* create full pathnames */
            s=strlen(src);
            d=strlen(dst);
            strcat(dst,dinfo->d_name);
            strcat(src,dinfo->d_name);

            if(stat(src,&sinfo)){
		sprintf(errmsg," Couldn't stat \"%s\"",src);
		perror(errmsg);
		Error = 1;
		continue;
            }
            
            if(sinfo.st_mode & S_IFDIR){
	      /* descend if it's a dir */
	      encap();
            } else {
		if(Options & OP_REMOVE){
                    do_unlink(src,dst);
		} else {
                    do_link(src,dst);
		}
            }       

	    /* remove the name we tacked on */
            dst[d]=0;
            src[s]=0;
	}
    }
    closedir(dp);  

    free_excludes(&excludes, &buffer);
    
    /* remove the '/' added earlier */
    dst[strlen(dst)-1]=0;
    src[strlen(src)-1]=0;
}

/* do some spring cleaning. remove any links that are ours that don't
   exist anymore, and then remove any resulting empty directories. 
   we limit the search to directories we've touched before so that we
   don't have to search all of /usr/local (or whatever) */
void do_inner_clean()
{
    int s;
    struct dirent *dinfo;
    struct stat sinfo;
    DIR *dir;
    int empty;
    int len;

    if (Verbose) fprintf(stderr,"Cleaning %s...\n", src);

    strcat(src, "/");
    s = strlen(src);
    dir = opendir(src);
    if (!dir) {
	sprintf(errmsg, "E Could not open directory %s", src);
	perror(errmsg);
	Error = 1;
	return;
    }

    while ((dinfo=readdir(dir))) {
	if (strcmp(dinfo->d_name, ".") && strcmp(dinfo->d_name, "..")) {
            empty = 0;
	    /* check the file */
            strcpy(src+s, dinfo->d_name);
            if (stat(src, &sinfo)) {
                    /* the file didn't exist; it's prolly a dead link */
		if (lstat(src, &sinfo)==0) 
                    if (sinfo.st_mode & S_IFLNK) 
                            /* it's a link; see if its ours */
			if ((len=readlink(src, dst, MAX_PATH_LEN))>-1) 
                            if (strncmp(dst, encapdir, edlen)==0) { 
				/* it's ours, nuke it */
				dst[len] = '\0';
				if (Verbose) 
				  fprintf(stderr,"- %s -> %s\n", src, dst);
				if (unlink(src)) {
                                    sprintf(errmsg, "E unlink(%s)", src);
                                    perror(errmsg);
				    Error = 1;
				}
                            }
            }	
#if 0
            else {
                    /* see if its a dir; if so, recurse */
		if (sinfo.st_mode & S_IFDIR) 
                    do_inner_clean();
            }
#endif
            src[s] = '\0';
	}
    }
    closedir(dir);

        /* If the directory is empty, then nuke it */
    empty = 1;
    dir = opendir(src);
    while (empty && (dinfo=readdir(dir))!=NULL) {
	if (strcmp(dinfo->d_name, ".") && strcmp(dinfo->d_name, ".."))
            empty = 0;
    }
    closedir(dir);

    if (empty) {
	if (Verbose) fprintf(stderr, "removing %s\n", src);
	if (unlink(src)) {
            sprintf(errmsg, "E Could not remove %s", src);
            perror(errmsg);
	    Error = 1;
	}
    }
}

/* look at the list of directories in trackdir, and clean the equivalents
   in localdir */
void do_clean()
{
    int p;
    struct dirent *dinfo;
    DIR *dir;
    struct stat sinfo;

    p = strlen(trk);

    dir = opendir(trk);
    if (!dir) {
	sprintf(errmsg, "E Couldn't open %s", trk);
	perror(errmsg);
	Error = 1;
	return;
    }

        /* see if there are subdirs.  if so recurse. */
    while ((dinfo = readdir(dir))) {
            /* skip . and .. */
	if (strcmp(dinfo->d_name, ".") && strcmp(dinfo->d_name, "..")) {
                /* recurse */
            trk[p] = '/'; trk[p+1] = '\0';
            strcat(trk, dinfo->d_name);
            do_clean();
                /* clean */
            strcpy(src, localdir);
            strcat(src, trk+strlen(trackdir));
            if (stat(src, &sinfo)) {
		if (Verbose) fprintf(stderr, "untracking %s\n", src);
		unlink(trk);
            }
            else
		do_inner_clean();
            trk[p] = '\0';
	}
    }
    closedir(dir);
}

int main(int argc, char *argv[])
{
    int i;
    char *p;
    int last_options = 0;

    umask(022);

    edlen = strlen(encapdir);

    if(argc==1)
	usage(argv[0]);
    
    strcpy(command, "install");
    for(i=1;i<argc;i++){
	if(argv[i][0]=='-'){
            p = &argv[i][1];
            while(*p){
		switch(*p){
		case 'v':
                    Verbose++;
                    break;
		case 'f':
                    Force++;
                    break;
		case 'i':
		    Options |= OP_INIT;
		    strcpy(command, "init");
                    break;
		case 'a':
		    Options |= OP_INSTALL;
		    strcpy(command, "install");
                    break;
		case 'r':
		    Options |= OP_REMOVE;
		    strcpy(command, "remove");
                    break;
		case 'm':
		    Options |= OP_MESG;
		    break;
		case 'c':
                    strcpy(trk, trackdir);
		    strcpy(command, "clean");
		    strcpy(package, "");
		    if (Options & OP_MESG) take_message();
                    do_clean();
		    if (!Error)
		      encap_log("", command, "Success");
		    else
		      encap_log("", command, errmsg);
                    break;
		    Options = 0;
		default:
                    fprintf(stderr,"Error: Invalid option \"%s\".\n",argv[i]);
                    usage(argv[0]);
		}
		p++;
            }
	} else {
                /* strip tailing slash */
            if(argv[i][strlen(argv[i])-1]=='/')
		argv[i][strlen(argv[i])-1]=0;

            if(strchr(argv[i],'/')){
		fprintf(stderr,"Error: Module names should not include '/'.\n");
		usage(argv[0]);
            }
	    strcpy(package, argv[i]); 
	    if (Options == 0) 
	      Options = (last_options ? last_options : OP_INSTALL);
	    last_options = Options;
	    if (Options & OP_MESG) 
	      take_message();
            if (Options & OP_INIT) {
		make_encap_dirs(argv[i]);
		if (!Error)
		  encap_log(package, command, "Success");
		else
		  encap_log(package, command, errmsg);
            } else {
		sprintf(src,"%s/%s",encapdir,argv[i]);
		strcpy(dst,localdir);
		encap();
		if (!Error)
		  encap_log(package, command, "Success");
		else
		  encap_log(package, command, errmsg);
            }
	    Options = 0;
	}
        
    }            
    return 0;
}

