/*****************************************************************************/
/*
                                pcache.c

This program can act as a command-line utility or CGI(plus)-compliant script.
It is used to access the information about or contents of a WASD proxy cache
file.

With both CLI utility and CGI script the items reported on may be filtered
according to the contents of the URL and/or the response header.  The filter
strings may contain the wildcard characters '*' (matching zero or more
characters) and '%' (matching any single character).  Any filter strings must
be enclosed by wildcards unless the specified character lies on either the
upper of lower boundary of the string (i.e. "*netscape*" matches the string
"netscape" anywhere in the string, but "netscape*" will only match it at the
start of the string).  Filtering is also available based on maximum or minimum
hits on the file/URL.

The default behaviour is to generate a form for requesting other functionality.
One major use is the profiling of the cache files listing the four main
characteristics; age ("Last-Modified:"), when loaded, when last accessed, and
by the number of hits, by hours and days, with the corresponding file count and
blocks used and allocated.  The next is to list cache file entries and then be
able to select individual files for closer analysis.

Two cache directory organizations are currently available.  The first provides
for a single level structure with a possible 256 directories at the top level
and files organized immediate below these.  This is the original structure and
works well where files do not exceed 256 per directory (assuming a relatively
uniform distribution of file names by the MD5 algorithm) for a total of
approximately 65,000 files.  For versions of VMS prior to V7.2 exceeding 256
files per directory incurs a significant performance penalty for some directory
operations.  A file in this structure cache might look like:

  WASD_CACHE_ROOT:[03]4A628C52356BCA71B507AA8E47ADF1.HTC

The second organization involves two levels of directory, each with a maximum
of 64 directories.  Again assuming a relatively uniform distribution of file
names by the MD5 algorithm, this allows for approximately 1,000,000 files
before exceeding the 256 file per directory point.  A file in this structure
cache might look like:

  WASD_CACHE_ROOT:[00.34]A628C52356BCA71B507AA8E47ADF1.HTC


FILE HEADER DATE/TIME FIELD USAGE
---------------------------------
(for v9.0.0ff cache files, different to the original version - v6.0.0ff)

BDT - backup contains the loaded date/time
CDT - creation is used to hold a responses last-modified date/time
RDT - revision contains the date/time the file was last accessed
EDT - for cached responses, set to any supplied "Expires:" date/time
      for cached "negative" responses, the load time plus "negative" period


CLI UTILITY
-----------
Needs a privileged account (SYSPRV or SETPRV).
Assign the foreign verb using:

  $ PCACHE == "$HT_EXE:PCACHE"

Output may be redirected to any file using the /OUTPUT= qualifier.

Just specifying

  $ PCACHE

generates a profile of the cache files.

As a CLI utility PCACHE may be used to list the contents of the entire cache
dirctory tree.  As this contains more than 80 columns it would need to be
printed in portrait format.  Specifying

  $ PCACHE /INDEX

will produce such a listing of the complete tree.  To list just a portion of
the tree provide a file specification containing suitable wildcards as
a parameter.  For example:

  $ PCACHE WASD_CACHE_ROOT:[00]*.HTC
or
  $ PCACHE WASD_CACHE_ROOT:[00.00]*.HTC

When a single, fully specified file is provided as a parameter similar
information is presented in a more readable format, as well as the response 
header stored in the cache file.  For example:

  $ PCACHE WASD_CACHE_ROOT:[03]4A628C52356BCA71B507AA8E47ADF1.HTC
or
  $ PCACHE WASD_CACHE_ROOT:[00.34]A628C52356BCA71B507AA8E47ADF1.HTC

To provide this format report for all (or a portion) of the tree add the /FULL
qualifier to a command line such as:

  $ PCACHE /INDEX /FULL

To obtain the response header use:

  $ PCACHE WASD_CACHE_ROOT:[03]4A628C52356BCA71B507AA8E47ADF1.HTC /HEADER
or
  $ PCACHE WASD_CACHE_ROOT:[00.34]A628C52356BCA71B507AA8E47ADF1.HTC /HEADER

The /HEADER qualifier also takes an optional redirect file specification
(although this could also be specified using /OUTPUT):

  $ PCACHE WASD_CACHE_ROOT:[03]4A~~~F1.HTC /HEADER=EXAMPLE.TXT
or
  $ PCACHE WASD_CACHE_ROOT:[00.34]A~~~F1.HTC /HEADER=EXAMPLE.TXT

This can also be used with a listing of all (or a portion) of the cache tree:

  $ PCACHE /INDEX /HEADER

To extract the content (i.e. the response body) of a cache file into a
standalone file use:

  $ PCACHE WASD_CACHE_ROOT:[03]4A628C52356BCA71B507AA8E47ADF1.HTC /BODY
or
  $ PCACHE WASD_CACHE_ROOT:[00.34]A628C52356BCA71B507AA8E47ADF1.HTC /BODY

For all but TEXT responses an output file would probably need to be specified
using the /OUTPUT qualifier, or an file specification optional on the /BODY
qualifier, as in the following example:

  $ PCACHE WASD_CACHE_ROOT:[03]4A~~~F1.HTC /BODY=EXAMPLE.GIF
or
  $ PCACHE WASD_CACHE_ROOT:[00.34]A~~~F1.HTC /BODY=EXAMPLE.GIF

To filter use the /FILTER= qualifier.  This takes at most one of URL: or
HEADER: (or U:, H:) specifying a wildcard string for either the URL or response
header.  To filter on both, two /FILTERs must be used.  These examples show all
three possibilities.

  $ PCACHE /INDEX FILTER=URL:*.html
  $ PCACHE /INDEX /FILTER=HEADER:*netscape*
  $ PCACHE /INDEX /FILTER=U:*.html /FILTER=H:*netscape*

Other filters are available, based on minimum and/or maximum hits (accesses to
the particular file/URL), hours since last accessed, hours since last loaded,
adn finally hours since last-modified (age of document).

  $ PCACHE /INDEX /MINHITS=100
  $ PCACHE /INDEX /MAXHITS=10
  $ PCACHE /INDEX /MINHITS=10 /MAXHITS=100
  $ PCACHE /INDEX /MINACCESS=24 /MAXACCESS=168
  $ PCACHE /INDEX /MAXLOAD=48
  $ PCACHE /INDEX /MINLASTMOD=672


CGI(PLUS) SCRIPT
----------------
To make this program available as a CGI script the following would need to be
added to the HTTPD$CONFIG file, in the [AddType] grouping:

  .HTC  application/x-script  /cgi-bin/pcache  WASD proxy cache file

To use it as a CGUplus script (RECOMMENDED):

  .HTC  application/x-script  /cgiplus-bin/pcache  WASD proxy cache file

With this configured aaccessing a .HTC file from a directory listing of the
cache directory tree returns the essential information from that file (dates,
times, URL, response header) plus a number of buttons allowing the content
(text, image, etc.) to be viewed, the file to be VMS DUMPed, ANA/RMSed or
DELETEd.

The cache directory will also need to be mapped in HTTPD$MAP:

  pass /wasd_cache_root/*

The script could be mapped for abbreviated access:

  script+ /pcache* /cgi-bin/pcache*

It is recommended to place the cache directory under some authorization control
to prevent casual browsing and access of the contents.  Something local, along
the lines of:

  [VMS]
  /wasd_cache_root/* ~webadmin,131.185.250.*,r+w ;

By default a profile of the cache is generated.

  http://host.name.domain/pcache

To generate a listing of the cache contents use:

  http://host.name.domain/pcache?do=index

Filtering on URL or response header may be accomplished using either or both
query form field strings, as in the following examples.

  http://host.name.domain/pcache?url=*.gif
  http://host.name.domain/pcache?header=*netscape*
  http://host.name.domain/pcache?url=*.gif&header=*netscape*

Filtering on the number of hits, last accessed, last loaded and last-modified
are made in much the same way as for the CLI.

  http://host.name.domain/pcache?do=index&minhits=100
  http://host.name.domain/pcache?do=index&maxhits=10
  http://host.name.domain/pcache?do=index&minhits=10&maxhits=50
  http://host.name.domain/pcache?do=index&minacc=24&maxacc=168
  http://host.name.domain/pcache?do=index&maxload=48
  http://host.name.domain/pcache?do=index&minage=672

Needs to be installed with SYSPRV privilege.

  $ INSTALL REPLACE CGI-BIN:[000000]PCACHE /PRIVILEGE=(SYSPRV)


CGI VARIABLES
-------------
WWW_FORM_DO       "anarms", "delete", "dump", "index", "form",
                  "response", "profile"
WWW_FORM_HEADER   wildcardable string allowing filtering on header contents
WWW_FORM_MAXACC   only files last accessed within the specified hours
WWW_FORM_MAXHITS  only files with no more than this many hits
WWW_FORM_MAXLOAD  only files last loaded within the specified hours
WWW_FORM_MAXLAST  only files last-modified within the specified hours
WWW_FORM_MINACC   only files last accessed more than the specified hours
WWW_FORM_MINHITS  only files with at least this many hits
WWW_FORM_MINLAST  only files last-modified more than the specified hours
WWW_FORM_MINLOAD  only files last loaded more than the specified hours
WWW_FORM_URL      wildcardable string allowing filtering on URL contents


QUALIFIERS
----------
/AUTHORIZED      requires all script access to be authorized
/BODY[=file]     (CLI) output body of the response (optional output redirect)
/CHARSET=        (CGI) "Content-Type: text/html; charset=..."
/DBUG            turns on all "if (Debug)" statements
/DELETE          (CLI) deletes matched files (CAUTION! no confirmation)
/FULL            (CLI) directory listing, with same information as single file
/FILTER[=string] (CLI) filter on the supplied wildcardable string
                       e.g. /FILTER=URL:*.HTML e.g. /FILTER=HEADER:*WASD*
/HEADER[=file]   (CLI) output header of the response (optional output redirect)
/INDEX           (CLI) generate a listing of all files in the cache
/MAXACCESS=      (CLI) only files last accessed within the specified hours
/MAXLASTMOD=     (CLI) only files last-modified within the specific hours
/MAXAGE=         (CLI) synonym for /maxlastmod=
/MAXEXPIRES=     (CLI) only files expiring within the specific hours
/MAXHITS=        (CLI) only files with no more than this many hits
/MAXLOAD=        (CLI) only files loaded within the specified hours
/MINACCESS=      (CLI) only files last accessed more than the specified hours
/MINLASTMOD=     (CLI) only files last-modified more than the specified hours
/MAXAGE=         (CLI) synonym for /minlastmod=
/MINEXPIRES=     (CLI) only files expiring after more than the specified hours
/MINHITS=        (CLI) only files with at least this many hits
/MINLOAD=        (CLI) only files last loaded more than the specified hours
/OUTPUT=file     (CLI) redirect output to specified file
/[NO]PROFILE     (CLI) include a profile of cache space usage (default)


ENVIRONMENT VARIABLES (logicals or symbols)
---------------------
PCACHE$DBUG         turns on all "if (Debug)" statements
PCACHE$PARAM        equivalent to (overrides) the command line
                    parameters/qualifiers (define as a system-wide logical)
PCACHE$AUTHORIZED   requires *all* script access to be authorized


BUILD DETAILS
-------------
See BUILD_PCACHE.COM procedure.


COPYRIGHT
---------
Copyright (C) 1999-2020 Mark G.Daniel
This program, comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it under the
conditions of the GNU GENERAL PUBLIC LICENSE, version 3, or later version.
http://www.gnu.org/licenses/gpl.txt


VERSION HISTORY (update SOFTWAREVN as well!)
---------------
14-AUG-2009  MGD  v1.6.2, WASD v10 moves from HT_CACHE_ROOT to WASD_CACHE_ROOT
12-MAY-2005  MGD  v1.6.1, WASD v9.1 uses bit 1 of the RAT field to indicate
                          the file contains GZIP compressed content
24-SEP-2004  MGD  v1.6.0, changes in line with WASD v9.0 use of date/times,
                          requires linking with CGILIB v1.7 or later
23-DEC-2003  MGD  v1.5.1, minor conditional mods to support IA64
05-SEP-2003  MGD  v1.5.0, enable SYSPRV to access cache files,
                          check for account SYSPRV before CLI activating
03-APR-2002  MGD  v1.4.1, bugfix; FaoPrint() use fwrite()
28-OCT-2000  MGD  v1.4.0, use CGILIB object module,
                          modifications for RELAXED_ANSI compilation
28-AUG-2000  MGD  v1.3.0, flat256 and 64x64 cache directory organizations
                          (this actually involved no code changes in PCACHE.C) 
18-AUG-2000  MGD  v1.2.0, add /AUTHORIZED and PCACHE$AUTHORIZED,
                          bugfix; authorization check against "delete"
12-APR-2000  MGD  v1.1.1, minor changes for CGILIB 1.4
29-JAN-2000  MGD  no changes required for ODS-5 compliance ... it's not
                  (see note in PROXYCACHE.C)
07-AUG-1999  MGD  v1.1.0, use more of the CGILIB functionality
10-JAN-1999  MGD  v1.0.0, initial development
*/
/*****************************************************************************/

#define SOFTWAREVN "1.6.2"
#define SOFTWARENM "PCACHE"
#ifdef __ALPHA
#  define SOFTWAREID SOFTWARENM " AXP-" SOFTWAREVN
#endif
#ifdef __ia64
#  define SOFTWAREID SOFTWARENM " IA64-" SOFTWAREVN
#endif
#ifdef __VAX
#  define SOFTWAREID SOFTWARENM " VAX-" SOFTWAREVN
#endif
#ifdef __x86_64
#  define SOFTWAREID SOFTWARENM " X86-" SOFTWAREVN
#endif

