/*
From: a-giles@uchicago.edu          Article 2813 of alt.binaries.pictures.d

Here's the latest version of my archive extractor.  Special features
and such are described in the comments at the beginning of the
program.  This was written in ANSI standard C under unix, and there
is most certainly some unix-specific code.  If you're running unix and
your compiler doesn't like my program, try compiling it with the GNU C
compiler gcc, if you have it.  (I've had a lot of complaints from
people who still have the old, non-ANSI C compilers on their systems!)
Anyhow, compiling the program should be straightforward once you've
got an ANSI-compliant compiler.

Enjoy!

Aaron
*/
--------------------------( CUT HERE  8< )-----------------------------------
/*
 * uundo.c: extract a multi-part uuencoded archive               Version 1.2
 * written by Aaron Giles                                           02/21/92
 * 
 * usage:
 *   uundo [-Lloqv] file1 [file2 [...]]
 *   uundo [-Lloqv] < file
 *
 * options:
 *   -L  lower all: convert all characters in all filenames to lower case
 *   -l  lower: convert only all-upper-case filenames to lower case
 *   -o  overwrite: automatically overwrite existing files without permission
 *   -q  query: allow overwriting of existing files, but ask permission first
 *   -v  verbose: display part numbers and status info
 *
 * special features:
 *   - does not require all parts to be in order*
 *   - attempts to identify missing parts*
 *   - works on uuencoded files included in shar archives
 *   - can accept either single or multiple files on the command line
 *   - can accept stream input from stdin
 * (* requires part numbering informating in the subject line of each part)
 *
 * bugs/requests/modifications to:
 *   a-giles@uchicago.edu     -or-
 *   gile@midway.uchicago.edu -or-
 *   giles@hep.uchicago.edu
 *
 * philosophy:
 *   I will try my best to keep up with any bug reports (first priority) or
 *   new feature requests.  Note that my primary goal is robustness, so that
 *   requests which would overly complicate things or result in "flaky" code
 *   will very likely not be put in.
 *
 * copyright control:
 *   This program is public domain, though copyrighted (c) 1992 by its
 *   author, Aaron Giles.  If you wish to distribute modified copies of
 *   this program, please contact the author first.
 *
 * standard disclaimer:
 *   Use this program at your own risk.  The author takes no responsibility
 *   for any loss of data or damage that results from using this program.
 *
 * known problems/limitations:
 *   - cannot handle multiple target files in the same input file (sorry!)
 *   - can't yet accept input piped from rn or trn
 *
 * to be added in future versions:
 *   - support for saving header information
 *   - support for piping, allowing use with rn/trn
 *   - support for a maximum filename length
 *
 * version history:
 *   1.0 (2/11/91) - initial release
 *   1.1 (2/13/91) - added stdin support for use with rn
 *                   no longer so restrictive on the first line of each part
 *                   added verbose option
 *                   added a warning to overwrite existing files
 *                   added an option to turn off this warning
 *                   now attempts to make file writeable before overwriting
 *                   fixed problem with "BEGIN..." lines fooling the decoder
 *   1.2 (2/21/91) - added force lowercase options
 *                   changed procedure dealing with conflicting filenames
 *                   added query option
 *                   now uses process ID in temporary names to avoid conflicts
 *                   output directory can be set via UUNDO environment variable
 */

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

/* GLOBAL DEFINITIONS */
#define PATHSEP '/'        /* path separator character */
#define MAXLEN 256         /* maximum length of strings/pathnames */
#define MAXPARTS 256       /* maximum number of separate parts in file */
#define BUFSIZE 32768      /* size of temporary buffer */
#define TEMPNAME "/usr/tmp/uu"
                           /* prefix for temporary files */

/* GLOBAL VARIABLES */
int debug = 0;             /* debug flag */
int Lower = 0;             /* all lower case flag */
int lower = 0;             /* lower case flag */
int overwrite = 0;         /* overwrite flag */
int query = 0;             /* query flag */
int verbose = 0;           /* verbose flag */

