
                              CD-ROM Maker
                           Philip J. Erdelsky

The CDMAKE utility converts files from DOS/Windows format to ISO9660
(CD-ROM) format.

First, gather all the files to be converted and put them into a single
base directory and its subdirectories, arranged just the way you want
them on the CD-ROM. Remember that ISO9660 allows subdirectories to be
nested only eight levels deep. Therefore, if the base directory is
C:\CDROM,

     C:\CDROM\D2\D3\D4\D5\D6\D7\D8\FOO.TXT is permitted, but

     C:\CDROM\D2\D3\D4\D5\D6\D7\D8\D9\FOO.TXT is forbidden.

Also, ISO9660 does not allow directories to have extensions, although
DOS does.

Finally, the characters in file and directory names and file extensions
must be letters, digits or underscores. Other punctuation marks
permitted by DOS/Windows are forbidden by ISO9660. You can use the -c
option to override this restriction, but the resulting CD-ROM may not be
readable on systems other than DOS/Windows.

Files in the base directory will be written to the root directory of the
CD-ROM image. All subdirectories of the base directory will appear as
subdirectories of the root directory of the CD-ROM image. Their
contents, and the contents of their subdirectories, down to the eighth
level, will be faithfully copied to the CD-ROM image.

System files will not be written to the CD-ROM image. Hidden files will
be written to the CD-ROM image, and will retain their hidden attributes.
Read-only files will be written, and will remain read-only on the
CD-ROM, but this does not distinguish them in any way, because on a
CD-ROM all files are read-only. The archive attribute will be lost.

File and directory date and time stamps will be preserved in the CD-ROM
image.

The utility is called up by a command line of the following form:

     CDMAKE  [-q] [-v] [-p] [-s N]  source  volume  image

     source      specifications of base directory containing all files to
                 be written to CD-ROM image

     volume      volume label

     image       image file or device

     -q          quiet mode - display nothing but error messages

     -v          verbose mode - display file information as files are
                 scanned and written - overrides -p option

     -p          show progress while writing

     -s N        abort operation before beginning write if image will be
                 larger than N megabytes (i.e. 1024*1024*N bytes)

     -m          accept punctuation marks other than underscores in
                 names and extensions

The utility makes three passes over the source files:

     (1) The scanning pass, in which the names and extensions are
         checked for validity, and the names, extensions, sizes, dates,
         times and attributes are recorded internally. The files are not
         actually read during this pass.

     (2) The layout pass, in which the sizes and positions of
         directories, files and other items in the CD-ROM image are
         determined.

     (3) The writing pass, in which the files are actually read and the
         CD-ROM image is actually written to the specified file or
         device. The image is always written sequentially.

If neither the -q nor the -v option is used, CDMAKE will display the
volume label, size, number of files and directories and the total bytes
in each at the end of the layout pass.

If the -p option is used, and is not overridden by the -v option, then
during the writing pass, CDMAKE will display the number of bytes still
to be written to the CD-ROM image, updating it frequently. The number
will decrease as the operation progresses, and will reach zero when the
operation is complete.

The operation of CDMAKE can be aborted by typing Ctrl-C when the utility
is displaying text of any kind.

// CD-ROM Maker

#include <stdio.h>
#include <fcntl.h>
#include <sys\stat.h>
#include <stdlib.h>
#include <string.h>
#include <dir.h>
#include <dos.h>
#include <ctype.h>
#include <setjmp.h>
#include <io.h>

extern "C" {
  struct directory_record *sort_linked_list(struct directory_record *,
    unsigned, int (*)(struct directory_record *, struct directory_record *));
}

typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef unsigned long DWORD;
typedef int BOOL;

const BOOL TRUE  = 1;
const BOOL FALSE = 0;

// file system parameters

const unsigned MAX_LEVEL            = 8;
const unsigned MAX_NAME_LENGTH      = 8;
const unsigned MAX_EXTENSION_LENGTH = 3;
const unsigned SECTOR_SIZE          = 2048;

const BYTE HIDDEN_FLAG    = 1;
const BYTE DIRECTORY_FLAG = 2;

const unsigned BUFFER_SIZE = 8*SECTOR_SIZE;