/* standard C header files */
#include <ctype.h>
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unixio.h>

/* VMS-related header files */
#include <atrdef.h>
#include <descrip.h>
#include <fibdef.h>
#include <jpidef.h>
#include <iodef.h>
#include <libdef.h>
#include <libdtdef.h>
#include <lnmdef.h>
#include <prvdef.h>
#include <rms.h>
#include <ssdef.h>
#include <stsdef.h>

/* application header file */
#include <cgilib.h>

#define boolean int
#define true 1
#define false 0

#ifndef __VAX
#   pragma nomember_alignment
#endif

#define VMSok(x) ((x) & STS$M_SUCCESS)
#define VMSnok(x) (!((x) & STS$M_SUCCESS))
 
#define FI_LI __FILE__, __LINE__

#define PROXY_CACHE_FILE_VERSION_9 0x090000
#define PROXY_CACHE_FILE_VERSION_6 0x060000

/* value returned when a binary time is zero */
#define ZERO_TIME 999999999
#define TIME_HOURS(hrs) hrs == ZERO_TIME ? 0 : hrs
#define ZERO_TIME_DAYS 999999999/24
#define TIME_DAYS(hrs) hrs == ZERO_TIME ? 0 : hrs/24

#define RAT_GZIP 0x01   /* (FAT$M_FORTRANCC, FAB$M_FTN) */

/* these structures are from PROXYSTRUCT.H (needless-to-say) */

struct AnIOsb
{
   unsigned short  Status;
   unsigned short  Count;
   unsigned long  Unused;
};

struct ProxyCacheDescrStruct
{
   int  CacheVersion,
        UrlLength,
        HeaderLength;
};

struct ProxyCacheRecordAttributes
{
   unsigned char  RecordType;
   unsigned char  RecordAttrib;
   unsigned short  RecordSize;
   unsigned short  AllocatedVbnHi;
   unsigned short  AllocatedVbnLo;
   unsigned short  EndOfFileVbnHi;
   unsigned short  EndOfFileVbnLo;
   unsigned short  FirstFreeByte;
   unsigned char   OfNoInterest1 [18];
};

struct ProxyCacheAscDates
{
   unsigned short  RevisionCount;
   unsigned char  OfNoInterest [33];
};

struct ProxyCacheFileAcpStruct
{
   unsigned short  Channel,
                   AcpFileNameLength;
   unsigned long  BdtBinTime [2],
                  CdtBinTime [2],
                  EdtBinTime [2],
                  RdtBinTime [2];
   char AcpFileName [128];
   struct ProxyCacheAscDates  AscDates;
   struct ProxyCacheRecordAttributes  RecAttr;
   struct fibdef  FileFib;
   struct atrdef  FileAtr [7];
   struct dsc$descriptor  FileFibAcpDsc,
                          FileNameAcpDsc;
   struct AnIOsb  AcpIOsb;
};

char  Utility [] = "PCACHE";

boolean  CgiDoAnaRms,
         CgiDoDelete,
         CgiDoDump,
         CgiDoResponse,
         CliDoBody,
         CliDoDelete,
         CliDoFull,
         CliDoHeader,
         CliMustBeAuthorized,
         Debug,
         DoFile,
         DoIndex,
         DoNoProfile,
         DoProfile,
         IsCgiPlus,
         IsCgiScript,
         IsCliUtility,
         OpenAndReadFile,
         ThereHasBeenOutput;

int  FileCount,
     MaxAccessHours,
     MaxExpiresHours,
     MaxLastModHours,
     MaxHits,
     MaxLoadHours,
     MinAccessHours,
     MinExpiresHours,
     MinLastModHours,
     MinHits,
     MinLoadHours,
     VersionSixCount;

unsigned long  TotalStatTimerContext;

char  *CgiFormDoPtr,
      *CgiPathInfoPtr,
      *CgiPathTranslatedPtr,
      *CgiPlusEofPtr,
      *CgiRemoteUserPtr,
      *CgiRequestMethodPtr,
      *CgiScriptNamePtr,
      *CgiServerSoftwarePtr,
      *CgiEnvironmentPtr,
      *CharsetPtr,
      *CliCharsetPtr,
      *CliFileSpecPtr,
      *CliOutputPtr,
      *HeaderFilterPtr,
      *UrlFilterPtr;
      
char  FilterComment [512],
      SoftwareID [48];

struct ProfileStruct
{
   int  BlocksAllocated,
        BlocksUsed,
        Count;
};

unsigned long  SysPrvMask [2] = { PRV$M_SYSPRV, 0 };

#define PROFILE_DAYS 28
#define PROFILE_HOURS_MAX (PROFILE_DAYS * 24)

char  *HitRange [] = {
"      0",
"      1", "      2", "      3", "      4", "      5",
"      6", "      7", "      8", "      9",
"  10-19", "  20-29", "  30-39", "  40-49", "  50-59",
"  60-69", "  70-79", "  80-89", "  90-99",
"100-199", "200-299", "300-399", "400-499", "500-599",
"600-699", "700-799", "800-899", "900-999",
"  >1000"
};

struct ProfileStruct  ProfileAccess [PROFILE_HOURS_MAX+2],
                      ProfileExpires [PROFILE_HOURS_MAX+2],
                      ProfileLoad [PROFILE_HOURS_MAX+2],
                      ProfileModified [PROFILE_HOURS_MAX+2],
                      ProfileRevisionCount [29];

/* required function prototypes */
void NeedsPrivilegedAccount ();
char* SearchTextString (char*, char*, boolean, int*);
char* SysGetMsg (int);
char* VmsToPath (char*, char*);
char* v10orPrev10 (char*, int);

/*****************************************************************************/
/*
*/

main ()

{
   /*********/
   /* begin */
   /*********/

   sprintf (SoftwareID, "%s (%s)", SOFTWAREID, CgiLibEnvironmentVersion());

   if (getenv ("PCACHE$DBUG"))
   {
      Debug = true;
      if (getenv ("HTTP$INPUT"))
         fprintf (stdout, "Content-Type: text/plain\r\n\r\n");
   }

   GetParameters ();

   if (getenv ("HTTP$INPUT"))
      CgiScript ();
   else
      CliUtility();

   exit (SS$_NORMAL);
}

/*****************************************************************************/
/*
Being used as a command-line utility.
*/

CliUtility ()

{
   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "CliUtility()\n");

   NeedsPrivilegedAccount ();

   IsCliUtility = true;

   if (CliOutputPtr)
   {
      if (Debug) fprintf (stdout, "CliOutputPtr |%s|\n", CliOutputPtr);
      if (!(stdout = freopen (CliOutputPtr, "w", stdout))) exit (vaxc$errno);
   }

   if (!CliFileSpecPtr) CliFileSpecPtr = "";

   sys$setprv (1, &SysPrvMask, 0, 0);
   SearchCache (CliFileSpecPtr);
   sys$setprv (0, &SysPrvMask, 0, 0);
}

/*****************************************************************************/
/*
Because it can be installed with privileges (for CLI usage) ...
*/ 

void NeedsPrivilegedAccount ()

{
   static unsigned long  PrivAcctMask [2] = { PRV$M_SETPRV | PRV$M_SYSPRV, 0 };

   static long  Pid = -1;
   static unsigned long  JpiAuthPriv [2];

   static struct
   {
      unsigned short  buf_len;
      unsigned short  item;
      void  *buf_addr;
      void  *ret_len;
   }
      JpiItems [] =
   {
      { sizeof(JpiAuthPriv), JPI$_AUTHPRIV, &JpiAuthPriv, 0 },
      {0,0,0,0}
   };

   int  status;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "NeedsPrivilegedAccount()\n");

   status = sys$getjpiw (0, &Pid, 0, &JpiItems, 0, 0, 0);
   if (VMSnok (status)) exit (status);

   if (!(JpiAuthPriv[0] & PrivAcctMask[0])) exit (SS$_NOSYSPRV);
}

/*****************************************************************************/
/*
Being used as a CGI(plus) script.
*/

CgiScript ()

{
   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "CgiScript()\n");

   IsCgiScript = true;
   CgiLibEnvironmentSetDebug (Debug);

   CgiLibEnvironmentInit (0, NULL, true);

   CgiLibResponseSetCharset (CliCharsetPtr);
   CgiLibResponseSetSoftwareID (SoftwareID);
   CgiLibResponseSetErrorMessage ("Reported by pCache");

   IsCgiPlus = CgiLibEnvironmentIsCgiPlus ();

   if (IsCgiPlus)
   {
      for (;;)
      {
         /* block waiting for the next request */
         CgiLibVar ("");
         ProcessRequest ();
         CgiLibCgiPlusEOF ();
      }
   }
   else
      ProcessRequest ();
}

/*****************************************************************************/
/*
Being used as a CGI(plus) script.  Get the required CGI variables and call
search cache.
*/ 
 
ProcessRequest ()

{
   char  *CgiPtr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ProcessRequest()\n");

   ThereHasBeenOutput = false;

   CgiRemoteUserPtr = CgiLibVar ("WWW_REMOTE_USER");
   if (!CgiRemoteUserPtr[0] &&
       (CliMustBeAuthorized || getenv("PCACHE$AUTHORIZED")))
   {
      CgiLibResponseError (FI_LI, 0, "Authorization mandatory!");
      return;
   }

   CgiLibResponseSetStreamMode (1);
   CgiEnvironmentPtr = CgiLibEnvironmentName ();

   CgiServerSoftwarePtr = CgiLibVar ("WWW_SERVER_SOFTWARE");
   CgiRequestMethodPtr = CgiLibVar ("WWW_REQUEST_METHOD");
   if (strcmp (CgiRequestMethodPtr, "GET"))
   {
      CgiLibResponseHeader (501, "text/html");
      fprintf (stdout, "Not implemented!\n");
      return;
   }

   CgiScriptNamePtr = CgiLibVar ("WWW_SCRIPT_NAME");
   CgiPathInfoPtr = CgiLibVar ("WWW_PATH_INFO");
   CgiPathTranslatedPtr = CgiLibVar ("WWW_PATH_TRANSLATED");
   CgiFormDoPtr =  CgiLibVar ("WWW_FORM_DO");

   HeaderFilterPtr =  CgiLibVar ("WWW_FORM_HEADER");
   if (!*HeaderFilterPtr) HeaderFilterPtr = NULL;
   UrlFilterPtr =  CgiLibVar ("WWW_FORM_URL");
   if (!*UrlFilterPtr) UrlFilterPtr = NULL;

   CgiPtr =  CgiLibVar ("WWW_FORM_MAXACC");
   MaxAccessHours = atoi(CgiPtr);
   CgiPtr =  CgiLibVar ("WWW_FORM_MAXEXPIRES");
   MaxExpiresHours = atoi(CgiPtr);
   CgiPtr =  CgiLibVar ("WWW_FORM_MAXHITS");
   MaxHits = atoi(CgiPtr);
   CgiPtr =  CgiLibVar ("WWW_FORM_MAXLAST");
   /* bit of backward compatibility */
   if (!CgiPtr[0]) CgiPtr = CgiLibVar ("WWW_FORM_MAXAGE");
   MaxLastModHours = atoi(CgiPtr);
   CgiPtr =  CgiLibVar ("WWW_FORM_MAXLOAD");
   MaxLoadHours = atoi(CgiPtr);
   CgiPtr =  CgiLibVar ("WWW_FORM_MINACC");
   MinAccessHours = atoi(CgiPtr);
   CgiPtr =  CgiLibVar ("WWW_FORM_MINEXPIRES");
   MinExpiresHours = atoi(CgiPtr);
   CgiPtr =  CgiLibVar ("WWW_FORM_MINHITS");
   MinHits = atoi(CgiPtr);
   CgiPtr =  CgiLibVar ("WWW_FORM_MINLAST");
   /* bit of backward compatibility */
   if (!CgiPtr[0]) CgiPtr = CgiLibVar ("WWW_FORM_MINAGE");
   MinLastModHours = atoi(CgiPtr);
   CgiPtr =  CgiLibVar ("WWW_FORM_MINLOAD");
   MinLoadHours = atoi(CgiPtr);

   if (!CgiPathInfoPtr[0] &&
       (!CgiFormDoPtr[0] || strsame(CgiFormDoPtr, "form", -1)))
      GenerateForm ();
   else
   {
      sys$setprv (1, &SysPrvMask, 0, 0);
      SearchCache (CgiPathTranslatedPtr);
      sys$setprv (1, &SysPrvMask, 0, 0);
   }
}

/*****************************************************************************/
/*
*/ 
 
GenerateForm (char *FileSpec)