int part = -1, total = 0;  /* current part number and total count */
int pending[MAXPARTS];     /* table of pending entries */

int partswritten = -2;     /* total number of parts written to target */
long int byteswritten = 0; /* count of the number of bytes written to target */

FILE *targetfile;          /* stream pointer for target file */
char targetname[MAXLEN];   /* name of target file */
char *targetpath;          /* pointer to path string for output directory */
int targetmode;            /* file mode for final target */

pid_t pid;                 /* process ID of this program */

/* createfile: attempt to create the named file */
FILE *createfile(char *name, char *type) {
  FILE *file;
  if (!(file = fopen(name, "w")))
    fprintf(stderr, "uundo: unable to create %s file %s\n", type, name);
  return file;
}

/* openfile: attempt to open the named file for reading */
FILE *openfile(char *name, char *type) {
  FILE *file;
  if (!(file = fopen(name, "r")))
    fprintf(stderr, "uundo: unable to open %s file %s\n", type, name);
  return file;
}

/* deletefile: remove the named temporary file */
void deletefile(char *name) {
  chmod(name, 0666);
  if (unlink(name))
    fprintf(stderr, "uundo: unable to delete temporary file %s\n", name);
}

/* checkfile: check for existence of output file before overwriting */
int checkfile(char *name) {
  FILE *file;
  char s[MAXLEN];
  char *end = name + strlen(name);
  int count = 0;

  while (file = fopen(name, "r")) {
    fclose(file);
    if (overwrite) {
      deletefile(name);
      return 1;
    } else if (query) {
      fprintf(stderr, "uundo: %s already exists: overwrite? ", name);
      gets(s);
      if ((*s == 'Y') || (*s == 'y')) {
	deletefile(name);
	return 1;
      }
    }
    sprintf(end, ".%02i", count++);
  }
  return 1;
}

/* readline: input a line and strip the newline character */
int readline(FILE *file, char *s) {
  int l;

  if (!fgets(s, MAXLEN, file)) return 0;
  if ((l = strlen(s)) == (MAXLEN - 1)) {
    fprintf(stderr, "uundo: invalid input file format\n");
    exit(1);
  }
  s[l - 1] = 0;
  return 1;
}

/* tempname: put temporary file name for part num into string variable name */
void tempname(int num, char *name) {
  strcpy(name, TEMPNAME);
  sprintf(name+strlen(name), "%ld.%03i", pid, num);
  return;
}

/* uuline: determine if the given line is a correct uuencoded line */
int uuline(char *s, int full) {
  int l, len, max, min;
  
  if ((strncasecmp(s, "end", 3) == 0) ||
      (strncasecmp(s, "begin", 5) == 0) ||
      ((l = s[0] - ' ') & 0xc0)) return 0;
  min = ((l * 4) + 2) / 3;
  max = ((min + 3) / 4) * 4 + 1;
  len = strlen(s) - 1;
  if ((len < min) || (len > max)) return 0;
  if (!full && ((l == 45) || (l == 0))) return 1;
  for (s++; *s; s++)
    if ((*s < ' ') || (*s > '`')) return 0;
  return 1;
}

/* decode a uuencoded line and output the binary data to ouf */
void decode(FILE *outf, char *line) {
  int count = line[0] - ' ';
  int i, j;

  if (debug) printf("%s\n", line);
  for (j = 1; line[j]; j++) line[j] = (line[j] - ' ') & 0x3f;
  for (i = 0, j = 1; i < count; i += 3, j += 4) {
    line[i]   = ((line[j]   << 2) | (line[j+1] >> 4)) & 0xff;
    line[i+1] = ((line[j+1] << 4) | (line[j+2] >> 2)) & 0xff;
    line[i+2] = ((line[j+2] << 6) | (line[j+3]     )) & 0xff;
  }
  if (count) {
    if (outf == targetfile) byteswritten += count;
    if (count -= fwrite(line, 1, count, outf)) {
      fprintf(stderr, "uundo: unable to write to output file\n");
      exit(1);
    }
  }
}

