#include <stdio.h>
#include <string.h>

const char verstring[] =
  "$VER: filename95 1.0 (21.12.96) Jeroen T. Vermeulen";
/*

NAME

    filename95


SYNOPSIS

    Renames files whose names have been mangled by a Windows 95 machine back to
    their original (long) names from a full recursive directory listing
    generated in Windows 95 by a "DIR /S" command.


DESCRIPTION

    The program traverses a directory to rename files based on a Windows
    recursive directory listing (this contains both the long and short filenames
    for each file).  This can be useful when taking eg. an LhA archive that was
    created on a Windows 95 PC (with all filenames truncated to 8.3 letters in
    true MS-DOS style) to your Amiga.

    I wrote this program to transfer the contents of my Amiga Developers' CD
    from a PC to my Amiga without any decent compression software available (ie.
    packers that support the rare practice of human-readable filenames).

    It worked.

    WARNING:  This program has only been tested in one case (albeit a big and
    complex one).  There is no guarantee whatsoever that it won't do any damage
    to your system and data.  THIS IS A VERY REAL CONSIDERATION in this case as
    the program is meant to touch a lot of files.  Make sure you have backed up
    any sensitive data before using it, preferably even files that are nowhere
    near the area of operations.  You never know.

    The program should work equally well under UNIX and AmigaOS, though only the
    latter has been tested.  Also I've only had the chance to use it on a
    directory listing generated with the Dutch version of Windows 95.  In a
    chivalrous effort to prevent communication between humans, Microsoft have
    carefully programmed their system software to not speak English unless you
    buy the Special English Version.

    Source in C is included, but ugly.  In keeping with the subject matter this
    is a Poorly Written Program.  Makefile for gcc included (type `make' to
    compile), plus SAS/C options file (type `sc fn95.c' to compile) and a StormC
    project file (double click to load, then do what comes natural).


USAGE

    fn95 <dirfile> [<basedir>]

    Where:

        <dirfile>       is the output of a Windows 95 "DIR /S" command
        <basedir>       is the directory where your files are.

    If no <basedir> is given, the current directory is assumed.  No other
    options are currently available.

    So the normal procedure for transporting the contents of a directory MyDir
    from a Windows 95 machine to an Amiga would be something like this:

      ON THE WINDOWS SIDE:
        1.  (Curse your fate.)
        2.  Lha -r -x a FILES.LHA MyDir             (pack directory)
        3.  Dir /S MyDir >NAMES.TXT                 (list filenames)
        4.  Move FILES.LHA A:                       (move archive to floppy)
        5.  Move NAMES.TXT A:                       (move dirlist to floppy)

      ON THE AMIGA SIDE:
        6.  (Breathe deeply.  Insert floppy in drive.)
        7.  Mount PC0:                              (mount CrossDOS if needed)
        8.  Lha x PC0:FILES.LHA                     (unpack directory)
        9.  fn95 PC0:NAMES.TXT MyDir                (restore filenames)

    NOTE that any errors that may occur while renaming will be dumped to the
    console but otherwise ignored.  This is done to allow you to rename
    partially restored directories without aborting over a few missing files.
    The current version isn't very specific about the nature of the error
    either, just letting you know when a file couldn't be renamed.

    If you wish to use fn95 to rename an entire CD's worth of files where a few
    hundred files or a few big directories are missing, be prepared to scroll
    through a lot of messages.


AUTHOR

    Jeroen T. Vermeulen, jtv@xs4all.nl / jvermeul@wi.leidenuniv.nl
             but please call me "Smith" when there's anybody watching.

    Copyright?  No way.  Do with it as you please, just don't remind me.


HISTORY

        None as yet, but I'm confident that it will repeat itself.

**/


/* I know, I know.  It's the lousiest source file you've ever seen.  I won't
 * give you the usual excuses, I am ashamed of myself, just write your own
 * software next time if you don't like it.
 */


/* This single glorious preprocessor directive represents 90% of the program's
 * configurability.  It determines how many words there are in the "leader"
 * above each individual directory listing in the listing file.
 * A typical leader should be something like "Directory of " followed by the
 * MS-DOS base directory.  But this depends on the *fucking* particular
 * language of the particular Windows 95 version used to generate the listing
 * and so far I've only seen the Dutch version where it's "Directory van ".
 * This obviously counts as 2 words.  If your mileage varies, change this
 * constant and recompile.
 */