struct cd_image
{
  int handle;
  long sector;          // sector to receive next byte
  int offset;           // offset of next byte in sector
  int count;            // number of bytes in buffer
  char filespecs[128];
  BYTE *buffer;
};

struct date_and_time
{
  BYTE second;
  BYTE minute;
  BYTE hour;
  BYTE day;
  BYTE month;
  WORD year;
};

struct directory_record
{
  struct directory_record *next_in_directory;
  struct directory_record *next_in_path_table; /* directory record only */
  struct directory_record *next_in_memory;
  struct directory_record *first_record;       /* directory record only */
  struct directory_record *parent;
  BYTE flags;
  char name[MAX_NAME_LENGTH+1];
  char extension[MAX_EXTENSION_LENGTH+1];
  struct date_and_time date_and_time;
  DWORD sector;
  DWORD size;
  unsigned level;                             /* directory record only */
  WORD path_table_index;                      /* directory record only */
};

enum directory_record_type
  {DOT_RECORD, DOT_DOT_RECORD, SUBDIRECTORY_RECORD, FILE_RECORD};

static char DIRECTORY_TIMESTAMP[] = "~Y$'KOR$.3K&";

static jmp_buf error;
static struct cd_image cd;

static char volume_label[32];
static struct directory_record root;
static char source[512];
static char *end_source;
static enum {QUIET, NORMAL, VERBOSE} verbosity;
static BOOL show_progress;
static DWORD size_limit;
static BOOL accept_punctuation_marks;

static DWORD total_sectors;
static DWORD path_table_size;
static DWORD little_endian_path_table_sector;
static DWORD big_endian_path_table_sector;
static DWORD number_of_files;
static DWORD bytes_in_files;
static DWORD unused_bytes_at_ends_of_files;
static DWORD number_of_directories;
static DWORD bytes_in_directories;

/*-----------------------------------------------------------------------------
This function edits a 32-bit unsigned number into a comma-delimited form, such
as 4,294,967,295 for the largest possible number, and returns a pointer to a
static buffer containing the result. It suppresses leading zeros and commas,
but optionally pads the result with blanks at the left so the result is always
exactly 13 characters long (excluding the terminating zero).

CAUTION: A statement containing more than one call on this function, such as
printf("%s, %s", edit_with_commas(a), edit_with_commas(b)), will produce
incorrect results because all calls use the same static bufffer.
-----------------------------------------------------------------------------*/

static char *edit_with_commas(DWORD x, BOOL pad = TRUE)
{
  static char s[14];
  unsigned i = 13;
  do
  {
    if (i % 4 == 2) s[--i] = ',';
    s[--i] = x % 10 + '0';
  } while ((x/=10) != 0);
  if (pad)
  {
    while (i > 0) s[--i] = ' ';
  }
  return s + i;
}

/*-----------------------------------------------------------------------------
This function releases all allocated memory blocks.
-----------------------------------------------------------------------------*/

static void release_memory(void)
{
  while (root.next_in_memory != NULL)
  {
    struct directory_record *next =
      root.next_in_memory->next_in_memory;
    delete root.next_in_memory;
    root.next_in_memory = next;
  }
  if (cd.buffer != NULL)
  {
    delete cd.buffer;
    cd.buffer = NULL;
  }
}

/*-----------------------------------------------------------------------------
This function edits and displays an error message and then jumps back to the
error exit point in main().
-----------------------------------------------------------------------------*/

static void error_exit(const char *format, ...)
{
  vfprintf(stderr, format, &format + 1);
  fprintf(stderr, "\n");
  if (cd.handle >= 0)
    close(cd.handle);
  release_memory();
  longjmp(error, 1);
}

/*-----------------------------------------------------------------------------
This function, which is called only on the second pass, and only when the
buffer is not empty, flushes the buffer to the CD-ROM image.
-----------------------------------------------------------------------------*/

static void flush_buffer(void)
{
  if (write(cd.handle, cd.buffer, cd.count) < cd.count)
    error_exit("File write error");
  cd.count = 0;
  if (show_progress)
  {
    printf("\r%s ",
      edit_with_commas((total_sectors - cd.sector) * SECTOR_SIZE));
  }
}