/* append pending file to the target */
void unpend(int number) {
  int i;
  FILE *inf;
  char inname[MAXLEN];
  char buffer[BUFSIZE];

  tempname(number, inname);
  if (!(inf = openfile(inname,"temporary"))) exit(1);
  while (i = fread(buffer, 1, BUFSIZE, inf)) {
    if (fwrite(buffer, 1, i, targetfile) != i) {
      fprintf(stderr, "uundo: unable to append temporary file %s\n", inname);
      exit(1);
    } else
      byteswritten += i;
  }
  fclose(inf);
  deletefile(inname);
  pending[number] = 0;
}

/* determine part number and total number from subject header line */
void parse(char *p) {
  char *q;
  int i;
  
  part = -1;
  for (; *p; p++) {
    switch (*p) {
      case 'P': case 'p':
        if (isalpha(*(p - 1)) || (strncasecmp(p, "part", 4)) ||
	    isalpha(*(p + 4))) continue;
	p += 4;
	break;
      case '(': case '{': case '[':
	p++;
	break;
      case 'O': case 'o':
	if (isalpha(*(p - 1)) || (strncasecmp(p, "of", 2)) ||
	    isalpha(*(p + 2))) continue;
      case '/':
	for (q = p; isspace(*(q - 1)); q--);
	if (!isdigit(*(q - 1))) continue;
	while (isdigit(*(q - 1))) q--;
	if (isalpha(*(q - 1))) continue;
	p = q;
	break;
      default:
	continue;
    }      
    i = (int)strtol(p, &q, 10);
    if (p == q) { p--; continue; }
    part = i;
    for (p = q; isspace(*p); p++);
    switch (*p) {
      case '/':
        p++;
	break;
      case 'o': case 'O':
	if (strncasecmp(p, "of", 2) == 0) {
	  p+=2;
	  break;
	}
      default:
	p--;
	continue;
    }
    while (isspace(*p)) p++;
    i = (int)strtol(p, &q, 10);
    if (p == q) { p--; continue; }
    total = i;
    if (verbose) fprintf(stderr, "uundo: found part %i of %i\n", part, total);
    return;
  }
  if (part == -1)
    fprintf(stderr, "uundo: warning: unable to find part number\n");
  else if (verbose)
    fprintf(stderr, "uundo: found part %i\n", part);
}

/* initfile: parse the uuencode header line and initialize the output file */
int initfile(char *p) {
  char *q;
  
  if (targetpath) {
    strcpy(targetname, targetpath);
    targetname[strlen(targetname) + 1] = 0;
    targetname[strlen(targetname)] = PATHSEP;
  } else
    targetname[0] = 0;
  if (partswritten > 0) {
    fprintf(stderr, "uundo: found extra part 1, ignored\n");
    return 1;
  }
  while (isspace(*p) && *p) p++;
  if (*p) targetmode = (int)strtol(p, (char **)NULL, 8);
  else targetmode = 0666;
  while (!isspace(*p) && *p) p++;
  while (isspace(*p) && *p) p++;
  if (*p) {
    if (targetpath)
      for (q = p + strlen(p) - 1; (q > p) && (*(q - 1) != PATHSEP); q--);
    else
      q = p;
    strcat(targetname, q);
  } else {
    strcat(targetname, "a.out");
    fprintf(stderr, "uundo: filename not found, using a.out instead\n");
  }
  for (p = targetname + strlen(targetname) - 1;
       !islower(*p) && (*p != PATHSEP) && (p >= targetname); p--);
  if (Lower || (lower && ((*p == PATHSEP) || (p < targetname))))
    for (p++; *p; *p = tolower(*p), p++);
  if (!checkfile(targetname)) exit(1);
  if (!(targetfile = createfile(targetname, "output"))) exit(1);
  partswritten = 0;
  return 0;
}