{
   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GenerateForm()\n");

   CgiLibResponseHeader (200, "text/html");

   fprintf (stdout,
"<HTML>\n\
<HEAD>\n\
<TITLE>%s</TITLE>\n\
</HEAD>\n\
<BODY>\n\
<FONT SIZE=+1><B><U>%s</U></B></FONT><P>\n\
<FORM ACTION=\"%s\">\n\
<TABLE BORDER=0>\n\
\
<TR><TH ALIGN=right>Action:</TH><TD>\n\
<SELECT NAME=do>\n\
<OPTION VALUE=profile CHECKED>profile\n\
<OPTION VALUE=index>index (listing)\n\
<OPTION VALUE=delete>delete (CAUTION!)\n\
</SELECT>\n\
\
</TD></TR>\n\
<TR><TH ALIGN=right>URL filter:</TH>\n\
<TD><INPUT TYPE=text SIZE=30 NAME=url></TD></TR>\n\
<TR><TH ALIGN=right>Header filter:</TH>\n\
<TD><INPUT TYPE=text SIZE=30 NAME=header></TD></TR>\n\
\
<TR><TH ALIGN=right>Last Accessed:</TH><TD><NOBR>\n\
Min.<SELECT NAME=minacc>\n\
<OPTION VALUE=0 CHECKED>none\n\
<OPTION VALUE=1>1 hr\n\
<OPTION VALUE=2>2 hrs\n\
<OPTION VALUE=4>4 hrs\n\
<OPTION VALUE=8>8 hrs\n\
<OPTION VALUE=12>12 hrs\n\
<OPTION VALUE=18>18 hrs\n\
<OPTION VALUE=24>1 day\n\
<OPTION VALUE=48>2 days\n\
<OPTION VALUE=72>3 days\n\
<OPTION VALUE=96>4 days\n\
<OPTION VALUE=120>5 days\n\
<OPTION VALUE=144>6 days\n\
<OPTION VALUE=168>1 week\n\
<OPTION VALUE=336>2 weeks\n\
<OPTION VALUE=504>3 weeks\n\
<OPTION VALUE=672>4 weeks\n\
</SELECT>\n\
Max.<SELECT NAME=maxacc>\n\
<OPTION VALUE=0 CHECKED>none\n\
<OPTION VALUE=1>1 hr\n\
<OPTION VALUE=2>2 hrs\n\
<OPTION VALUE=4>4 hrs\n\
<OPTION VALUE=8>8 hrs\n\
<OPTION VALUE=12>12 hrs\n\
<OPTION VALUE=18>18 hrs\n\
<OPTION VALUE=24>1 day\n\
<OPTION VALUE=48>2 days\n\
<OPTION VALUE=72>3 days\n\
<OPTION VALUE=96>4 days\n\
<OPTION VALUE=120>5 days\n\
<OPTION VALUE=144>6 days\n\
<OPTION VALUE=168>1 week\n\
<OPTION VALUE=336>2 weeks\n\
<OPTION VALUE=504>3 weeks\n\
<OPTION VALUE=672>4 weeks\n\
</SELECT>\n\
</NOBR></TD></TR>\n\
\
<TR><TH ALIGN=right>Last Loaded:</TH><TD><NOBR>\n\
Min.<SELECT NAME=minload>\n\
<OPTION VALUE=0 CHECKED>none\n\
<OPTION VALUE=1>1 hr\n\
<OPTION VALUE=2>2 hrs\n\
<OPTION VALUE=4>4 hrs\n\
<OPTION VALUE=8>8 hrs\n\
<OPTION VALUE=12>12 hrs\n\
<OPTION VALUE=18>18 hrs\n\
<OPTION VALUE=24>1 day\n\
<OPTION VALUE=48>2 days\n\
<OPTION VALUE=72>3 days\n\
<OPTION VALUE=96>4 days\n\
<OPTION VALUE=120>5 days\n\
<OPTION VALUE=144>6 days\n\
<OPTION VALUE=168>1 week\n\
<OPTION VALUE=336>2 weeks\n\
<OPTION VALUE=504>3 weeks\n\
<OPTION VALUE=672>4 weeks\n\
</SELECT>\n\
Max.<SELECT NAME=maxload>\n\
<OPTION VALUE=0 CHECKED>none\n\
<OPTION VALUE=1>1 hr\n\
<OPTION VALUE=2>2 hrs\n\
<OPTION VALUE=4>4 hrs\n\
<OPTION VALUE=8>8 hrs\n\
<OPTION VALUE=12>12 hrs\n\
<OPTION VALUE=18>18 hrs\n\
<OPTION VALUE=24>1 day\n\
<OPTION VALUE=48>2 days\n\
<OPTION VALUE=72>3 days\n\
<OPTION VALUE=96>4 days\n\
<OPTION VALUE=120>5 days\n\
<OPTION VALUE=144>6 days\n\
<OPTION VALUE=168>1 week\n\
<OPTION VALUE=336>2 weeks\n\
<OPTION VALUE=504>3 weeks\n\
<OPTION VALUE=672>4 weeks\n\
</SELECT>\n\
</NOBR></TD></TR>\n\
\
<TR><TH ALIGN=right>Last Modified:</TH><TD><NOBR>\n\
Min.<SELECT NAME=minlast>\n\
<OPTION VALUE=0 CHECKED>none\n\
<OPTION VALUE=1>1 hr\n\
<OPTION VALUE=2>2 hrs\n\
<OPTION VALUE=4>4 hrs\n\
<OPTION VALUE=8>8 hrs\n\
<OPTION VALUE=12>12 hrs\n\
<OPTION VALUE=18>18 hrs\n\
<OPTION VALUE=24>1 day\n\
<OPTION VALUE=48>2 days\n\
<OPTION VALUE=72>3 days\n\
<OPTION VALUE=96>4 days\n\
<OPTION VALUE=120>5 days\n\
<OPTION VALUE=144>6 days\n\
<OPTION VALUE=168>1 week\n\
<OPTION VALUE=336>2 weeks\n\
<OPTION VALUE=504>3 weeks\n\
<OPTION VALUE=672>4 weeks\n\
</SELECT>\n\
Max.<SELECT NAME=maxlast>\n\
<OPTION VALUE=0 CHECKED>none\n\
<OPTION VALUE=1>1 hr\n\
<OPTION VALUE=2>2 hrs\n\
<OPTION VALUE=4>4 hrs\n\
<OPTION VALUE=8>8 hrs\n\
<OPTION VALUE=12>12 hrs\n\
<OPTION VALUE=18>18 hrs\n\
<OPTION VALUE=24>1 day\n\
<OPTION VALUE=48>2 days\n\
<OPTION VALUE=72>3 days\n\
<OPTION VALUE=96>4 days\n\
<OPTION VALUE=120>5 days\n\
<OPTION VALUE=144>6 days\n\
<OPTION VALUE=168>1 week\n\
<OPTION VALUE=336>2 weeks\n\
<OPTION VALUE=504>3 weeks\n\
<OPTION VALUE=672>4 weeks\n\
</SELECT>\n\
</NOBR></TD></TR>\n\
\
<TR><TH ALIGN=right>Expires:</TH><TD><NOBR>\n\
Min.<SELECT NAME=minexpires>\n\
<OPTION VALUE=0 CHECKED>none\n\
<OPTION VALUE=1>1 hr\n\
<OPTION VALUE=2>2 hrs\n\
<OPTION VALUE=4>4 hrs\n\
<OPTION VALUE=8>8 hrs\n\
<OPTION VALUE=12>12 hrs\n\
<OPTION VALUE=18>18 hrs\n\
<OPTION VALUE=24>1 day\n\
<OPTION VALUE=48>2 days\n\
<OPTION VALUE=72>3 days\n\
<OPTION VALUE=96>4 days\n\
<OPTION VALUE=120>5 days\n\
<OPTION VALUE=144>6 days\n\
<OPTION VALUE=168>1 week\n\
<OPTION VALUE=336>2 weeks\n\
<OPTION VALUE=504>3 weeks\n\
<OPTION VALUE=672>4 weeks\n\
</SELECT>\n\
Max.<SELECT NAME=maxexpires>\n\
<OPTION VALUE=0 CHECKED>none\n\
<OPTION VALUE=1>1 hr\n\
<OPTION VALUE=2>2 hrs\n\
<OPTION VALUE=4>4 hrs\n\
<OPTION VALUE=8>8 hrs\n\
<OPTION VALUE=12>12 hrs\n\
<OPTION VALUE=18>18 hrs\n\
<OPTION VALUE=24>1 day\n\
<OPTION VALUE=48>2 days\n\
<OPTION VALUE=72>3 days\n\
<OPTION VALUE=96>4 days\n\
<OPTION VALUE=120>5 days\n\
<OPTION VALUE=144>6 days\n\
<OPTION VALUE=168>1 week\n\
<OPTION VALUE=336>2 weeks\n\
<OPTION VALUE=504>3 weeks\n\
<OPTION VALUE=672>4 weeks\n\
</SELECT>\n\
</NOBR></TD></TR>\n\
\
<TR><TH ALIGN=right>Hits:</TH><TD><NOBR>\n\
Min.<SELECT NAME=minhits>\n\
<OPTION VALUE=0 CHECKED>none\n\
<OPTION VALUE=1>1\n\
<OPTION VALUE=2>2\n\
<OPTION VALUE=3>3\n\
<OPTION VALUE=4>4\n\
<OPTION VALUE=5>5\n\
<OPTION VALUE=6>6\n\
<OPTION VALUE=7>7\n\
<OPTION VALUE=8>8\n\
<OPTION VALUE=9>9\n\
<OPTION VALUE=10>10\n\
<OPTION VALUE=20>20\n\
<OPTION VALUE=30>30\n\
<OPTION VALUE=40>40\n\
<OPTION VALUE=50>50\n\
<OPTION VALUE=60>60\n\
<OPTION VALUE=70>70\n\
<OPTION VALUE=80>80\n\
<OPTION VALUE=90>90\n\
<OPTION VALUE=100>100\n\
<OPTION VALUE=200>200\n\
<OPTION VALUE=300>300\n\
<OPTION VALUE=400>400\n\
<OPTION VALUE=500>500\n\
<OPTION VALUE=600>600\n\
<OPTION VALUE=700>700\n\
<OPTION VALUE=800>800\n\
<OPTION VALUE=900>900\n\
<OPTION VALUE=1000>1000\n\
</SELECT>\n\
Max.<SELECT NAME=maxhits>\n\
<OPTION VALUE=0 CHECKED>none\n\
<OPTION VALUE=1>1\n\
<OPTION VALUE=2>2\n\
<OPTION VALUE=3>3\n\
<OPTION VALUE=4>4\n\
<OPTION VALUE=5>5\n\
<OPTION VALUE=6>6\n\
<OPTION VALUE=7>7\n\
<OPTION VALUE=8>8\n\
<OPTION VALUE=9>9\n\
<OPTION VALUE=10>10\n\
<OPTION VALUE=20>20\n\
<OPTION VALUE=30>30\n\
<OPTION VALUE=40>40\n\
<OPTION VALUE=50>50\n\
<OPTION VALUE=60>60\n\
<OPTION VALUE=70>70\n\
<OPTION VALUE=80>80\n\
<OPTION VALUE=90>90\n\
<OPTION VALUE=100>100\n\
<OPTION VALUE=200>200\n\
<OPTION VALUE=300>300\n\
<OPTION VALUE=400>400\n\
<OPTION VALUE=500>500\n\
<OPTION VALUE=600>600\n\
<OPTION VALUE=700>700\n\
<OPTION VALUE=800>800\n\
<OPTION VALUE=900>900\n\
<OPTION VALUE=1000>1000\n\
</SELECT>\n\
</NOBR></TD></TR>\n\
\
<TR><TH></TH><TD>\n\
<INPUT TYPE=submit VALUE=\"PCACHE\">\n\
<INPUT TYPE=reset VALUE=\"reset\">\n\
</TD></TR>\n\
\
</TABLE>\n\
</FORM>\n\
<BODY>\n\
<HTML>\n",
      SoftwareID, SoftwareID, CgiScriptNamePtr);

   ThereHasBeenOutput = false;
}

/*****************************************************************************/
/*
Search the cache tree using a wildcard specification or fully-specified file,
it doesn't matter.  With wildcards multiple files will be found, with a full
specified file only the one!  Process each/the file.
*/ 
 
SearchCache (char *FileSpec)

{
   static char  ElapsedTime [32];
   static $DESCRIPTOR (ElapsedFaoDsc, "!%D\0");
   static $DESCRIPTOR (ElapsedTimeDsc, ElapsedTime);
   static unsigned long  StatTimerElapsedTime = 1;

   int  status;
   unsigned long  ElapsedBinTime [2];
   char  *cptr;
   char  DclCommand [256],
         ExpandedFileName [256],
         FileName [256];
   struct FAB  SearchFab;
   struct NAM  SearchNam;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "SearchCache() |%s|\n", FileSpec);

   /* initialize statistics timer */
   if (VMSnok (status = lib$init_timer (&TotalStatTimerContext)))
      exit (status);

   /* we'll just take the chance of including an HTML-forbidden char here! */
   FilterComment[0] = '\0';

   if (UrlFilterPtr)
      sprintf (FilterComment+strlen(FilterComment),
               "\nURL=%s", UrlFilterPtr);
   if (HeaderFilterPtr)
      sprintf (FilterComment+strlen(FilterComment),

               "\nHeader=%s", HeaderFilterPtr);
   if (MinLastModHours)
      sprintf (FilterComment+strlen(FilterComment), "\nMin.Modified=%d",
               MinLastModHours);
   if (MaxLastModHours)
      sprintf (FilterComment+strlen(FilterComment), "\nMax.Modified=%d",
               MaxLastModHours);
   if (MinExpiresHours)
      sprintf (FilterComment+strlen(FilterComment), "\nMin.Expires=%d",
               MinExpiresHours);
   if (MaxExpiresHours)
      sprintf (FilterComment+strlen(FilterComment), "\nMax.Expires=%d",
               MaxExpiresHours);
   if (MinLoadHours)
      sprintf (FilterComment+strlen(FilterComment), "\nMin.Loaded=%d",
               MinLoadHours);
   if (MaxLoadHours)
      sprintf (FilterComment+strlen(FilterComment), "\nMax.Loaded=%d",
               MaxLoadHours);
   if (MinAccessHours)
      sprintf (FilterComment+strlen(FilterComment), "\nMin.Access=%d",
               MinAccessHours);
   if (MaxAccessHours)
      sprintf (FilterComment+strlen(FilterComment), "\nMax.Access=%d",
               MaxAccessHours);
   if (MinHits)
      sprintf (FilterComment+strlen(FilterComment), "\nMin.Hits=%d", MinHits);
   if (MaxHits)
      sprintf (FilterComment+strlen(FilterComment), "\nMax.Hits=%d", MaxHits);