/*-----------------------------------------------------------------------------
This function writes a single byte to the CD-ROM image. On the first pass (in
which cd.handle < 0), it does not actually write anything but merely updates
the file pointer as though the byte had been written.
-----------------------------------------------------------------------------*/

static void write_byte(BYTE x)
{
  if (cd.handle >= 0)
  {
    cd.buffer[cd.count] = x;
    if (++cd.count == BUFFER_SIZE)
      flush_buffer();
  }
  if (++cd.offset == SECTOR_SIZE)
  {
    cd.sector++;
    cd.offset = 0;
  }
}

/*-----------------------------------------------------------------------------
These functions write a word or double word to the CD-ROM image with the
specified endianity.
-----------------------------------------------------------------------------*/

static void write_little_endian(WORD x)
{
  write_byte(x);
  write_byte(x >> 8);
}

static void write_big_endian(WORD x)
{
  write_byte(x >> 8);
  write_byte(x);
}

static void write_both_endian(WORD x)
{
  write_little_endian(x);
  write_big_endian(x);
}

static void write_little_endian(DWORD x)
{
  write_byte(x);
  write_byte(x >> 8);
  write_byte(x >> 16);
  write_byte(x >> 24);
}

static void write_big_endian(DWORD x)
{
  write_byte(x >> 24);
  write_byte(x >> 16);
  write_byte(x >> 8);
  write_byte(x);
}

static void write_both_endian(DWORD x)
{
  write_little_endian(x);
  write_big_endian(x);
}

/*-----------------------------------------------------------------------------
This function writes enough zeros to fill out the end of a sector, and leaves
the file pointer at the beginning of the next sector. If the file pointer is
already at the beginning of a sector, it writes nothing.
-----------------------------------------------------------------------------*/

static void fill_sector(void)
{
  while (cd.offset != 0)
    write_byte(0);
}

/*-----------------------------------------------------------------------------
This function writes a string to the CD-ROM image. The terminating \0 is not
written.
-----------------------------------------------------------------------------*/

static void write_string(char *s)
{
  while (*s != 0)
    write_byte(*s++);
}

/*-----------------------------------------------------------------------------
This function writes a block of identical bytes to the CD-ROM image.
-----------------------------------------------------------------------------*/

static void write_block(unsigned count, BYTE value)
{
  while (count != 0)
  {
    write_byte(value);
    count--;
  }
}

/*-----------------------------------------------------------------------------
This function writes a directory record to the CD_ROM image.
-----------------------------------------------------------------------------*/

static void write_directory_record(directory_record *d,
  enum directory_record_type type)
{
  unsigned identifier_size;
  switch (type)
  {
    case DOT_RECORD:
    case DOT_DOT_RECORD:
      identifier_size = 1;
      break;
    case SUBDIRECTORY_RECORD:
      identifier_size = strlen(d->name);
      break;
    case FILE_RECORD:
      identifier_size = strlen(d->name) + 2;
      if (d->extension[0] != 0)
        identifier_size += 1 + strlen(d->extension);
      break;
  }
  unsigned record_size = 33 + identifier_size;
  if ((identifier_size & 1) == 0)
    record_size++;
  if (cd.offset + record_size > SECTOR_SIZE)
    fill_sector();
  write_byte(record_size);
  write_byte(0); // number of sectors in extended attribute record
  write_both_endian(d->sector);
  write_both_endian(d->size);
  write_byte(d->date_and_time.year - 1900);
  write_byte(d->date_and_time.month);
  write_byte(d->date_and_time.day);
  write_byte(d->date_and_time.hour);
  write_byte(d->date_and_time.minute);
  write_byte(d->date_and_time.second);
  write_byte(0);  // GMT offset
  write_byte(d->flags);
  write_byte(0);  // file unit size for an interleaved file
  write_byte(0);  // interleave gap size for an interleaved file
  write_both_endian((WORD) 1); // volume sequence number
  write_byte(identifier_size);
  switch (type)
  {
    case DOT_RECORD:
      write_byte(0);
      break;
    case DOT_DOT_RECORD:
      write_byte(1);
      break;
    case SUBDIRECTORY_RECORD:
      write_string(d->name);
      break;
    case FILE_RECORD:
      write_string(d->name);
      if (d->extension[0] != 0)
      {
        write_byte('.');
        write_string(d->extension);
      }
      write_string(";1");
      break;
  }
  if ((identifier_size & 1) == 0)
    write_byte(0);
}