#define LEADER_WORDS 2


/* Define this if text streams may still contain carriage-return/line-feed
 * pairs.  It really shouldn't, but this may depend on the C compiler used.
 * Defining this is the safest option, but also slightly slower (in theory).
 * In short:  You shouldn't really have to play with this ever.
 */
#define STRIP_CR

/*********** The fun starts here.  No more configuration options. ***********/

/* Get character from file, ignoring CRs, and return it.  If not succesful, an
 * error message is printed to stderr and the file is closed.
 * EOF is returned if not successful.
 */
static int mustgetc(FILE *const F)
{
  int c = getc(F);
#ifdef STRIP_CR
  if (c == '\r') c = getc(F);
#endif
  if (c == EOF)
  {
    fprintf(stderr, "Directory file appears to be truncated\n");
    fclose(F);
  }
  return c;
}


/* Skip any leading blank lines.  The first next character is returned, if
 * available.  Behaviour is as with mustgetc() otherwise.
 */
static int skip_empty_lines(FILE *const F)
{
  int c;
  do
  {
    c = mustgetc(F);
  } while ((char)c == '\n');
  return c;
}


/* Skip a given number of lines, ignoring their contents and leaving the file
 * pointer at the first next character.  So the return value is a boolean for
 * this function; if it is zero, an error message is displayed and the file
 * is closed.
 */
static int skip_lines(FILE *const F, int n)
{
  while (n--)
  {
    int c;
    do
    {
      c = mustgetc(F);
      if (c == EOF) return 0;
    } while (c != '\n');
  }
  return 1;
}


/* Skip "words" on the input.  The n parameter specifies the number of blank
 * spaces to ignore (these may consist of any number of blanks each), not the
 * actual number of non-blank sequences.  The next character is returned.
 * So to skip to the start of the next word, skip 1 word (that is, past 1
 * blank space) whether you're currently in whitespace or in text.
 * If n == 0, the return value will be zero.
 */
static int skip_words(FILE *const F, int n)
{
  int c = '\0';
  while (n)
  {
    c = mustgetc(F);
    if (c == EOF) return EOF;
    else if (c == ' ')
    {
      n--;
      do
      {
        c = mustgetc(F);
      }
      while (c == ' ');
      if (c == EOF) return EOF;
    }
  }
  return c;
}


/* Maximum acceptable length for path names (both MS-DOS and local).
 * Hope you won't mind if I make it a little large to be on the safe side.
 */
#define MAXPATHLEN 4000

/* A full path name.  Occasionally used for other purposes as well; the
 * observant reader will notice that it's really more like a special "string"
 * class (yes, Object-Oriented Programming) with a restricted length.
 */
struct fullpath
{
  int  len;
  char path[MAXPATHLEN+1];
};


struct fullpath MSbasedir,  /* MS-DOS base path                 */
                basedir,    /* Local base path                  */
                MScurdir,   /* Current MS-DOS directory         */
                curdir,     /* Current local directory          */
                dir_leader; /* String at start of directory listing */


/* This is where the old (short, MS-DOS style) and new (long, rest-of-world
 * style) filenames are constructed including their full paths.  The one is
 * then renamed to the other.
 * NOTE:  The paths are constructed for the local machine, so they are in the
 * "rest-of-world" style with normal, everyday, common and intellible SLASHES
 * as separators--rather than the "insane" style depraved arcane neo-Nazi
 * backslash-ridden aberration that we see under MS-DOS.
 * Thus oldnamebuf contains a UNIX/Amiga-like path ending in an MS-DOS-style
 * filename.  Only this name is then changed.
 */
static char oldnamebuf[MAXPATHLEN + 12],
            newnamebuf[MAXPATHLEN + 12];


/* Skip over the date/time indication in a line of the input file.  This is
 * tacky but hey, if MS are willing to consider open standards someday I'll be
 * happy to revise my code.  Nobody defined the format so we can only guess.
 */