#define CACHE_ROOT_SPEC \
"?WASD_CACHE_ROOT:[*...]*.HTC;0,HT_CACHE_ROOT:[*...]*.HTC;0"

   SearchFab = cc$rms_fab;
   SearchFab.fab$l_dna = v10orPrev10(CACHE_ROOT_SPEC,-1);
   SearchFab.fab$b_dns = 27;
   SearchFab.fab$l_fna = FileSpec;
   SearchFab.fab$b_fns = strlen (FileSpec);
   SearchFab.fab$l_nam = &SearchNam;  

   SearchNam = cc$rms_nam;
   SearchNam.nam$l_esa = ExpandedFileName;
   SearchNam.nam$b_ess = sizeof(ExpandedFileName)-1;
   SearchNam.nam$l_rsa = FileName;
   SearchNam.nam$b_rss = sizeof(FileName)-1;

   if (VMSnok (status = sys$parse (&SearchFab, 0, 0)))
   {
      if (IsCliUtility) exit (status);
      CgiLibResponseError (FI_LI, status, SearchFab.fab$l_dna);
      return;
   }

   SearchNam.nam$l_ver[SearchNam.nam$b_ver] = '\0';
   if (Debug) fprintf (stdout, "ExpandedFileName |%s|\n", ExpandedFileName);

   /*************************/
   /* what needs to be done */
   /*************************/

   /* done here because we need to check on the presence of wildcards */
   if (IsCliUtility)
   {
      if (!(CliDoBody || CliDoHeader || CliDoFull))
      {
         if (SearchNam.nam$l_fnb & NAM$M_WILDCARD)
            DoProfile = true;
         else
            DoFile = true;
      }
      if (CliDoFull) DoFile = true;
      if (DoIndex && !DoNoProfile) DoProfile= true;

      if (CliDoBody || CliDoHeader || CliDoFull || DoFile || DoIndex ||
          HeaderFilterPtr || UrlFilterPtr)
         OpenAndReadFile = true;
      else
         OpenAndReadFile = false;
   }
   else
   {
      CgiDoAnaRms = CgiDoDelete = CgiDoDump = CgiDoResponse = 
         DoFile = DoIndex = DoProfile = false;
      if (strsame (CgiFormDoPtr, "anarms", -1))
         CgiDoAnaRms = true;
      else
      if (strsame (CgiFormDoPtr, "delete", -1))
      {
         if (!CgiRemoteUserPtr[0])
         {
            CgiLibResponseError (FI_LI, 0, "Authorization required!");
            return;
         }
         CgiDoDelete = true;
      }
      else
      if (strsame (CgiFormDoPtr, "dump", -1))
         CgiDoDump = true;
      else
      if (strsame (CgiFormDoPtr, "index", -1))
         DoIndex = true;
      else
      if (strsame (CgiFormDoPtr, "profile", -1))
         DoProfile = true;
      else
      if (strsame (CgiFormDoPtr, "response", -1))
         CgiDoResponse = true;
      else
      {
         if (SearchNam.nam$l_fnb & NAM$M_WILDCARD)
            DoProfile = true;
         else
            DoFile = true;
      }
      if (DoIndex) DoProfile = true;

      if (CgiDoResponse || DoFile || DoIndex ||
          HeaderFilterPtr || UrlFilterPtr)
         OpenAndReadFile = true;
      else
         OpenAndReadFile = false;
   }

   memset (ProfileAccess, 0, sizeof(ProfileAccess));
   memset (ProfileExpires, 0, sizeof(ProfileExpires));
   memset (ProfileLoad, 0, sizeof(ProfileLoad));
   memset (ProfileModified, 0, sizeof(ProfileModified));
   memset (ProfileRevisionCount, 0, sizeof(ProfileRevisionCount));
   FileCount = VersionSixCount = 0;

   /********************/
   /* file search loop */
   /********************/

   for (;;)
   {
      status = sys$search (&SearchFab, 0, 0);
      if (Debug) fprintf (stdout, "sys$search() %%X%08.08X\n", status);
      if (VMSnok (status)) break;

      *SearchNam.nam$l_ver = '\0';
      if (Debug) fprintf (stdout, "FileName |%s|\n", FileName);

      if (IsCliUtility)
         ProcessFile (FileName);
      else
      if (IsCgiScript)
      {
         if (CgiDoAnaRms)
         {
            CgiLibResponseSetRecordMode (1);
            CgiLibResponseHeader (200, "text/plain");
            sprintf (DclCommand, "ANALYZE/RMS %s", FileName);
            system (DclCommand);
            ThereHasBeenOutput = true;
         }
         else
         if (CgiDoDump)
         {
            CgiLibResponseSetRecordMode (1);
            CgiLibResponseHeader (200, "text/plain");
            sprintf (DclCommand, "DUMP %s", FileName);
            system (DclCommand);
            ThereHasBeenOutput = true;
         }
         else
            ProcessFile (FileName);
      }

      *SearchNam.nam$l_ver = ';';
   }

   if (status == RMS$_NMF) status = SS$_NORMAL;

   if (VMSnok (status))
   {
      if (IsCliUtility) exit (status);
      CgiLibResponseError (FI_LI, status, ExpandedFileName);
      return;
   }

   if ((DoIndex || DoProfile) && !DoNoProfile) ProcessProfile ();

   if (DoIndex || DoProfile)
   {
      if (VMSnok (status =
          lib$stat_timer (&StatTimerElapsedTime,
                          &ElapsedBinTime,
                          &TotalStatTimerContext)))
         exit (status);

      if (VMSnok (sys$fao (&ElapsedFaoDsc, 0, &ElapsedTimeDsc,
                           &ElapsedBinTime)))
         strcpy (ElapsedTime, "*ERROR*");
      for (cptr = ElapsedTime;
           *cptr == ' ' || *cptr == '0' || *cptr == ':';
           cptr++);
      CgiLibFaoPrint (stdout, "\nFinished: !20%D (!AZ elapsed)\n", 0, cptr);
      if (VersionSixCount)
         CgiLibFaoPrint (stdout, "Version 6 Cache Files: !UL\n",
                         VersionSixCount);
   }

   if (IsCgiScript)
   {
      if (ThereHasBeenOutput && (DoIndex || DoFile || DoProfile))
         CgiLibFaoPrint (stdout, "</PRE>\n</BODY>\n</HTML>\n");
      else
      if (!ThereHasBeenOutput)
         CgiLibResponseError (FI_LI, 0, "Nothing matched!");
   }

   /* free timer */
   if (VMSnok (status = lib$free_timer (&TotalStatTimerContext)))
      exit (status);
}

/*****************************************************************************/
/*
*/ 
 
ProcessProfile ()

{
   int  idx,
        TotalCount,
        TotalAllocated,
        TotalUsed;
   char  *BeginBoldPtr,
         *EndBoldPtr;
   char BlocksPercent [8],
        CountPercent [8];

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ProcessProfile()\n");

   if (IsCliUtility)
   {
      BeginBoldPtr = "";
      EndBoldPtr = "";
   }
   else
   {
      BeginBoldPtr = "<B>";
      EndBoldPtr = "</B>";
   }

   if (IsCgiScript && DoProfile && !DoIndex)
      CgiLibFaoPrint (stdout, " !UL -->", FileCount);

   PrintProfile ("Modified", &ProfileModified); 

   PrintProfile ("Expires", &ProfileExpires); 

   PrintProfile ("Loaded", &ProfileLoad); 

   PrintProfile ("Accessed", &ProfileAccess); 

   CgiLibFaoPrint (stdout,
"\n!AZ!10<Hits!> !10<     Count!> !4*  !10<      Used!> !10< Allocated!>\n\
!10*- !10*- !4*  !10*- !10*-!AZ\n",
             BeginBoldPtr, EndBoldPtr);

   TotalCount = TotalAllocated = TotalUsed = 0;
   for (idx = 0; idx < 29; idx++)
   {
      if (!ProfileRevisionCount[idx].Count) continue;
      TotalCount += ProfileRevisionCount[idx].Count;
      TotalAllocated += ProfileRevisionCount[idx].BlocksAllocated;
      TotalUsed += ProfileRevisionCount[idx].BlocksUsed;
   }

   for (idx = 0; idx < 29; idx++)
   {
      if (!ProfileRevisionCount[idx].Count) continue;

      PercentOf (ProfileRevisionCount[idx].Count, TotalCount,
                 CountPercent);
      PercentOf (ProfileRevisionCount[idx].BlocksAllocated, TotalAllocated,
                 BlocksPercent);

      CgiLibFaoPrint (stdout, "!10<!AZ!> !10UL !4AZ !10UL !10UL !4AZ\n",
                HitRange[idx],
                ProfileRevisionCount[idx].Count, CountPercent,
                ProfileRevisionCount[idx].BlocksUsed,
                ProfileRevisionCount[idx].BlocksAllocated, BlocksPercent);
   }

   CgiLibFaoPrint (stdout,
"\n!AZ!10*  !10*- !4*  !10*- !10*-\n\
!10<Total:!> !10UL !4*  !10UL !10UL!AZ\n",
             BeginBoldPtr, TotalCount, TotalUsed, TotalAllocated, EndBoldPtr);
}

/*****************************************************************************/
/*
*/ 
 
PrintProfile
(
char *ProfileName,
struct ProfileStruct *ProfileArrayPtr
)
{
   int  DayAllocated,
        DayCount,
        DayUsed,
        FieldWidth,
        Hour,
        NextDay,
        TotalCount,
        TotalBlocks;
   char  *BeginBoldPtr,
         *EndBoldPtr,
         *GreaterThanPtr,
         *LessThanPtr;
   char BlocksPercent [8],
        CountPercent [8];
   struct ProfileStruct  *paptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "PrintProfile()\n");

   if (IsCliUtility)
   {
      BeginBoldPtr = "";
      EndBoldPtr = "";
      GreaterThanPtr = ">";
      LessThanPtr = "<";
      FieldWidth = 10;
   }
   else
   {
      BeginBoldPtr = "<B>";
      EndBoldPtr = "</B>";
      GreaterThanPtr = "&gt;";
      LessThanPtr = "&lt;";
      FieldWidth = 13;
   }

   CgiLibFaoPrint (stdout,
"\n!AZ!10<!AZ!> !10<     Count!> !4<!> \
!10<      Used!> !10< Allocated!> !4<!>\n\
!10*- !10*- !4*  !10*- !10*-!AZ\n",
             BeginBoldPtr, ProfileName, EndBoldPtr);

   paptr = ProfileArrayPtr;
   Hour = TotalCount = TotalBlocks = 0;
   while (Hour <= PROFILE_HOURS_MAX+1)
   {
      if (paptr->Count)
      {
         TotalCount += paptr->Count;
         TotalBlocks += paptr->BlocksAllocated;
      }
      Hour++;
      paptr++;
   }
   if (Debug)
      fprintf (stdout, "Count: %d Blocks: %d\n", TotalCount, TotalBlocks);

   paptr = ProfileArrayPtr;
   Hour = 0;
   while (Hour < PROFILE_HOURS_MAX)
   {
      if (Hour == 0)
      {
         if (paptr->Count)
         {
            PercentOf (paptr->Count, TotalCount, CountPercent);
            PercentOf (paptr->BlocksAllocated, TotalBlocks, BlocksPercent);

            CgiLibFaoPrint (stdout,
               "!#< !AZ1 hrs!> !10UL !4AZ !10UL !10UL !4AZ\n",
               FieldWidth, LessThanPtr, paptr->Count, CountPercent,
               paptr->BlocksUsed, paptr->BlocksAllocated, BlocksPercent);
         }
         Hour++;
         paptr++;
         continue;
      }

      if (Hour < 24)
      {
         if (paptr->Count)
         {
            PercentOf (paptr->Count, TotalCount, CountPercent);
            PercentOf (paptr->BlocksAllocated, TotalBlocks, BlocksPercent);

            CgiLibFaoPrint (stdout,
               "!10<!3UL hrs!> !10UL !4AZ !10UL !10UL !4AZ\n",
               Hour, paptr->Count, CountPercent, 
               paptr->BlocksUsed, paptr->BlocksAllocated, BlocksPercent);
         }
         Hour++;
         paptr++;
         continue;
      }

      if (Hour < PROFILE_HOURS_MAX)
      {
         /* accumulate 24 hours into a per-day total */
         DayCount = DayUsed = DayAllocated = 0;
         NextDay = Hour + 24;
         while (Hour < NextDay)
         {
            DayCount += paptr->Count;
            DayUsed += paptr->BlocksUsed;
            DayAllocated += paptr->BlocksAllocated;
            Hour++;
            paptr++;
         }

         if (DayCount)
         {
            PercentOf (DayCount, TotalCount, CountPercent);
            PercentOf (DayAllocated, TotalBlocks, BlocksPercent);

            CgiLibFaoPrint (stdout,
               "!10<!3UL days!> !10UL !4AZ !10UL !10UL !4AZ\n",
               NextDay/24, DayCount, CountPercent,
               DayUsed, DayAllocated, BlocksPercent);
         }
         continue;
      }
   }

   if (paptr->Count)
   {
      PercentOf (paptr->Count, TotalCount, CountPercent);
      PercentOf (paptr->BlocksAllocated, TotalBlocks, BlocksPercent);

      CgiLibFaoPrint (stdout,
         "!#<!AZ!UL days!> !10UL !4AZ !10UL !10UL !4AZ\n",
         FieldWidth, GreaterThanPtr, Hour/24, paptr->Count, CountPercent, 
         paptr->BlocksUsed, paptr->BlocksAllocated, BlocksPercent);
   }

   paptr++;

   if (paptr->Count)
   {
      PercentOf (paptr->Count, TotalCount, CountPercent);
      PercentOf (paptr->BlocksAllocated, TotalBlocks, BlocksPercent);

      CgiLibFaoPrint (stdout,
         "!#<    none!> !10UL !4AZ !10UL !10UL !4AZ\n",
         FieldWidth-3, paptr->Count, CountPercent, 
         paptr->BlocksUsed, paptr->BlocksAllocated, BlocksPercent);
   }
}