/*-----------------------------------------------------------------------------
This function converts the date and time words from an ffblk structure and
puts them into a date_and_time structure.
-----------------------------------------------------------------------------*/

static void convert_date_and_time(date_and_time &dt, int date, int time)
{
  dt.second = 2 * (time & 0x1F);
  dt.minute = time >> 5 & 0x3F;
  dt.hour = time >> 11 & 0x1F;
  dt.day = date & 0x1F;
  dt.month = date >> 5 & 0xF;
  dt.year = (date >> 9 & 0x7F) + 1980;
}

/*-----------------------------------------------------------------------------
This function checks the specified character, if necessary, and
generates an error if it is a punctuation mark other than an underscore.
It also converts small letters to capital letters and returns the
result.
-----------------------------------------------------------------------------*/

static int check_for_punctuation(int c, char *name)
{
  c = toupper(c & 0xFF);
  if (!accept_punctuation_marks && !isalnum(c) && c != '_')
    error_exit("Punctuation mark in %s", name);
  return c;
}

/*-----------------------------------------------------------------------------
This function creates a new directory record with the information from the
specified ffblk. It links it into the beginning of the directory list
for the specified parent and returns a pointer to the new record.
-----------------------------------------------------------------------------*/

directory_record *new_directory_record(struct ffblk &f,
  directory_record *parent)
{
  directory_record *d = new directory_record;
  if (d == NULL)
    error_exit("Insufficient memory");
  d->next_in_memory = root.next_in_memory;
  root.next_in_memory = d;
  {
    char *s = f.ff_name;
    char *t = d->name;
    while (*s != 0)
    {
      if (*s == '.')
      {
        s++;
        break;
      }
      *t++ = check_for_punctuation(*s++, f.ff_name);
    }
    *t = 0;
    t = d->extension;
    while (*s != 0)
      *t++ = check_for_punctuation(*s++, f.ff_name);
    *t = 0;
  }
  convert_date_and_time(d->date_and_time, f.ff_fdate, f.ff_ftime);
  if (f.ff_attrib & FA_DIREC)
  {
    if (d->extension[0] != 0)
      error_exit("Directory with extension %s", f.ff_name);
    d->flags = DIRECTORY_FLAG;
  }
  else
    d->flags = f.ff_attrib & FA_HIDDEN ? HIDDEN_FLAG : 0;
  d->size = f.ff_fsize;
  d->next_in_directory = parent->first_record;
  parent->first_record = d;
  d->parent = parent;
  return d;
}

/*-----------------------------------------------------------------------------
This function compares two directory records according to the ISO9660 rules
for directory sorting and returns a negative value if p is before q, or a
positive value if p is after q.
-----------------------------------------------------------------------------*/

static int compare_directory_order(directory_record *p, directory_record *q)
{
  int n = strcmp(p->name, q->name);
  if (n == 0)
    n = strcmp(p->extension, q->extension);
  return n;
}

/*-----------------------------------------------------------------------------
This function compares two directory records (which must represent
directories) according to the ISO9660 rules for path table sorting and returns
a negative value if p is before q, or a positive vlaue if p is after q.
-----------------------------------------------------------------------------*/

static int compare_path_table_order(directory_record *p, directory_record *q)
{
  if (p == q)
    return 0;
  int n = p->level - q->level;
  if (n == 0)
  {
    n = compare_path_table_order(p->parent, q->parent);
    if (n == 0)
      n = compare_directory_order(p, q);
  }
  return n;
}

/*-----------------------------------------------------------------------------
This function appends the specified string to the buffer source[].
-----------------------------------------------------------------------------*/

static void append_string_to_source(char *s)
{
  while (*s != 0)
    *end_source++ = *s++;
}

/*-----------------------------------------------------------------------------
This function scans all files from the current source[] (which must end in \,
and represents a directory already in the database as d),
and puts the appropriate directory records into the database in memory, with
the specified root. It calls itself recursively to scan all subdirectories.
-----------------------------------------------------------------------------*/