static int skip_time(FILE *const F)
{
  int c;

  do
  {
    c = mustgetc(F);
    if (c == EOF) return EOF;
    if (c == '\n')
    {
      fprintf(stderr,
        "Parse error:  Truncated line in \"%s\"\n",
        MScurdir.path);
      fclose(F);
      return EOF;
    }
  } while (c != ' ');
  return c;
}


/* Find the string that preceeds each directory in the listing.  Unfortunately
 * this string depends entirely on the *language* of the Windows 95 that was
 * used to generate the listing so all we can do is skip a fixed number of
 * words.
 * This is Very Bad Indeed.  Blame Gates.
 */
static int get_dirleader(FILE *const F)
{
  int blanks = 0, c, p = 0;
  while (blanks < LEADER_WORDS)
  {
    c = mustgetc(F);
    if (c == '\n')
    {
      dir_leader.path[p] = '\0';
      if (p)
      {
        fprintf(stderr,
          "Parse error:  Directory leader is shorter than I expected\n"
          "(what I have now is \"%s\")\n",
          dir_leader.path);
        fclose(F);
        return 0;
      }
      else c = mustgetc(F);
    }
    if (c == EOF) return 0;
    if (c == ' ') blanks++;
    dir_leader.path[p++] = c;
  }
  dir_leader.path[p] = '\0';
  dir_leader.len = p;
  if (dir_leader.path[0] == ' ')
  {
    fprintf(stderr,
      "Parse error:  Directory leader begins with a blank\n"
      "(what I have now is \"%s\"\n",
      dir_leader.path);
    fclose(F);
    return 0;
  }
  if (dir_leader.len == 0)
  {
    fprintf(stderr, "Parse error:  Couldn't find directory leader\n");
    fclose(F);
    return 0;
  }
  printf("Using \"%s\" as directory leader\n", dir_leader.path);
  return 1;
}


/* Once we've skipped the directory "leader" string, we can read the name of
 * the upcoming directory.
 */
static int getdirname(FILE *const F, struct fullpath *const P)
{
  int c, len = 0;

  P->len = 0;
  do
  {
    c = mustgetc(F);
    if (c == EOF) return 0;
    P->path[len++] = (char)c;
    if (len >= MAXPATHLEN)
    {
      fprintf(stderr, "Parse error:  MS-DOS directory name is too long!\n");
      fclose(F);
      return 0;
    }
  } while (c != '\n');

  if (len < 3)
  {
    fprintf(stderr,
            "Parse error:  Blank line where I expected directory name\n");
    fclose(F);
    return 0;
  }

  len -= 2;
  c = P->path[len];
  P->path[len] = '\0';
  P->len = len;

  if ((P->path[len-1] != ':') && (P->path[len-1] != '\\'))
  {
    P->path[len] = '\\';
    len++;
    P->path[len] = '\0';
    P->len = len;
  }

  if (c != '.')
  {
    fprintf(stderr,
      "Parse error--Expected dot after directory name\n"
      "(name was \"%s\")\n",
      P->path);
    fclose(F);
    return 0;
  }
  return 1;
}


/* Set curdir to base dir, adding slash to both if necessary.  To better suit
 * the Amiga OS, no slash is added if the base path already ends in ":"
 * (meaning it's an assign or volume name).
 */
static void setorgdir(struct fullpath *const base,
                      struct fullpath *const local)
{
  if (base->len && (base->path[base->len-1] != ':') &&
      (base->path[base->len-1] != '/'))
  {
    base->path[base->len++] = '/';
    base->path[base->len] = '\0';
  }
  local->len = base->len;
  strcpy(local->path, base->path);
  strcpy(oldnamebuf, base->path);
  strcpy(newnamebuf, base->path);
}


/* Strip the MS-DOS base path from a directory name, checking if its initial
 * part matches the MSbasedir path, replace that with the basedir path if any,
 * and of course convert backslashes to slashes.
 *
 * Aliasing between the MS and local arguments is NOT allowed; make sure
 * they're different (and valid) pointers.
 */
static int setcurdir(const struct fullpath *const MS,
                     struct fullpath       *const local)
{
  const int newprt = MS->len - MSbasedir.len,
            newlen = newprt + basedir.len;

  int p;