/*****************************************************************************/
/*
*/ 
 
PercentOf 
(
int SubTotal,
int Total,
char *String
)
{
   static $DESCRIPTOR (PercentFaoDsc, "!3UL%\0");
   static $DESCRIPTOR (StringDsc, "");

   int  Percent;
   float  FloatPercent;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "PercentOf() %d %d = ", SubTotal, Total);

   if (Total)
   {
      FloatPercent = (float)SubTotal * 100.0 / (float)Total;
      Percent = (int)FloatPercent;
      if (FloatPercent - (float)Percent >= 0.5) Percent++;
   }
   else
      Percent = 0;
   if (Debug) fprintf (stdout, "%d%%\n", Percent);
   if (String)
   {
      if (Percent)
      {
         StringDsc.dsc$a_pointer = String;
         StringDsc.dsc$w_length = 5;
         if (VMSnok (sys$fao (&PercentFaoDsc, 0, &StringDsc, Percent)))
            strcpy (String, "****");
      }
      else
         String[0] = '\0';
   }
   return (Percent);
}

/*****************************************************************************/
/*
This overly-long function receives a file specification from SearchCache() and
processes it according to the information requested.
*/ 
 
int ProcessFile (char* FileName)

{
   static $DESCRIPTOR (DeviceDsc, "");

   boolean  GzipCompressed;
   int  status,
        idx,
        BlocksAllocated,
        BlocksUsed,
        AccessHours,
        ExpiresHours,
        LastModHours,
        LoadHours;
   unsigned long  AllocatedVbn,
                  EndOfFileVbn;
   unsigned long  CurrentBinTime[2];
   char  ContentBuffer [4096],
         ExpandedFileName [256];
   char  *cptr, *sptr, *zptr,
         *HeaderPtr,
         *PathPtr,
         *UrlPtr;
   struct FAB  FileFab;
   struct NAM  FileNam;
   struct RAB  FileRab;
   struct ProxyCacheFileAcpStruct  *faptr;
   struct ProxyCacheDescrStruct  ProxyCacheDescr;
   struct ProxyCacheFileAcpStruct  FileAcpData;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ProcessFile() |%s|\n", FileName);

   HeaderPtr = UrlPtr = NULL;

   if (IsCliUtility && !ThereHasBeenOutput)
   {
      if (DoIndex)
      {
         CgiLibFaoPrint (stdout,
"!AZ, !20%D!AZ\n\
\n\
        !50<File Name (used/alloc)!>  URL\n\
        !34<Last-Modified (hrs/days)!> \
!34<Loaded (hrs/days)!> \
!34<Expires (hrs/days)!> \
!34<Accessed (hrs/days)!> Count\n\
\n",
            SoftwareID, 0, FilterComment);
         ThereHasBeenOutput = true;
      }
      else
      if (DoFile)
      {
         CgiLibFaoPrint (stdout,
            "!AZ, !20%D!AZ\n\n", SoftwareID, 0, FilterComment);
         ThereHasBeenOutput = true;
      }
      else
      if (DoProfile && !DoIndex)
      {
         CgiLibFaoPrint (stdout,
            "!AZ, !20%D!AZ\n", SoftwareID, 0, FilterComment);
         ThereHasBeenOutput = true;
      }
   }
   else
   if (IsCgiScript && !ThereHasBeenOutput)
   {
      if (DoIndex)
      {
         CgiLibResponseHeader (200, "text/html");

         CgiLibFaoPrint (stdout,
"<HTML>\n\
<HEAD>\n\
<TITLE>!AZ !20%D</TITLE>\n\
</HEAD>\n\
<BODY>\n\
<PRE><B><U>!AZ</U>\n\
!20%D!AZ</B>\n\n\
<B>        !50<File Name (used/alloc)!>  URL\n\
        !34<Last-Modified (hrs/days)!> \
!34<Expires (hrs/days)!> \
!34<Loaded (hrs/days)!> \
!34<Accessed (hrs/days)!> Count</B>\n\
\n",
            SoftwareID, 0,
            SoftwareID, 0, FilterComment);
         fflush (stdout);
         ThereHasBeenOutput = true;
      }
      else
      if (DoFile)
      {
         CgiLibResponseHeader (200, "text/html",
                               "Expires: Fri, 13 JAN 1978 14:00:00 GMT\n");
         CgiLibFaoPrint (stdout,
"<HTML>\n\
<HEAD>\n\
<TITLE>!AZ !20%D</TITLE>\n\
</HEAD>\n\
<BODY>\n\
<PRE><B><U>!AZ</U>\n\
!20%D!AZ</B>\n\n",
            SoftwareID, 0,
            SoftwareID, 0, FilterComment);
         fflush (stdout);
         ThereHasBeenOutput = true;
      }
      else
      if (DoProfile && !DoIndex)
      {
         CgiLibResponseHeader (200, "text/html");

         CgiLibFaoPrint (stdout,
"<HTML>\n\
<HEAD>\n\
<TITLE>!AZ !20%D</TITLE>\n\
</HEAD>\n\
<BODY>\n\
<PRE><B><U>!AZ</U>\n\
!20%D!AZ</B>\
<!!-- files processed:",
            SoftwareID, 0,
            SoftwareID, 0, FilterComment);
         fflush (stdout);
         ThereHasBeenOutput = true;
      }
   }

   if (IsCgiScript && DoProfile && !DoIndex)
   {
      if (FileCount && !(FileCount % 100))
      {
         CgiLibFaoPrint (stdout, " !UL\n", FileCount);
         fflush (stdout);
      }
   }

   /*******************/
   /* open/parse file */
   /*******************/

   FileFab = cc$rms_fab;
   FileFab.fab$b_fac = FAB$M_GET;
   FileFab.fab$l_fna = FileName;
   FileFab.fab$b_fns = strlen(FileName);
   FileFab.fab$l_nam = &FileNam;  
   FileFab.fab$b_shr = FAB$M_SHRGET;

   FileNam = cc$rms_nam;
   FileNam.nam$l_esa = ExpandedFileName;
   FileNam.nam$b_ess = sizeof(ExpandedFileName)-1;

   if (OpenAndReadFile)
   {
      status = sys$open (&FileFab, 0, 0);
      if (Debug) fprintf (stdout, "sys$open() %%X%08.08X\n", status);
   }
   else
   {
      status = sys$parse (&FileFab, 0, 0);
      if (Debug) fprintf (stdout, "sys$parse() %%X%08.08X\n", status);
   }

   if (VMSnok (status))
   {
      if (DoIndex)
      {
         if (IsCliUtility)
            CgiLibFaoPrint (stdout, "!6UL. !AZ %X!XL !AZ\n",
               ++FileCount, FileName, status, SysGetMsg(status));
         else
            CgiLibFaoPrint (stdout, "<B>!6UL.</B> !AZ %X!XL !AZ\n",
               ++FileCount, FileName, status, SysGetMsg(status));
         return (status);
      }
      else
      if (IsCliUtility)
         exit (status);
      else
      {
         CgiLibResponseError (FI_LI, status, FileName);
         return (status);
      }
   }

   /* terminate the expanded file name */
   *FileNam.nam$l_ver = '\0';

   if (OpenAndReadFile)
   {
      /*******************/
      /* connect the RAB */
      /*******************/

      FileRab = cc$rms_rab;
      FileRab.rab$l_fab = &FileFab;
      /* 2 buffers and read ahead performance option */
      FileRab.rab$b_mbf = 2;
      FileRab.rab$l_rop = RAB$M_RAH;

      if (VMSnok (status = sys$connect (&FileRab, 0, 0)))
      {
         sys$close (&FileFab, 0, 0);
         if (DoIndex)
         {
            if (IsCliUtility)
               CgiLibFaoPrint (stdout, "!6UL. !AZ %X!XL !AZ\n",
                  ++FileCount, FileName, status, SysGetMsg(status));
            else
               CgiLibFaoPrint (stdout, "<B>!6UL.</B> !AZ %X!XL !AZ\n",
                  ++FileCount, FileName, status, SysGetMsg(status));
            return (status);
         }
         else
         if (IsCliUtility)
            exit (status);
         else
         {
            CgiLibResponseError (FI_LI, status, FileName);
            exit (status | STS$M_INHIB_MSG);
         }
      }

      /*****************/
      /* file contents */
      /*****************/

      FileRab.rab$l_ubf = (char*)&ProxyCacheDescr;
      FileRab.rab$w_usz = sizeof(ProxyCacheDescr);
      if (VMSnok (status = sys$get (&FileRab, 0, 0)))
      {
         sys$close (&FileFab, 0, 0);
         if (DoIndex)
         {
            if (IsCliUtility)
               CgiLibFaoPrint (stdout, "!6UL. !AZ %X!XL !AZ\n",
                  ++FileCount, FileName, status, SysGetMsg(status));
            else
               CgiLibFaoPrint (stdout, "<B>!6UL.</B> !AZ %X!XL !AZ\n",
                  ++FileCount, FileName, status, SysGetMsg(status));
            return (status);
         }
         else
         if (IsCliUtility)
            exit (status);
         else
         {
            CgiLibResponseError (FI_LI, status, FileName);
            return (status);
         }
      }

      if (Debug)
         fprintf (stdout, "version: %08.08X\n", ProxyCacheDescr.CacheVersion);
      if (ProxyCacheDescr.CacheVersion != PROXY_CACHE_FILE_VERSION_9)
      {
         sys$close (&FileFab, 0, 0);
         if (DoIndex)
         {
            if (ProxyCacheDescr.CacheVersion == PROXY_CACHE_FILE_VERSION_6)
               VersionSixCount++;
            else
            {
               if (IsCliUtility)
                  CgiLibFaoPrint (stdout,
                     "!6UL. !AZ unknown cache file version\n",
                     ++FileCount, FileName);
               else
                  CgiLibFaoPrint (stdout,
                     "<B>!6UL.</B> !AZ unknown cache file version\n",
                     ++FileCount, FileName);
            }
            return (status);
         }
         else
         if (IsCliUtility)
         {
            if (ProxyCacheDescr.CacheVersion == PROXY_CACHE_FILE_VERSION_6)
               VersionSixCount++;
            else
            {
               fprintf (stdout,
"%%%s-E-VERSION, unknown cache file version\n \\%s\\\n",
                        Utility, FileName);
               exit (STS$K_ERROR | STS$M_INHIB_MSG);
            }
         }
         else
         {
            if (ProxyCacheDescr.CacheVersion == PROXY_CACHE_FILE_VERSION_6)
               VersionSixCount++;
            else
            {
               CgiLibResponseError (FI_LI, 0, "Unknown cache file version!");
               return (STS$K_ERROR);
            }
         }
      }

      if (!(UrlPtr = malloc(ProxyCacheDescr.UrlLength)))
      {
         status = vaxc$errno;
         sys$close (&FileFab, 0, 0);
         if (IsCliUtility)
            exit (status);
         else
            CgiLibResponseError (FI_LI, status, "malloc()");
         return (status);
      }
      FileRab.rab$l_ubf = UrlPtr;
      FileRab.rab$w_usz = ProxyCacheDescr.UrlLength;
      if (VMSnok (status = sys$get (&FileRab, 0, 0)))
      {
         sys$close (&FileFab, 0, 0);
         free (UrlPtr);
         if (DoIndex)
         {
            if (IsCliUtility)
               CgiLibFaoPrint (stdout, "!6UL. !AZ %X!XL !AZ\n",
                  ++FileCount, FileName, status, SysGetMsg(status));
            else
               CgiLibFaoPrint (stdout, "<B>!6UL.</B> !AZ %X!XL !AZ\n",
                  ++FileCount, FileName, status, SysGetMsg(status));
            return (status);
         }
         else
         if (IsCliUtility)
            exit (status);
         else
         {
            CgiLibResponseError (FI_LI, status, FileName);
            return (status);
         }
      }
      if (Debug) fprintf (stdout, "|%s|\n", UrlPtr);

      if (UrlFilterPtr)
      {
         /**************/
         /* URL filter */
         /**************/

         if (!SearchTextString (UrlPtr, UrlFilterPtr, false, NULL))
         {
            if (Debug) fprintf (stdout, "URL filtered out\n");
            sys$close (&FileFab, 0, 0);
            free (UrlPtr);
            return (SS$_NORMAL);
         }
      }

      if (CgiDoResponse)
      {
         /****************************/
         /* response header and body */
         /****************************/

         FileRab.rab$l_ubf = ContentBuffer;
         FileRab.rab$w_usz = sizeof(ContentBuffer);
         while (VMSok (status = sys$get (&FileRab, 0, 0)))
            fwrite (ContentBuffer, FileRab.rab$w_rsz, 1, stdout);

         if (Debug) fprintf (stdout, "sys$get() %%X%08.08X\n", status);
         sys$close (&FileFab, 0, 0);
         free (UrlPtr);
         if (status == RMS$_EOF) status = SS$_NORMAL;
         if (VMSnok (status))
            CgiLibResponseError (FI_LI, status, FileName);
         return (status);
      }

      /*******************/
      /* go on to header */
      /*******************/

      if (!(HeaderPtr = malloc(ProxyCacheDescr.HeaderLength+1)))
      {
         status = vaxc$errno;
         sys$close (&FileFab, 0, 0);
         if (DoIndex)
         {
            if (IsCliUtility)
               CgiLibFaoPrint (stdout, "!6UL. !AZ %X!XL !AZ\n",
                  ++FileCount, FileName, status, SysGetMsg(status));
            else
               CgiLibFaoPrint (stdout, "<B>!6UL.</B> !AZ %X!XL !AZ\n",
                  ++FileCount, FileName, status, SysGetMsg(status));
            return (status);
         }
         else
         if (IsCliUtility)
            exit (status);
         else
         {
            CgiLibResponseError (FI_LI, status, "malloc()");
            exit (status);
         }
      }
      FileRab.rab$l_ubf = HeaderPtr;
      FileRab.rab$w_usz = ProxyCacheDescr.HeaderLength;
      if (VMSnok (status = sys$get (&FileRab, 0, 0)))
      {
         sys$close (&FileFab, 0, 0);
         free (UrlPtr);
         free (HeaderPtr);
         if (DoIndex)
         {
            if (IsCliUtility)
               CgiLibFaoPrint (stdout, "!6UL. !AZ %X!XL !AZ\n",
                  ++FileCount, FileName, status, SysGetMsg(status));
            else
               CgiLibFaoPrint (stdout, "<B>!6UL.</B> !AZ %X!XL !AZ\n",
                  ++FileCount, FileName, status, SysGetMsg(status));
            return (status);
         }
         else
         if (IsCliUtility)
            exit (status);
         else
         {
            CgiLibResponseError (FI_LI, status, FileName);
            return (status);
         }
      }
      HeaderPtr[ProxyCacheDescr.HeaderLength] = '\0';
      if (Debug) fprintf (stdout, "|%s|\n", HeaderPtr);

      if (HeaderFilterPtr)
      {
         /*****************/
         /* header filter */
         /*****************/

         if (!SearchTextString (HeaderPtr, HeaderFilterPtr, false, NULL))
         {
            if (Debug) fprintf (stdout, "header filtered out\n");
            sys$close (&FileFab, 0, 0);
            free (UrlPtr);
            free (HeaderPtr);
            return (SS$_NORMAL);
         }
      }

      if (CliDoHeader)
      {
         /***************/
         /* header only */
         /***************/

         fputs (HeaderPtr, stdout);

         sys$close (&FileFab, 0, 0);
         free (UrlPtr);
         free (HeaderPtr);

         return (status);
      }

      if (CliDoBody)
      {
         /**********************/
         /* response body only */
         /**********************/

         /* this would most commonly be to a redirected output file */
         FileRab.rab$l_ubf = ContentBuffer;
         FileRab.rab$w_usz = sizeof(ContentBuffer);
         while (VMSok (status = sys$get (&FileRab, 0, 0)))
            fwrite (ContentBuffer, FileRab.rab$w_rsz, 1, stdout);

         if (Debug) fprintf (stdout, "sys$get() %%X%08.08X\n", status);
         if (status == RMS$_EOF) status = SS$_NORMAL;

         sys$close (&FileFab, 0, 0);
         free (UrlPtr);
         free (HeaderPtr);

         return (status);
      }
   }

   /*****************/
   /* file ACP info */
   /*****************/

   memset (&FileAcpData, 0, sizeof(FileAcpData));
   faptr = &FileAcpData;

   /* assign a channel to the disk device containing the file */
   DeviceDsc.dsc$a_pointer = FileNam.nam$l_dev;
   DeviceDsc.dsc$w_length = FileNam.nam$b_dev;
   if (Debug)
      fprintf (stdout, "device |%*.*s|\n",
               FileNam.nam$b_dev, FileNam.nam$b_dev, FileNam.nam$l_dev);

   status = sys$assign (&DeviceDsc, &faptr->Channel, 0, 0, 0);
   if (Debug) fprintf (stdout, "sys$assign() %%X%08.08X\n", status);
   if (VMSnok (status))
   {
      if (IsCliUtility)
         exit (status);
      else
      {
         CgiLibResponseError (FI_LI, status, FileName);
         exit (status | STS$M_INHIB_MSG);
      }
   }

   /* set up the File Information Block for the ACP interface */
   faptr->FileFibAcpDsc.dsc$w_length = sizeof(struct fibdef);
   faptr->FileFibAcpDsc.dsc$a_pointer = (char*)&faptr->FileFib;

   memcpy (&faptr->FileFib.fib$w_did, &FileNam.nam$w_did, 6);

   faptr->FileNameAcpDsc.dsc$a_pointer = FileNam.nam$l_name;
   faptr->FileNameAcpDsc.dsc$w_length = FileNam.nam$b_name +
                                        FileNam.nam$b_type;
   if (Debug)
      fprintf (stdout, "name |%*.*s|\n",
               faptr->FileNameAcpDsc.dsc$w_length,
               faptr->FileNameAcpDsc.dsc$w_length,
               faptr->FileNameAcpDsc.dsc$a_pointer);

#ifdef __VAX
#define FILEATR_CAST (unsigned int)
#else
#define FILEATR_CAST (void*)
#endif
   faptr->FileAtr[0].atr$w_size = sizeof(faptr->BdtBinTime);
   faptr->FileAtr[0].atr$w_type = ATR$C_BAKDATE;
   faptr->FileAtr[0].atr$l_addr = FILEATR_CAST &faptr->BdtBinTime;
   faptr->FileAtr[1].atr$w_size = sizeof(faptr->CdtBinTime);
   faptr->FileAtr[1].atr$w_type = ATR$C_CREDATE;
   faptr->FileAtr[1].atr$l_addr = FILEATR_CAST &faptr->CdtBinTime;
   faptr->FileAtr[2].atr$w_size = sizeof(faptr->EdtBinTime);
   faptr->FileAtr[2].atr$w_type = ATR$C_EXPDATE;
   faptr->FileAtr[2].atr$l_addr = FILEATR_CAST &faptr->EdtBinTime;
   faptr->FileAtr[3].atr$w_size = sizeof(faptr->RdtBinTime);
   faptr->FileAtr[3].atr$w_type = ATR$C_REVDATE;
   faptr->FileAtr[3].atr$l_addr = FILEATR_CAST &faptr->RdtBinTime;
   faptr->FileAtr[4].atr$w_size = sizeof(faptr->AscDates);
   faptr->FileAtr[4].atr$w_type = ATR$C_ASCDATES;
   faptr->FileAtr[4].atr$l_addr = FILEATR_CAST &faptr->AscDates;
   faptr->FileAtr[5].atr$w_size = sizeof(faptr->RecAttr);
   faptr->FileAtr[5].atr$w_type = ATR$C_RECATTR;
   faptr->FileAtr[5].atr$l_addr = FILEATR_CAST &faptr->RecAttr;
   faptr->FileAtr[6].atr$w_size =
     faptr->FileAtr[6].atr$w_type = 0;
   faptr->FileAtr[6].atr$l_addr = 0;

   status = sys$qiow (0, faptr->Channel, IO$_ACCESS, 0, 0, 0, 
                      &faptr->FileFibAcpDsc, &faptr->FileNameAcpDsc, 0, 0,
                      &faptr->FileAtr, 0);
   if (Debug) fprintf (stdout, "sys$qiow() %%X%08.08X\n", status);

   sys$dassgn (faptr->Channel);

   if (VMSnok (status))
   {
      /* ACP error */
      if (FileFab.fab$w_ifi) sys$close (&FileFab, 0, 0);
      if (UrlPtr) free (UrlPtr);
      if (HeaderPtr) free (HeaderPtr);

      if (DoIndex)
      {
         if (IsCliUtility)
            CgiLibFaoPrint (stdout, "!6UL. !AZ %X!XL !AZ\n",
               ++FileCount, FileNam.nam$l_dir, status, SysGetMsg(status));
         else
            CgiLibFaoPrint (stdout, "<B>!6UL.</B> !AZ %X!XL !AZ\n",
               ++FileCount, FileNam.nam$l_dir, status, SysGetMsg(status));
         return (status);
      }
      else
      if (IsCliUtility)
         exit (status);
      else
      {
         CgiLibResponseError (FI_LI, status, FileName);
         return (status);
      }
   }

   GzipCompressed = faptr->RecAttr.RecordAttrib & RAT_GZIP;

   if ((MinHits && faptr->AscDates.RevisionCount < MinHits) ||
       (MaxHits && faptr->AscDates.RevisionCount > MaxHits))
   {
      /****************/
      /* min/max hits */
      /****************/

      if (FileFab.fab$w_ifi) sys$close (&FileFab, 0, 0);
      if (UrlPtr) free (UrlPtr);
      if (HeaderPtr) free (HeaderPtr);
      return (SS$_NORMAL);
   }

   /***************/
   /* usage/hours */
   /***************/

   AllocatedVbn = faptr->RecAttr.AllocatedVbnLo +
                  (faptr->RecAttr.AllocatedVbnHi << 16);

   EndOfFileVbn = faptr->RecAttr.EndOfFileVbnLo +
                  (faptr->RecAttr.EndOfFileVbnHi << 16);

   BlocksAllocated = AllocatedVbn;
   if (EndOfFileVbn == 1 && !faptr->RecAttr.FirstFreeByte)
      BlocksUsed = 0;
   else
      BlocksUsed = EndOfFileVbn;

   sys$gettim (&CurrentBinTime);
   AccessHours = ProxyCacheDeltaHours (&CurrentBinTime, &faptr->RdtBinTime);
   ExpiresHours = ProxyCacheDeltaHours (&CurrentBinTime, &faptr->EdtBinTime);
   LastModHours = ProxyCacheDeltaHours (&CurrentBinTime, &faptr->CdtBinTime);
   LoadHours = ProxyCacheDeltaHours (&CurrentBinTime, &faptr->BdtBinTime);

   if ((MinAccessHours && AccessHours < MinAccessHours) ||
       (MaxAccessHours && AccessHours > MaxAccessHours) ||
       (MinExpiresHours && ExpiresHours < MinExpiresHours) ||
       (MaxExpiresHours && ExpiresHours > MaxExpiresHours) ||
       (MinLastModHours && LastModHours < MinLastModHours) ||
       (MaxLastModHours && LastModHours > MaxLastModHours) ||
       (MinLoadHours && LoadHours < MinLoadHours) ||
       (MaxLoadHours && LoadHours > MaxLoadHours))
   {
      /*****************/
      /* min/max hours */
      /*****************/

      if (FileFab.fab$w_ifi) sys$close (&FileFab, 0, 0);
      if (UrlPtr) free (UrlPtr);
      if (HeaderPtr) free (HeaderPtr);
      return (SS$_NORMAL);
   }

   if (CgiDoDelete)
   {
      /*******************/
      /* delete the file */
      /*******************/

      if (FileFab.fab$w_ifi) sys$close (&FileFab, 0, 0);
      if (UrlPtr) free (UrlPtr);
      if (HeaderPtr) free (HeaderPtr);

      status = sys$erase (&FileFab, 0, 0);
      if (Debug) fprintf (stdout, "sys$erase() %%X%08.08X\n", status);
      CgiLibResponseHeader (200, "text/plain");
      if (VMSok (status))
         CgiLibFaoPrint (stdout, "%!AZ-I-DELETE, !AZ\n",
                         Utility, FileName);
      else
         CgiLibFaoPrint (stdout, "%!AZ-E-DELETE, !AZ\n-!AZ\n",
                         Utility, FileName, SysGetMsg(status)+1);
      ThereHasBeenOutput = true;
      fflush (stdout);
      return (status);
   }

   /**************/
   /* statistics */
   /**************/

   FileCount++;

   if (LastModHours < PROFILE_HOURS_MAX)
   {
      ProfileModified[LastModHours].Count++;
      ProfileModified[LastModHours].BlocksAllocated += BlocksAllocated;
      ProfileModified[LastModHours].BlocksUsed += BlocksUsed;
   }
   else
   if (LastModHours < ZERO_TIME_DAYS)
   {
      ProfileModified[PROFILE_HOURS_MAX].Count++;
      ProfileModified[PROFILE_HOURS_MAX].BlocksAllocated += BlocksAllocated;
      ProfileModified[PROFILE_HOURS_MAX].BlocksUsed += BlocksUsed;
   }
   else
   {
      ProfileModified[PROFILE_HOURS_MAX+1].Count++;
      ProfileModified[PROFILE_HOURS_MAX+1].BlocksAllocated += BlocksAllocated;
      ProfileModified[PROFILE_HOURS_MAX+1].BlocksUsed += BlocksUsed;
   }

   if (ExpiresHours < PROFILE_HOURS_MAX)
   {
      ProfileExpires[ExpiresHours].Count++;
      ProfileExpires[ExpiresHours].BlocksAllocated += BlocksAllocated;
      ProfileExpires[ExpiresHours].BlocksUsed += BlocksUsed;
   }
   else
   if (ExpiresHours < ZERO_TIME_DAYS)
   {
      ProfileExpires[PROFILE_HOURS_MAX].Count++;
      ProfileExpires[PROFILE_HOURS_MAX].BlocksAllocated += BlocksAllocated;
      ProfileExpires[PROFILE_HOURS_MAX].BlocksUsed += BlocksUsed;
   }
   else
   {
      ProfileExpires[PROFILE_HOURS_MAX+1].Count++;
      ProfileExpires[PROFILE_HOURS_MAX+1].BlocksAllocated += BlocksAllocated;
      ProfileExpires[PROFILE_HOURS_MAX+1].BlocksUsed += BlocksUsed;
   }

   if (LoadHours < PROFILE_HOURS_MAX)
   {
      ProfileLoad[LoadHours].Count++;
      ProfileLoad[LoadHours].BlocksAllocated += BlocksAllocated;
      ProfileLoad[LoadHours].BlocksUsed += BlocksUsed;
   }
   else
   if (LoadHours < ZERO_TIME_DAYS)
   {
      ProfileLoad[PROFILE_HOURS_MAX].Count++;
      ProfileLoad[PROFILE_HOURS_MAX].BlocksAllocated += BlocksAllocated;
      ProfileLoad[PROFILE_HOURS_MAX].BlocksUsed += BlocksUsed;
   }
   else
   {
      ProfileLoad[PROFILE_HOURS_MAX+1].Count++;
      ProfileLoad[PROFILE_HOURS_MAX+1].BlocksAllocated += BlocksAllocated;
      ProfileLoad[PROFILE_HOURS_MAX+1].BlocksUsed += BlocksUsed;
   }

   if (AccessHours < PROFILE_HOURS_MAX)
   {
      ProfileAccess[AccessHours].Count++;
      ProfileAccess[AccessHours].BlocksAllocated += BlocksAllocated;
      ProfileAccess[AccessHours].BlocksUsed += BlocksUsed;
   }
   else
   if (AccessHours < ZERO_TIME_DAYS)
   {
      ProfileAccess[PROFILE_HOURS_MAX].Count++;
      ProfileAccess[PROFILE_HOURS_MAX].BlocksAllocated += BlocksAllocated;
      ProfileAccess[PROFILE_HOURS_MAX].BlocksUsed += BlocksUsed;
   }
   else
   {
      ProfileAccess[PROFILE_HOURS_MAX+1].Count++;
      ProfileAccess[PROFILE_HOURS_MAX+1].BlocksAllocated += BlocksAllocated;
      ProfileAccess[PROFILE_HOURS_MAX+1].BlocksUsed += BlocksUsed;
   }

   /* rev cnt: 1,2,..9, 10-19,20-29...90-99, 100-199,200-299,900-999, 1000+ */
   if (faptr->AscDates.RevisionCount < 10)
      idx = faptr->AscDates.RevisionCount;
   else
   if (faptr->AscDates.RevisionCount < 100)
      idx = 9 + (faptr->AscDates.RevisionCount / 10);
   else
   if (faptr->AscDates.RevisionCount < 1000)
      idx = 18 + (faptr->AscDates.RevisionCount / 100);
   else
      idx = 28;

   ProfileRevisionCount[idx].Count++;
   ProfileRevisionCount[idx].BlocksAllocated += BlocksAllocated;
   ProfileRevisionCount[idx].BlocksUsed += BlocksUsed;

   if (DoProfile && !DoIndex)
   {
      /****************/
      /* profile only */
      /****************/

      if (FileFab.fab$w_ifi) sys$close (&FileFab, 0, 0);
      if (UrlPtr) free (UrlPtr);
      if (HeaderPtr) free (HeaderPtr);
      return (SS$_NORMAL);
   }

   /**********/
   /* report */
   /**********/

   if (IsCliUtility)
   {
      if (DoIndex)
      {
         CgiLibFaoPrint (stdout,
"!6UL. !AZ !11<(!UL/!UL)!>  !AZ\n\
        !20%D !13<(!UL/!UL)!> \
!20%D !13<(!UL/!UL)!> \
!20%D !13<(!UL/!UL)!> \
!20%D !13<(!UL/!UL)!> !UL\n",
            FileCount,
            FileNam.nam$l_dir, BlocksUsed, BlocksAllocated, UrlPtr,
            &faptr->CdtBinTime, TIME_HOURS(LastModHours),
                                TIME_DAYS(LastModHours),
            &faptr->EdtBinTime, TIME_HOURS(ExpiresHours),
                                TIME_DAYS(ExpiresHours),
            &faptr->BdtBinTime, TIME_HOURS(LoadHours),
                                TIME_DAYS(LoadHours),
            &faptr->RdtBinTime, TIME_HOURS(AccessHours),
                                TIME_DAYS(AccessHours),
            faptr->AscDates.RevisionCount);
      }
      else
      if (DoFile)
      {
         CgiLibFaoPrint (stdout,
"   Cache File: !AZ\n\
       Blocks: !UL/!UL\n\
Last-Modified: !20%D !7UL hours !6UL days\n\
      Expires: !20%D !7UL       !6UL\n\
       Loaded: !20%D !7UL       !6UL\n\
     Accessed: !20%D !7UL       !6UL\n\
 Access Count: !UL\n\
 GZIP encoded: !AZ\n\
          URL: !AZ\n\
+-------------------------\n\
!AZ\
+-------------------------\n",
            ExpandedFileName, BlocksUsed, BlocksAllocated,
            &faptr->CdtBinTime, TIME_HOURS(LastModHours),
                                TIME_DAYS(LastModHours),
            &faptr->EdtBinTime, TIME_HOURS(ExpiresHours),
                                TIME_DAYS(ExpiresHours),
            &faptr->BdtBinTime, TIME_HOURS(LoadHours),
                                TIME_DAYS(LoadHours),
            &faptr->RdtBinTime, TIME_HOURS(AccessHours),
                                TIME_DAYS(AccessHours),
            faptr->AscDates.RevisionCount,
            GzipCompressed ? "yes" : "no",
            UrlPtr,
            HeaderPtr);
      }
   }
   else
   if (IsCgiScript)
   {
      PathPtr = VmsToPath (NULL, ExpandedFileName);

      if (DoIndex)
      {
         CgiLibFaoPrint (stdout,
"<B>!6UL.</B> <A HREF=\"!AZ!AZ\">!AZ</A> !11<(!UL/!UL)!>  \
<A HREF=\"!AZ\"><B>!AZ</B></A>\n\
        !20%D !13<(!UL/!UL)!> \
!20%D !13<(!UL/!UL)!> \
!20%D !13<(!UL/!UL)!> \
!20%D !13<(!UL/!UL)!> !UL\n",
            FileCount,
            CgiScriptNamePtr, PathPtr, FileNam.nam$l_dir,
            BlocksUsed, BlocksAllocated,
            UrlPtr, UrlPtr,
            &faptr->CdtBinTime, TIME_HOURS(LastModHours),
                                TIME_DAYS(LastModHours),
            &faptr->EdtBinTime, TIME_HOURS(ExpiresHours),
                                TIME_DAYS(ExpiresHours),
            &faptr->BdtBinTime, TIME_HOURS(LoadHours),
                                TIME_DAYS(LoadHours),
            &faptr->RdtBinTime, TIME_HOURS(AccessHours),
                                TIME_DAYS(AccessHours),
            faptr->AscDates.RevisionCount);

         /* ensure more immediate feedback when filtering cache files */
         if (HeaderFilterPtr ||
             UrlFilterPtr ||
             MinAccessHours || MaxAccessHours ||
             MinLastModHours || MaxLastModHours ||
             MinLoadHours || MaxLoadHours ||
             MinHits || MaxHits)
            fflush (stdout);
      }
      else
      if (DoFile)
      {
         CgiLibFaoPrint (stdout,
"   <B>Cache File:</B> !AZ\n\
       <B>Blocks</B>: !UL/!UL\n\
<B>Last-Modified:</B> !20%D !7UL hours !6UL days\n\
      <B>Expires:</B> !20%D !7UL       !6UL\n\
       <B>Loaded:</B> !20%D !7UL       !6UL\n\
     <B>Accessed:</B> !20%D !7UL       !6UL\n\
 <B>Access Count:</B> !UL\n\
 <B>GZIP encoded:</B> !AZ\n\
          <B>URL:</B> <A HREF=\"!AZ\">!AZ</A>\n\
\n\
<HR SIZE=1 WIDTH=85% ALIGN=left>\
!AZ\
<HR SIZE=1 WIDTH=85% ALIGN=left>\
<B>[<A HREF=\"!AZ!AZ?do=response\">VIEW</A>] \
[<A HREF=\"!AZ!AZ?do=dump\">DUMP</A>] \
[<A HREF=\"!AZ!AZ?do=anarms\">ANA/RMS</A>] \
[<A HREF=\"!AZ!AZ?do=delete\">DELETE!!</A>]</B>\n",
            ExpandedFileName, BlocksUsed, BlocksAllocated,
            &faptr->CdtBinTime, TIME_HOURS(LastModHours),
                                TIME_DAYS(LastModHours),
            &faptr->EdtBinTime, TIME_HOURS(ExpiresHours),
                                TIME_DAYS(ExpiresHours),
            &faptr->BdtBinTime, TIME_HOURS(LoadHours),
                                TIME_DAYS(LoadHours),
            &faptr->RdtBinTime, TIME_HOURS(AccessHours),
                                TIME_DAYS(AccessHours),
            faptr->AscDates.RevisionCount,
            GzipCompressed ? "yes" : "no",
            UrlPtr, UrlPtr,
            HeaderPtr,
            CgiScriptNamePtr, PathPtr,
            CgiScriptNamePtr, PathPtr,
            CgiScriptNamePtr, PathPtr,
            CgiScriptNamePtr, PathPtr);
      }
   }

   /**********/
   /* finish */
   /**********/

   if (FileFab.fab$w_ifi) sys$close (&FileFab, 0, 0);
   if (UrlPtr) free (UrlPtr);
   if (HeaderPtr) free (HeaderPtr);

   return (SS$_NORMAL);
}