static void make_directory_records(directory_record *d)
{
  struct ffblk f;
  d->first_record = NULL;
  strcpy(end_source, "*.*");
  if (findfirst(source, &f, FA_HIDDEN) == 0)
  {
    do
    {
      if (strcmp(f.ff_name, DIRECTORY_TIMESTAMP) == 0)
        convert_date_and_time(d->date_and_time, f.ff_fdate, f.ff_ftime);
      else
      {
        if (verbosity == VERBOSE)
        {
          char *old_end_source = end_source;
          strcpy(end_source, f.ff_name);
          printf("%d: file %s\n", d->level, source);
          end_source = old_end_source;
        }
        (void) new_directory_record(f, d);
      }
    } while (findnext(&f) == 0);
  }
  strcpy(end_source, "*.*");
  if (findfirst(source, &f, FA_DIREC) == 0)
  {
    do
    {
      if (f.ff_attrib & FA_DIREC && f.ff_name[0] != '.')
      {
        char *old_end_source = end_source;
        append_string_to_source(f.ff_name);
        *end_source++ = '\\';
        if (verbosity == VERBOSE)
        {
          *end_source = 0;
          printf("%d: directory %s\n", d->level + 1, source);
        }
        if (d->level < MAX_LEVEL)
        {
          directory_record *new_d = new_directory_record(f, d);
          new_d->next_in_path_table = root.next_in_path_table;
          root.next_in_path_table = new_d;
          new_d->level = d->level + 1;
          make_directory_records(new_d);
        }
        else
          error_exit("Directory is nested too deep");
        end_source = old_end_source;
      }
    } while (findnext(&f) == 0);
  }
  // sort directory
  d->first_record =
    sort_linked_list(d->first_record, 0, compare_directory_order);
}

/*-----------------------------------------------------------------------------
This function loads the file specifications for the file or directory
corresponding to the specified directory record into the source[] buffer. It
is recursive.
-----------------------------------------------------------------------------*/

static void get_file_specifications(directory_record *d)
{
  if (d != &root)
  {
    get_file_specifications(d->parent);
    append_string_to_source(d->name);
    if ((d->flags & DIRECTORY_FLAG) == 0 && d->extension[0] != 0)
    {
      *end_source++ = '.';
      append_string_to_source(d->extension);
    }
    if (d->flags & DIRECTORY_FLAG)
      *end_source++ = '\\';
  }
}