/* startpart: figure out where to output the newly-found part */
FILE *startpart() {
  char name[MAXLEN];
  FILE *outf;
  
  if (partswritten > 0)
    while (pending[partswritten + 1]) unpend(++partswritten);
  if ((part == -1) && (partswritten < 0)) {
    fprintf(stderr, "uundo: unnumbered parts found out of order\n");
    exit(1);
  }
  if ((part == -1) || (part == (partswritten + 1)))
    return targetfile;
  if ((part <= partswritten) || (pending[part])) {
    fprintf(stderr, "uundo: found extra part %i, ignored\n", part);
    return (FILE *)NULL;
  }
  pending[part] = 1;
  tempname(part, name);
  if (!(outf = createfile(name, "temporary"))) exit(1);
  return outf;
}

/* init: initialize pending array and do general setup */
int init(int argc, char *argv[]) {
  int i;
  char *p;

  pid = getpid();
  targetpath = getenv("UUNDO");
  for (i = 0; i < MAXPARTS; pending[i++] = 0);
  for (i = 1; *argv[i] == '-'; i++) {
    for (p = argv[i] + 1; *p; p++) {
      switch (*p) {
        case 'd': debug = 1; break;
	case 'L': Lower = 1; break;
	case 'l': lower = 1; break;
	case 'o': overwrite = 1; break;
	case 'q': query = 1; break;
        case 'v': verbose = 1; break;
      }
    }
  }
  return i;
}

/* findstart -- parse the header and skip garbage before uuencoded data */
FILE *findstart(FILE *inf) {
  FILE *outf;
  char line[MAXLEN];
  int ignore = 0;
  
  while (readline(inf, line)) {
    if (!line[0]) continue;
    if (strncasecmp(line, "Subject:", 8) == 0) {
      ignore = 0;
      parse(line + 8);
    } else if (strncmp(line, "begin ", 6) == 0) {
      if (!(ignore = initfile(line + 6)))
	return targetfile;
    } else if (!ignore && uuline(line, 1) &&
	       (line[0] != '#') && (line[0] != '-') && (line[0] != ' ')) {
      if (outf = startpart()) {
	decode(outf, line);
	return outf;
      } else ignore = 1;
    }
  }
  return (FILE *)NULL;
}
    
/* clean up our little mess */
void cleanup() {
  char name[MAXLEN];
  int i;
  
  if (partswritten < 0)
    fprintf(stderr, "uundo: missing file part 1\n");
  else {
    while (pending[partswritten + 1]) unpend(++partswritten);
    if ((total) && (partswritten < total)) 
      fprintf(stderr, "uundo: missing part %i of %i\n", partswritten+1, total);
    else if (verbose)
      fprintf(stderr, "Wrote file %s (%i parts, %ld bytes)\n", targetname,
	      partswritten, byteswritten);
  }
  fclose(targetfile);
  chmod(targetname, targetmode);
  for (i = 0; i < MAXPARTS; i++)
    if (pending[i]) {
      tempname(i, name);
      deletefile(name);
    }
}

/* main program loop */
int main(int argc, char *argv[]) {
  FILE *infile = stdin, *outfile;
  int fptr, start;
  char line[MAXLEN];

  start = init(argc, argv);
  for (fptr = start;
       (fptr < argc) || ((fptr == start) && (argc == start)); fptr++) {
    if ((argc > start) && (!(infile = openfile(argv[fptr], "input"))))
      continue;
    while (outfile = findstart(infile)) {
      while (readline(infile, line)) {
	if (!uuline(line, 0)) break;
	decode(outfile, line);
      }
      if (outfile != targetfile)
	fclose(outfile);
      else
	partswritten++;
    }
    fclose(infile);
  }
  cleanup();
}