/****************************************************************************/
/*
Return the age in hours relative to the the supplied time.
*/

int ProxyCacheDeltaHours
(
unsigned long *BinaryTimePtr,
unsigned long *AgeBinaryTimePtr
)
{
   static unsigned long  LibDeltaHours = LIB$K_DELTA_HOURS;

   int  status;
   unsigned long  LastModHours;
   unsigned long  BinTime[2],
                  ResultBinTime [2];

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ProxyCacheDeltaHours()\n");

   if (!AgeBinaryTimePtr[0] && !AgeBinaryTimePtr[1]) return (ZERO_TIME);

   if (!BinaryTimePtr) sys$gettim (BinaryTimePtr = (unsigned long*)&BinTime);

   status = lib$sub_times (BinaryTimePtr,
                           AgeBinaryTimePtr,
                           &ResultBinTime);
   if (Debug) fprintf (stdout, "lib$sub_times() %%X%08.08X\n", status);
   if (status == LIB$_NEGTIM) return (0);

   status = lib$cvt_from_internal_time (&LibDeltaHours,
                                        &LastModHours,
                                        &ResultBinTime);
   if (Debug) fprintf (stdout, "lib$cvt_() %%X%08.08X\n", status);

   if (Debug) fprintf (stdout, "hours: %d\n", LastModHours);
   return (LastModHours);
}