static void pass(void)
{
  // first 16 sectors are zeros

  write_block(16 * SECTOR_SIZE, 0);

  // Primary Volume Descriptor

  write_string("\1CD001\1");
  write_byte(0);
  write_block(32, ' ');  // system identifier
  {
    char *t = volume_label;
    for (unsigned i = 0; i < 32; i++)
      write_byte(*t != 0 ? toupper(*t++) : ' ');
  }
  write_block(8, 0);
  write_both_endian(total_sectors);
  write_block(32, 0);
  write_both_endian((WORD) 1); // volume set size
  write_both_endian((WORD) 1); // volume sequence number
  write_both_endian((WORD) 2048); // sector size
  write_both_endian(path_table_size);
  write_little_endian(little_endian_path_table_sector);
  write_little_endian((DWORD) 0);  // second little endian path table
  write_big_endian(big_endian_path_table_sector);
  write_big_endian((DWORD) 0);  // second big endian path table
  write_directory_record(&root, DOT_RECORD);
  write_block(128, ' ');      // volume set identifier
  write_block(128, ' ');      // publisher identifier
  write_block(128, ' ');      // data preparer identifier
  write_block(128, ' ');      // application identifier
  write_block(37, ' ');       // copyright file identifier
  write_block(37, ' ');       // abstract file identifier
  write_block(37, ' ');       // bibliographic file identifier
  write_string("0000000000000000");  // volume creation
  write_byte(0);
  write_string("0000000000000000");  // most recent modification
  write_byte(0);
  write_string("0000000000000000");  // volume expires
  write_byte(0);
  write_string("0000000000000000");  // volume is effective
  write_byte(0);
  write_byte(1);
  write_byte(0);
  fill_sector();

  // Volume Descriptor Set Terminator

  write_string("\377CD001\1");
  fill_sector();

  // Little Endian Path Table

  little_endian_path_table_sector = cd.sector;
  write_byte(1);
  write_byte(0);  // number of sectors in extended attribute record
  write_little_endian(root.sector);
  write_little_endian((WORD) 1);
  write_byte(0);
  write_byte(0);
  {
    directory_record *d;
    unsigned index = 1;
    root.path_table_index = 1;
    for (d = root.next_in_path_table; d != NULL;
      d = d->next_in_path_table)
    {
      unsigned name_length = strlen(d->name);
      write_byte(name_length);
      write_byte(0);  // number of sectors in extended attribute record
      write_little_endian(d->sector);
      write_little_endian(d->parent->path_table_index);
      write_string(d->name);
      if (name_length & 1)
        write_byte(0);
      d->path_table_index = ++index;
    }
    path_table_size = (cd.sector - little_endian_path_table_sector) *
      SECTOR_SIZE + cd.offset;
    fill_sector();
  }

  // Big Endian Path Table

  big_endian_path_table_sector = cd.sector;
  write_byte(1);
  write_byte(0);  // number of sectors in extended attribute record
  write_big_endian(root.sector);
  write_big_endian((WORD) 1);
  write_byte(0);
  write_byte(0);
  {
    directory_record *d;
    for (d = root.next_in_path_table; d != NULL;
      d = d->next_in_path_table)
    {
      unsigned name_length = strlen(d->name);
      write_byte(name_length);
      write_byte(0);  // number of sectors in extended attribute record
      write_big_endian(d->sector);
      write_big_endian(d->parent->path_table_index);
      write_string(d->name);
      if (name_length & 1)
        write_byte(0);
    }
    fill_sector();
  }

  // directories and files

  {
    directory_record *d;
    for (d = &root; d != NULL; d = d->next_in_path_table)
    {
      directory_record *q;

      // write directory

      d->sector = cd.sector;
      write_directory_record(d, DOT_RECORD);
      write_directory_record(d == &root ? d : d->parent, DOT_DOT_RECORD);
      for (q = d->first_record; q != NULL; q = q->next_in_directory)
      {
        write_directory_record(q,
          q->flags & DIRECTORY_FLAG ? SUBDIRECTORY_RECORD : FILE_RECORD);
      }
      fill_sector();
      d->size = (cd.sector - d->sector) * SECTOR_SIZE;
      number_of_directories++;
      bytes_in_directories += d->size;

      // write file data

      for (q = d->first_record; q != NULL; q = q->next_in_directory)
      {
        if ((q->flags & DIRECTORY_FLAG) == 0)
        {
          q->sector = cd.sector;
          DWORD size = q->size;
          if (cd.handle < 0)
          {
            DWORD number_of_sectors = (size + SECTOR_SIZE - 1) / SECTOR_SIZE;
            cd.sector += number_of_sectors;
            number_of_files++;
            bytes_in_files += size;
            unused_bytes_at_ends_of_files +=
              number_of_sectors * SECTOR_SIZE - size;
          }
          else
          {
            char *old_end_source = end_source;
            get_file_specifications(q);
            *end_source = 0;
            if (verbosity == VERBOSE)
              printf("Writing %s\n", source);
            int h = _open(source, O_RDONLY);
            if (h < 0)
              error_exit("Can't open %s\n", source);
            while (size > 0)
            {
              int n = BUFFER_SIZE - cd.count;
              if ((DWORD) n > size)
                n = size;
              if (_read(h, cd.buffer + cd.count, n) < n)
              {
                close(h);
                error_exit("Read error in file %s\n", source);
              }
              cd.count += n;
              if (cd.count == BUFFER_SIZE)
                flush_buffer();
              cd.sector += n / SECTOR_SIZE;
              cd.offset += n % SECTOR_SIZE;
              size -= n;
            }
            close(h);
            end_source = old_end_source;
            fill_sector();
          }
        }
      }
    }
  }

  total_sectors = cd.sector;
}