  if (strncmp(MS->path, MSbasedir.path, MSbasedir.len) != 0)
  {
    fprintf(stderr,
      "Parse error:  Directory path does not match base dir!\n"
      "(path was \"%s\")\n",
      MS->path);
    return 0;
  }
  if (newlen >= MAXPATHLEN)
  {
    fprintf(stderr, "Local subdirectory path is too long!\n");
    if (basedir.len)
      fprintf(stderr,
        "Try cd'ing to \"%s\" first, then running me without the basedir "
        "argument.\n",
        basedir.path);
    return 0;
  }

  local->len = newlen;
  memcpy(local->path, basedir.path, basedir.len);
  memcpy(local->path + basedir.len, MS->path + MSbasedir.len, newprt);
  for (p=basedir.len; p<newlen; p++)
    if (local->path[p]=='\\') local->path[p] = '/';
  local->path[newlen] = '\0';
  strcpy(oldnamebuf, local->path);
  strcpy(newnamebuf, local->path);
  return 1;
}


/* Read through a full directory in the listing, renaming anything with a "~"
 * in it to its "long filename" on the way.  Anything else is left unchanged.
 */
int rename_block(FILE *const F)
{
  static char MSFileNameTM[13];

  for (;;)
  {
    int c, p = 0, x, tildes = 0;

    c = mustgetc(F);
    switch (c)
    {
      case EOF: return 1;
      case ' ': return skip_lines(F, 1); /* Indented summary line */
      case '\n':
        fprintf(stderr,
          "Parse error:  Looks like a missing summary line\n"
          "(directory is \"%s\")\n",
          curdir.path);
        fclose(F);
        return 0;
    }
    do
    {
      MSFileNameTM[p++] = (char)c;
      c = mustgetc(F);
      if (c == EOF) return 0;
    } while (p < 8);  /* Eight.  Sheesh.  Pathetic. */

    MSFileNameTM[p] = '\0';
    if (c != ' ')
    {
      fprintf(stderr,
        "Parse error:  Expected filename but 9th character is not blank\n"
        "(all I got was \"%s\")\n",
        MSFileNameTM);
      fclose(F);
      return 0;
    }

    /* Rewind to last non-blank character in filename */
    while (MSFileNameTM[--p] == ' ');
    p++;
    MSFileNameTM[p] = '\0';

    c = mustgetc(F);
    if (c == EOF) return 0;
    if (c != ' ')   /* Oh dear, an extension.  The mind boggles. */
    {
      MSFileNameTM[p++] = '.';
      do
      {
        MSFileNameTM[p++] = (char)c;
        c = mustgetc(F);
        if (c == EOF) return 0;
      } while ((p < 12) && (c != ' '));
      MSFileNameTM[p] = '\0';
      if (p > 12)
      {
        fprintf(stderr,
          "Parse error:  Apparent extension is too long\n"
          "(the filename I have now reads \"%s\")\n",
          MSFileNameTM);
        fclose(F);
        return 0;
      }
    }

    x = p;
    while (x--) switch (MSFileNameTM[x])
    {
      case ' ':
        fprintf(stderr,
          "Parse error:  Apparent filename contains blank\n"
          "(the filename I have now reads \"%s\")\n",
          MSFileNameTM);
        fclose(F);
        return 0;
      case '\n':
        fprintf(stderr,
          "Parse error:  Apparent filename contains a line feed\n"
          "(the filename I have now reads \"%s\")\n",
          MSFileNameTM);
        fclose(F);
        return 0;
      case '~':
        tildes++;
        break;
      default:
        break;
    }
    if (tildes)
    {
      int q;

      if (tildes > 1)
      {
        fprintf(stderr,
          "Parse error:  Apparent filename contains multiple ~ signs\n"
          "(filename was \"%s\")\n",
          MSFileNameTM);
        fclose(F);
        return 0;
      }

      strcpy(oldnamebuf + curdir.len, MSFileNameTM);

      c = skip_words(F, 3);
      if (c == EOF) return 0;
      c = skip_time(F);
      if (c == EOF) return 0;
      q = curdir.len;
      do
      {
        c = mustgetc(F);
        if (c == EOF) return 0;
        newnamebuf[q++] = (char)c;
        if (q >= MAXPATHLEN)
        {
          fprintf(stderr,
            "Error:  Name for file \"%s\" is too long!\n"
            "(directory was \"%s\")\n",
            newnamebuf,
            curdir.path);
          fclose(F);
          return 0;
        }
      } while (c != '\n');
      newnamebuf[--q] = '\0';
      if (q == curdir.len)
      {
        fprintf(stderr,
          "Error:  No long filename found for \"%s\"\n"
          "(directory was \"%s\")\n",
          MSFileNameTM,
          curdir.path);
        fclose(F);
        return 0;
      }
      if (rename(oldnamebuf, newnamebuf) != 0)
      {
        fprintf(stderr,
          "Error for \"%s\" --> \"%s\"\n",
          oldnamebuf,
          newnamebuf);
      }
    }
    else if (!skip_lines(F, 1)) return 0;
  }
}