/*****************************************************************************/
/*
Convert a VMS file specification into a URL-style specification.  For example:
"DEVICE:[DIR1.DIR2]FILE.TXT" into "/device/dir1/dir2/file.txt".
*/ 
 
char* VmsToPath
(
char *PathPtr,
char *VmsPtr
)
{
   static char  Path [256];

   char  *pptr, *vptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "VmsToPath() |%s|\n", VmsPtr);

   vptr = VmsPtr;
   if (!(pptr = PathPtr)) pptr = PathPtr = Path;
   *pptr++ = '/';
   /* copy the device and directory components */
   while (*vptr)
   {
      if (*vptr == ':' && *(vptr+1) == '[')
      {
         vptr++;
         vptr++;
         /* remove any reference to a Master File Directory */
         if (strncmp (vptr, "000000", 6) == 0)
            vptr += 6;
         else
            *pptr++ = '/';
      }
      if (*vptr == '.')
      {
         if (vptr[1] == '.' && vptr[2] == '.')
         {
            *pptr++ = '/';
            *pptr++ = *vptr++;
            *pptr++ = *vptr++;
            *pptr++ = *vptr++;
         }
         else
         {
            vptr++;
            *pptr++ = '/';
         }
      }
      else
      if (*vptr == ']')
      {
         vptr++;
         *pptr++ = '/';
         break;
      }
      else
         *pptr++ = tolower(*vptr++);
   }
   /* copy the file component */
   while (*vptr) *pptr++ = tolower(*vptr++);
   *pptr++ = '\0';
   *pptr = '\0';

   if (Debug) fprintf (stdout, "PathPtr |%s|\n", PathPtr);

   return (PathPtr);
}

/*****************************************************************************/
/*
String search allowing wildcard "*" (matching any multiple characters) and "%" 
(matching any single character).  Returns NULL if not found or a pointer to
start of matched string.
*/ 