static char HELP[] =
  "CDMAKE  [-q] [-v] [-p] [-s N]  source  volume  image\n"
  "\n"
  "  source      specifications of base directory containing all files to\n"
  "              be written to CD-ROM image\n"
  "\n"
  "  volume      volume label\n"
  "\n"
  "  image       image file or device\n"
  "\n"
  "  -q          quiet mode - display nothing but error messages\n"
  "\n"
  "  -v          verbose mode - display file information as files are\n"
  "              scanned and written - overrides -p option\n"
  "\n"
  "  -p          show progress while writing\n"
  "\n"
  "  -s N        abort operation before beginning write if image will be\n"
  "              larger than N megabytes (i.e. 1024*1024*N bytes)\n"
  "\n"
  "  -m          accept punctuation marks other than underscores in\n"
  "              names and extensions\n";

/*-----------------------------------------------------------------------------
Program execution starts here.
-----------------------------------------------------------------------------*/

int main(int argc, char **argv)
{

  if (argc < 2)
  {
    puts(HELP);
    return 1;
  }

  if (setjmp(error))
    return 1;

  // initialize root directory

  cd.buffer = new BYTE[BUFFER_SIZE];
  if (cd.buffer == NULL)
    error_exit("Insufficient memory");

  memset(&root, 0, sizeof(root));
  root.level = 1;
  root.flags = DIRECTORY_FLAG;

  // initialize CD-ROM write buffer

  cd.handle = -1;
  cd.filespecs[0] = 0;

  // initialize parameters

  verbosity = NORMAL;
  size_limit = 0;
  show_progress = FALSE;
  accept_punctuation_marks = FALSE;
  source[0] = 0;
  volume_label[0] = 0;

  // scan command line arguments

  {
    BOOL q_option = FALSE;
    BOOL v_option = FALSE;
    for (int i = 1; i < argc; i++)
    {
      if (memcmp(argv[i], "-s", 2) == 0)
      {
        char *t = argv[i] + 2;
        if (*t == 0)
        {
          if (++i < argc)
            t = argv[i];
          else
            error_exit("Missing size limit parameter");
        }
        while (isdigit(*t))
          size_limit = size_limit * 10 + *t++ - '0';
        if (size_limit < 1 || size_limit > 800)
          error_exit("Invalid size limit");
        size_limit <<= 9;  // convert megabyte to sector count
      }
      else if (strcmp(argv[i], "-q") == 0)
        q_option = TRUE;
      else if (strcmp(argv[i], "-v") == 0)
        v_option = TRUE;
      else if (strcmp(argv[i], "-p") == 0)
        show_progress = TRUE;
      else if (strcmp(argv[i], "-m") == 0)
        accept_punctuation_marks = TRUE;
      else if (i + 2 < argc)
      {
        strcpy(source, argv[i++]);
        strncpy(volume_label, argv[i++], sizeof(volume_label) - 1);
        strcpy(cd.filespecs, argv[i]);
      }
      else
        error_exit("Missing command line argument");
    }
    if (v_option)
    {
      show_progress = FALSE;
      verbosity = VERBOSE;
    }
    else if (q_option)
    {
      verbosity = QUIET;
      show_progress = FALSE;
    }
    if (source[0] == 0)
      error_exit("Missing source directory");
    if (volume_label[0] == 0)
      error_exit("Missing volume label");
    if (cd.filespecs[0] == 0)
      error_exit("Missing image file specifications");
  }

  // set source[] and end_source to source directory, with a terminating \

  end_source = source + strlen(source);
  if (end_source[-1] == ':')
    *end_source++ = '.';
  if (end_source[-1] != '\\')
    *end_source++ = '\\';

  // scan all files and create directory structure in memory

  make_directory_records(&root);

  // sort path table entries

  root.next_in_path_table = sort_linked_list(root.next_in_path_table, 1,
    compare_path_table_order);

  // initialize CD-ROM write buffer

  cd.handle = -1;
  cd.sector = 0;
  cd.offset = 0;
  cd.count = 0;

  // make non-writing pass over directory structure to obtain the proper
  // sector numbers and offsets and to determine the size of the image

  number_of_files = bytes_in_files = number_of_directories =
    bytes_in_directories = unused_bytes_at_ends_of_files =0;
  pass();

  if (verbosity >= NORMAL)
  {
    printf("%s bytes ", edit_with_commas(bytes_in_files));
    printf("in %s files\n", edit_with_commas(number_of_files, FALSE));
    printf("%s unused bytes at ends of files\n",
      edit_with_commas(unused_bytes_at_ends_of_files));
    printf("%s bytes ", edit_with_commas(bytes_in_directories));
    printf("in %s directories\n",
      edit_with_commas(number_of_directories, FALSE));
    printf("%s other bytes\n", edit_with_commas(root.sector * SECTOR_SIZE));
    puts("-------------");
    printf("%s total bytes\n",
      edit_with_commas(total_sectors * SECTOR_SIZE));
    puts("=============");
  }

  if (size_limit != 0 && total_sectors > size_limit)
    error_exit("Size limit exceeded");

  // re-initialize CD-ROM write buffer

  cd.handle = _creat(cd.filespecs, 0);
  if (cd.handle < 0)
    error_exit("Can't open image file %s", cd.filespecs);
  cd.sector = 0;
  cd.offset = 0;
  cd.count = 0;

  // make writing pass over directory structure

  pass();

  if (cd.count > 0)
    flush_buffer();
  if (show_progress)
    printf("\r             \n");
  if (close(cd.handle) != 0)
  {
    cd.handle = -1;
    error_exit("File write error in image file %s", cd.filespecs);
  }

  if (verbosity >= NORMAL)
    puts("CD-ROM image made successfully");

  release_memory();
  return 0;
}