/* Done with directory.  See if there is another; return 1 if yes, 0 if no.
 * The file is closed if not; note that there may be some trailing trash in
 * the directory file that will be ignored.  The program may therefore abort
 * quietly if a parse error occurs at this point.
 */
static int get_next_dir(FILE *const F)
{
  int c, p = 0;

  c = skip_empty_lines(F);
  if (c == EOF) return 0;

  while (p < (dir_leader.len-1))
  {
    if ((char)c != dir_leader.path[p++])
    {
      fclose(F);
      return 0;
    }
    c = mustgetc(F);
    if (c == EOF) return 0;
  }
  if ((char)c != dir_leader.path[p])
  {
    fclose(F);
    return 0;
  }
  return 1;
}


int main(int argc, char *argv[])
{
  FILE *dirfile;

  if ((argc != 2) && (argc != 3))
  {
    fprintf(stderr,
      "Usage: %s <dirfile> [<basedir>]\nWhere:\n"
      "<dirfile>\t"  "is the output of a Windows 95 \"DIR /S\" command\n"
      "<basedir>\t" "is the directory where your files are\n",
      argv[0]);
    return 1;
  }

  if (argc >= 3)
  {
    basedir.len = strlen(argv[2]);
    if (basedir.len > MAXPATHLEN)
    {
      fprintf(stderr,
        "Base directory name too long!\n"
        "Try cd'ing to it first, then run %s without the <basedir> "
        "argument\n",
        argv[0]);
      return 1;
    }
    strcpy(basedir.path, argv[2]);
  }
  else
  {
    basedir.path[0] = '\0';
    basedir.len = 0;
  }
  setorgdir(&basedir, &curdir);

  dirfile = fopen(argv[1],"r");
  if (!dirfile)
  {
    fprintf(stderr, "Couldn't open directory file %s for reading\n", argv[1]);
    return 1;
  }

  /* Skip any leading blank lines */
  if (skip_empty_lines(dirfile) == EOF) return 1;
  /* Skip volume name and number (two lines) */
  if (!skip_lines(dirfile, 3)) return 1;
  /* Learn the "Directory of" keyphrase preceding each directory listing */
  if (!get_dirleader(dirfile)) return 1;
  /* Read base directory path under MS-DOS, stripping dot and newline */
  if (!getdirname(dirfile, &MSbasedir)) return 1;
  if (!MSbasedir.len)
  {
    fprintf(stderr,
      "Parse error:  Zero-length MS-DOS base directory name!\n");
    fclose(dirfile);
    return 1;
  }
  printf("Base directory:\t\"%s\"\n", MSbasedir.path);
  if (basedir.len) printf("Local directory:\t\"%s\"\n", basedir.path);

  for (;;)
  {
    /* Skip 1 blank line and the . and .. directories */
    if (!skip_lines(dirfile, 3)) return 1;
    /* Read block of file/dir names and rename where necessary */
    if (!rename_block(dirfile)) return 1;
    /* See if there is a new directory in the list */
    if (!get_next_dir(dirfile)) return 0;
    /* Get new MS-DOS directory name */
    if (!getdirname(dirfile, &MScurdir)) return 1;
    printf("MS-DOS directory:\t\"%s\"\n", MScurdir.path);
    /* Translate directory name to local directory name */
    if (!setcurdir(&MScurdir, &curdir))
    {
      fclose(dirfile);
      return 1;
    }
    printf("Local directory:\t\"%s\"\n", curdir.path);
  }
}