char* SearchTextString
( 
char *InThat,
char *This,
boolean CaseSensitive,
int *MatchedLengthPtr
)
{
/* wildcards implied at both ends of the search string */
#define IMPLIED_WILDCARDS 0

   char  *cptr, *sptr, *inptr,
        *RestartCptr,
         *RestartInptr,
         *MatchPtr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "SearchTextString()\n|%s|%s|\n", This, InThat);

   if (MatchedLengthPtr) *MatchedLengthPtr = 0;
   if (!*(cptr = This)) return (NULL);
   inptr = MatchPtr = InThat;

#if IMPLIED_WILDCARDS
   /* skip leading text up to first matching character (if any!) */
   if (*cptr != '*' && *cptr != '%')
   {
      if (CaseSensitive)
         while (*inptr && *inptr != *cptr) inptr++;
      else
         while (*inptr && toupper(*inptr) != toupper(*cptr)) inptr++;
      if (Debug && !*inptr) fprintf (stdout, "1. NOT matched!\n");
      if (!*inptr) return (NULL);
      cptr++;
      MatchPtr = inptr++;
   }
#endif /* IMPLIED_WILDCARDS */

   for (;;)
   {
      if (CaseSensitive)
      {
         while (*cptr && *inptr && *cptr == *inptr)
         {
            cptr++;
            inptr++;
         }
      }
      else
      {
         while (*cptr && *inptr && toupper(*cptr) == toupper(*inptr))
         {
            cptr++;
            inptr++;
         }
      }

#if IMPLIED_WILDCARDS
      if (!*cptr)
      {
         if (Debug) fprintf (stdout, "1. matched!\n");
         if (MatchedLengthPtr) *MatchedLengthPtr = inptr - MatchPtr;
         return (MatchPtr);
      }
#else
      if (!*cptr && !*inptr)
      {
         if (Debug) fprintf (stdout, "2. matched!\n");
         if (MatchedLengthPtr) *MatchedLengthPtr = inptr - MatchPtr;
         return (MatchPtr);
      }
#endif /* IMPLIED_WILDCARDS */

      if (*cptr != '*' && *cptr != '%')
      {
         if (Debug) fprintf (stdout, "3. NOT matched!\n");
         return (NULL);
      }

      if (*cptr == '%')
      {
         /* single char wildcard processing */
         if (!*inptr)
         {
            if (Debug) fprintf (stdout, "4. NOT matched!\n");
            return (NULL);
         }
         cptr++;
         inptr++;
         continue;
      }

      /* asterisk wildcard matching */
      while (*cptr == '*') cptr++;

      /* an asterisk wildcard at end matches all following */
      if (!*cptr)
      {
         if (Debug) fprintf (stdout, "5. matched!\n");
         while (*inptr) inptr++;
         if (MatchedLengthPtr) *MatchedLengthPtr = inptr - MatchPtr;
         return (MatchPtr);
      }

      /* note the current position in the string (first after the wildcard) */
      RestartCptr = cptr;
      for (;;)
      {
         /* find first char in InThat matching char after wildcard */
         if (CaseSensitive)
            while (*inptr && *cptr != *inptr) inptr++;
         else
            while (*inptr && toupper(*cptr) != toupper(*inptr)) inptr++;
         /* if did not find matching char in InThat being searched */
         if (Debug && !*inptr) fprintf (stdout, "6. NOT matched!\n");
         if (!*inptr) return (NULL);
         /* note the current position in InThat being searched */
         RestartInptr = inptr;
         /* try to match the remainder of the string and InThat */
         if (CaseSensitive)
         {
            while (*cptr && *inptr && *cptr == *inptr)
            {
               cptr++;
               inptr++;
            }
         }
         else
         {
            while (*cptr && *inptr && toupper(*cptr) == toupper(*inptr))
            {
               cptr++;
               inptr++;
            }
         }
         /* if reached the end of both string and InThat - match! */
#if IMPLIED_WILDCARDS
         if (!*cptr)
         {
            if (Debug) fprintf (stdout, "7. matched!\n");
            if (MatchedLengthPtr) *MatchedLengthPtr = inptr - MatchPtr;
            return (MatchPtr);
         }
#else
         if (!*cptr && !*inptr)
         {
            if (Debug) fprintf (stdout, "8. matched!\n");
            if (MatchedLengthPtr) *MatchedLengthPtr = inptr - MatchPtr;
            return (MatchPtr);
         }
#endif /* IMPLIED_WILDCARDS */
         /* break to the external loop if we encounter another wildcard */
         if (*cptr == '*' || *cptr == '%') break;
         /* lets have another go */
         cptr = RestartCptr;
         /* starting the character following the previous attempt */
         inptr = MatchPtr = RestartInptr + 1;
      }
   }
}

/*****************************************************************************/
/*
*/
 
char* SysGetMsg (int StatusValue)
 
{
   static char  Message [256];
   short int  Length;
   $DESCRIPTOR (MessageDsc, Message);
 
   sys$getmsg (StatusValue, &Length, &MessageDsc, 0, 0);
   Message[Length] = '\0';
   if (Debug) fprintf (stdout, "SysGetMsg() |%s|\n", Message);
   return (Message);
}
 
/*****************************************************************************/
/*
From [SRC.HTTPD]SUPPORT.C
Support version 10 and pre-version-10 logical and file naming conventions. 
Look for one of multiple (usually just two but can be more) possible logical
names.  When one is successfully translated return a pointer to it's null
terminated name.  Otherwise test for a subsequent and return a pointer to it if
found.  If none is found then return NoneFound if zero or positive, return the
LogicalName if minus one.  The constants should be #defined in the manner of
"?WASD_CONFIG_GLOBAL\0HTTPD$CONFIG\0".
*/

char* v10orPrev10
(
char *LogicalName,
int NoneFound
)
{
   static unsigned short  Length;
   static char  ValueString [128];
   static $DESCRIPTOR (LogicalNameDsc, "");
   static $DESCRIPTOR (LnmFileDevDsc, "LNM$FILE_DEV");
   static struct
   {
      unsigned short  buf_len;
      unsigned short  item;
      void  *buf_addr;
      void  *ret_len;
   } LnmItems [] =
   {
      { sizeof(ValueString)-1, LNM$_STRING, ValueString, &Length },
      { 0,0,0,0 }
   };

   int  status;
   char  *cptr, *sptr;

   /*********/
   /* begin */
   /*********/

   if (!LogicalName || LogicalName[0] != '?') return (LogicalName);

   /* stop at any logical name delimiting colon (perhaps of a file name) */
   for (sptr = cptr = LogicalName+1; *sptr && *sptr != ':'; sptr++);
   while (*cptr)
   {
      LogicalNameDsc.dsc$a_pointer = cptr;
      LogicalNameDsc.dsc$w_length = sptr - cptr;
      status = sys$trnlnm (0, &LnmFileDevDsc, &LogicalNameDsc, 0, &LnmItems);
      if (VMSok (status)) return (cptr);
      /* scan past any logical-delimiting colon */
      while (*sptr) sptr++;
      /* stop at any logical name delimiting colon (perhaps of a file name) */
      for (cptr = (sptr += 1); *sptr && *sptr != ':'; sptr++);
   }

   if (NoneFound != -1) return ((char*)NoneFound);
   return (LogicalName+1);
}

/*****************************************************************************/
/*
Get "command-line" parameters, whether from the command-line or from a
configuration symbol or logical containing the equivalent.
*/

GetParameters ()

{
   static char  CommandLine [256];
   static unsigned long  Flags = 0;

   int  status;
   unsigned short  Length;
   char  ch;
   char  *aptr, *cptr, *clptr, *sptr;
   $DESCRIPTOR (CommandLineDsc, CommandLine);

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GetParameters()\n");

   if (!(clptr = getenv ("PCACHE$PARAM")))
   {
      /* get the entire command line following the verb */
      if (VMSnok (status =
          lib$get_foreign (&CommandLineDsc, 0, &Length, &Flags)))
         exit (status);
      (clptr = CommandLine)[Length] = '\0';
   }

   aptr = NULL;
   ch = *clptr;
   for (;;)
   {
      if (aptr && *aptr == '/') *aptr = '\0';
      if (!ch) break;

      *clptr = ch;
      if (Debug) fprintf (stdout, "clptr |%s|\n", clptr);
      while (*clptr && isspace(*clptr)) *clptr++ = '\0';
      aptr = clptr;
      if (*clptr == '/') clptr++;
      while (*clptr && !isspace (*clptr) && *clptr != '/')
      {
         if (*clptr != '\"')
         {
            clptr++;
            continue;
         }
         cptr = clptr;
         clptr++;
         while (*clptr)
         {
            if (*clptr == '\"')
               if (*(clptr+1) == '\"')
                  clptr++;
               else
                  break;
            *cptr++ = *clptr++;
         }
         *cptr = '\0';
         if (*clptr) clptr++;
      }
      ch = *clptr;
      if (*clptr) *clptr = '\0';
      if (Debug) fprintf (stdout, "aptr |%s|\n", aptr);
      if (!*aptr) continue;

      if (strsame (aptr, "/AUTHORIZED", 4))
      {
         CliMustBeAuthorized = true;
         continue;
      }
      if (strsame (aptr, "/BODY=", 4))
      {
         CliDoBody = true;
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (!*cptr) continue;
         CliOutputPtr = cptr+1;
         continue;
      }
      if (strsame (aptr, "/CHARSET=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         CliCharsetPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/DBUG", -1))
      {
         Debug = true;
         continue;
      }
      if (strsame (aptr, "/DELETE", 4))
      {
         CliDoDelete = true;
         continue;
      }
      if (strsame (aptr, "/FULL", 4))
      {
         CliDoFull = true;
         continue;
      }
      if (strsame (aptr, "/FILTER=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (!*cptr) continue;
         cptr++;
         if (toupper(*cptr) == 'H')
         {
            while (*cptr && *cptr != '=' && *cptr != ':') cptr++;
            if (!*cptr) continue;
            HeaderFilterPtr = cptr+1;
            continue;
         }
         if (toupper(*cptr) == 'U')
         {
            while (*cptr && *cptr != '=' && *cptr != ':') cptr++;
            if (!*cptr) continue;
            UrlFilterPtr = cptr+1;
            continue;
         }
         continue;
      }
      if (strsame (aptr, "/HEADER=", 4))
      {
         CliDoHeader = true;
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (!*cptr) continue;
         CliOutputPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/INDEX=", 4))
      {
         DoIndex = true;
         continue;
      }
      if (strsame (aptr, "/MAXACCESSED=", 6))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (!*cptr) continue;
         MaxAccessHours = atoi(cptr);
         continue;
      }
      if (strsame (aptr, "/MAXEXPIRES=", 6))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (!*cptr) continue;
         MaxExpiresHours = atoi(cptr);
         continue;
      }
      if (strsame (aptr, "/MAXHITS=", 5))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (!*cptr) continue;
         MaxHits = atoi(cptr);
         continue;
      }
      if (strsame (aptr, "/MAXLASTMOD=", 6) ||
          /* bit of backward compatibility */
          strsame (aptr, "/MAXAGE=", 6))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (!*cptr) continue;
         MaxLastModHours = atoi(cptr);
         continue;
      }
      if (strsame (aptr, "/MAXLOAD=", 5))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (!*cptr) continue;
         MaxLoadHours = atoi(cptr);
         continue;
      }
      if (strsame (aptr, "/MINACCESSED=", 6))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (!*cptr) continue;
         MinAccessHours = atoi(cptr);
         continue;
      }
      if (strsame (aptr, "/MINEXPIRES=", 6))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (!*cptr) continue;
         MinExpiresHours = atoi(cptr);
         continue;
      }
      if (strsame (aptr, "/MINHITS=", 5))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (!*cptr) continue;
         MinHits = atoi(cptr);
         continue;
      }
      if (strsame (aptr, "/MINLASTMOD=", 6) ||
          /* bit of backward compatibility */
          strsame (aptr, "/MINAGE=", 6))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (!*cptr) continue;
         MinLastModHours = atoi(cptr);
         continue;
      }
      if (strsame (aptr, "/MINLOAD=", 5))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (!*cptr) continue;
         MinLoadHours = atoi(cptr);
         continue;
      }
      if (strsame (aptr, "/OUTPUT=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         CliOutputPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/PROFILE=", 4))
      {
         DoProfile = true;
         continue;
      }
      if (strsame (aptr, "/NOPROFILE=", 6))
      {
         DoNoProfile = true;
         continue;
      }

      if (*aptr == '/')
      {
         fprintf (stdout, "%%%s-E-IVQUAL, unrecognized qualifier\n \\%s\\\n",
                  Utility, aptr+1);
         exit (STS$K_ERROR | STS$M_INHIB_MSG);
      }

      if (!CliFileSpecPtr)
      {
         CliFileSpecPtr = aptr;
         continue;
      }

      if (CliFileSpecPtr)
      {
         fprintf (stdout, "%%%s-E-MAXPARM, too many parameters\n \\%s\\\n",
                  Utility, aptr);
         exit (STS$K_ERROR | STS$M_INHIB_MSG);
      }
   }
}

/****************************************************************************/
/*
Does a case-insensitive, character-by-character string compare and returns 
true if two strings are the same, or false if not.  If a maximum number of 
characters are specified only those will be compared, if the entire strings 
should be compared then specify the number of characters as 0.
*/ 

boolean strsame
(
char *sptr1,
char *sptr2,
int  count
)
{
   while (*sptr1 && *sptr2)
   {
      if (toupper (*sptr1++) != toupper (*sptr2++)) return (false);
      if (count)
         if (!--count) return (true);
   }
   if (*sptr1 || *sptr2)
      return (false);
   else
      return (true);
}

/*****************************************************************************/