LLMOSRT.C:

/* A Linked-List Memory Sort
   by Philip J. Erdelsky
   pje@acm.org
   http://www.alumni.caltech.edu/~pje/
*/

#include <stdio.h>

void *sort_linked_list(void *p, unsigned index, int (*compare)(void *, void *))
{
  unsigned base;
  unsigned long block_size;

  struct record
  {
    struct record *next[1];
    /* other members not directly accessed by this function */
  };

  struct tape
  {
    struct record *first, *last;
    unsigned long count;
  } tape[4];

  /* Distribute the records alternately to tape[0] and tape[1]. */

  tape[0].count = tape[1].count = 0L;
  tape[0].first = NULL;
  base = 0;
  while (p != NULL)
  {
    struct record *next = ((struct record *)p)->next[index];
    ((struct record *)p)->next[index] = tape[base].first;
    tape[base].first = ((struct record *)p);
    tape[base].count++;
    p = next;
    base ^= 1;
  }

  /* If the list is empty or contains only a single record, then */
  /* tape[1].count == 0L and this part is vacuous.               */

  for (base = 0, block_size = 1L; tape[base+1].count != 0L;
    base ^= 2, block_size <<= 1)
  {
    int dest;
    struct tape *tape0, *tape1;
    tape0 = tape + base;
    tape1 = tape + base + 1;
    dest = base ^ 2;
    tape[dest].count = tape[dest+1].count = 0;
    for (; tape0->count != 0; dest ^= 1)
    {
      unsigned long n0, n1;
      struct tape *output_tape = tape + dest;
      n0 = n1 = block_size;
      while (1)
      {
        struct record *chosen_record;
        struct tape *chosen_tape;
        if (n0 == 0 || tape0->count == 0)
        {
          if (n1 == 0 || tape1->count == 0)
            break;
          chosen_tape = tape1;
          n1--;
        }
        else if (n1 == 0 || tape1->count == 0)
        {
          chosen_tape = tape0;
          n0--;
        }
        else if ((*compare)(tape0->first, tape1->first) > 0)
        {
          chosen_tape = tape1;
          n1--;
        }
        else
        {
          chosen_tape = tape0;
          n0--;
        }
        chosen_tape->count--;
        chosen_record = chosen_tape->first;
        chosen_tape->first = chosen_record->next[index];
        if (output_tape->count == 0)
          output_tape->first = chosen_record;
        else
          output_tape->last->next[index] = chosen_record;
        output_tape->last = chosen_record;
        output_tape->count++;
      }
    }
  }

  if (tape[base].count > 1L)
    tape[base].last->next[index] = NULL;
  return tape[base].first;
}

