/*****************************************************************************/
/*
                                Cache.c

This module implements both a file data and revision time cache, and a general
non-file (e.g. script) output cache.  With file content caching the file
revision time is stored (along with the directory and file IDs for efficiency)
allowing the entry validity to expire and to be periodically revalidated
against the on-disk version.  If the on-disk version has changed the cache is
purged and the content reloaded, otherwise the expiry period just recommences. 
For non-file content the revalidation is not possible so when the validity of
the cache entry expires the entry is purged from the cache forcing it to be
reloaded through full request processing. 

The cache has limits imposed on it.  A maximum number of files that can be
cached at any one time, and a maximum amount of memory that can be allocated
in total by the cache.  Each of these two operates to limit the other.  In
addition the maximum size of a file that can be loaded into the cache can be
specified.

Fine control on cache behaviour is exercised using mapping rules.
See the MAPURL.C module for a list of those applicable.


FILE CONTENT
------------
File content cache data is loaded by the file module while concurrently
transfering that data to the request client, using buffer space supplied by
cache module, space that can then be retained for reuse as cache.  Hence the
cache load adds no significant overhead to the actual reading and initial
transfer of the file.  An entirely accurate estimate of the quantity of cache
memory required for file content can be derived from the file header.  Cache
loads that exceed the configuration or rule mapping maxima do not proceed. 


NON-FILE CONTENT
----------------
Non-file content is a little different from file content in that the size of
the cache memory required is generally not known when the cache load begins
(content-length is often not supplied with script output for example).  When
this is not known the cache module just allocates an ambit quantity of memory,
determined by configuration or rule mapping indicated maxima.  When the load
successfully completes the actual memory required is compared to that
originally allocated and reduced as necessary.  If non-file output exceeds the
original ambit memory allocation (and hence configuration or rule maxima) the
data is just discarded at end-of-request.

Non-file content entries are sourced in two ways.  First, from the CGI module. 
This can discriminate and load either (or both) CGI-compliant responses and
Non-Parse-Header (NPH) responses.  The former allows the server to generate the
response header, the latter contains the response header as the leading output
from the script.  Cached CGI responses generate a new header with each
subsequent use, while cached NPH response reuse the original header.  The
second source of cached response is direct from network-to-client output via
the NET module.  Like scripts, network loaded content can contain only the body
of the response, or the full header and body (viz. NPH).  Some care should be
excercised with the caching of responses using the NET source.

By default only GET requests, without query strings, generating responses with
a success HTTP status (200) will be cached.  Other response status content will
be discarded.  Requests containing query strings may be cached by using the
'cache=query' mapping rule.  This sort of caching should be done carefully and
selectively.  The multitude of differing query string usually accepted by such
resources would soon fill the cache with generally non-repeated data and thus
render the cache completely ineffective.


BYTE RANGES
-----------
See comments in FILE.C module.


TERMINOLOGY
-----------
"hit" refers to a request path being found in cache. If the data is still
valid the request can be supplied from cache.

"load"ing the cache refers to reading the contents of a file into cache
memory.

"valid" means that the file from which the cached data was originally read has
not had it's revision date changed (the implication being is the file contents
have not changed.


WHY IMPLEMENT CACHING?
----------------------
Caching, be definition, attempts to improve performance by keeping data in
storage that is faster to access than it's usual location.  The performance
improvement can be assessed in three basic ways.  Reduction in latency when
accessing the data, of processing involved, and in impact on the usual storage
location.

This cache is provided to address all three.  Where networks are particularly
responsive a reduction in request latency can often be noticable.  Where
servers are particularly busy or where disk systems particularly loaded a
reduction in the need to access the file system can significantly improve
performance.  My suggestion is though, that for most VMS sites high levels of
hits are not a great concern, and for these caching can easily be left
disabled.
            
CACHE SUITABILITY CONSIDERATIONS
--------------------------------
A cache is not always of benefit!  It's cost *may* outweigh it's return.

Any cache's efficiencies can only occur where subsets of data are consistently
being demanded. Although these subsets may change slowly over time a consistent
and rapidly changing aggregate of requests lose the benefit of more readily
accessable data to the overhead of cache management due to the constant and
continuous flushing and reloading of cache data. This server's cache is no
different, it will only improve performance if the site experiences some
consistency in the files requested. For sites that have only a small
percentage of files being repeatedly requested it is probably better that the
cache be disabled. The other major consideration is available system memory.
On a system where memory demand is high there is little value in having cache
memory sitting in page space, trading disk I/O and latency for paging I/O and
latency. On memory-challenged systems cache is probably best disabled.

With "loads not hit", the count represents the cumulative number of files
loaded but never subsequently hit. If this percentage is high it means most
files loaded are never hit, indicating the site's request profile is possibly
unsuitable for caching.

The item "hits" respresents the cumulative, total number of hits against the
cumulative, total number of loads. The percentage here can range from zero to
many thousands of percent :^) with less than 100% indicating poor cache
performance, from 200% upwards better and good performance. The items "1-9",
"10-99" and "100+" show the count and percentage of total hits that occured
when a given entry had experienced hits within that range (e.g. if an entry
has had 8 previous hits, the ninth increments the "1-9" item whereas the tenth
and eleventh increments the "10-99" item, etc.)

Other considerations also apply when assessing the benefit of having a cache.
For example, a high number and percentage of hits can be generated while the
percentage of "loads not hit" could be in the also be very high. The
explanation for this would be one or two frequently requested files being hit
while most others are loaded, never hit, and flushed as other files request
cache space. In situations such as this it is difficult to judge whether cache
processing is improving performance or just adding overhead.

Again, my suggestion is, that for most VMS sites, high levels of access are not
a great concern, and for these caching can easily be left disabled.
            

DESCRIPTION
-----------
An MD5 digest (16 byte hash) is used to uniquely identify cached files.  The
hash is generated either from a mapped path or from the file name before
calling any of the cache search, load or completion functions.  This MD5 hash
is then stored along with the file details and contents so that identical
paths/files can be matched during subsequent searches.  The MD5 algorithm
"guarantees" a unique identifier for any resource name, and the 16 byte size
makes matching using just 4 longword comparisons very efficient.

If using a path it MUST be the mapped path, not the client-supplied, request
path.  With conditional mapping, identical request paths may be mapped to
completely different virtual paths.

Space for a file's data is dynamically allocated and reallocated if necessary
as cache entries are reused.  It is allocated in user-specifiable chunks.  It
is expected this mechanism provides some efficiencies when reusing cache
entries.

A simple hash table is used to try and initially hit an entry.  A collision
list allows rapid subsequent searching.  The hash table is a fixed 4096 entries
with the hash value generated by directly using a fixed three bytes of the
MD5 hash for a range from 0..4095.

Cache entries are also maintained in a global linked list with the most recent
and most frequently hit entries towards the head of the list. The linked-list
organisation allows a simple implementation of a least-recently-used (LRU)
algorithm for selecting an entry when a new request demands an entry and space
for cache loading. The linked list is naturally ordered from most recently and
most frequently accessed at the head, to the least recently and least
frequently accessed at the tail. Hence an infrequently accessed entry is
selected from the tail end of the list, it's data invalidated and given to the
new request for cache load. Invalidated data cache entries are also
immediately placed at the tail of the list for reuse/reloading.

When a new entry is initially loaded it is placed at the top of the list. Hits
on other entries result in a check being made against the number of hits of
head entry in the list. If the entry being hit has a higher hit count it is
placed at the head of the list, pushing the previously head entry "down". If
not then it is again checked against the entry immediately before it in the
list. If higher then the two are swapped. This results in the most recently
loaded entries and the more frequently hit being nearest and migrating towards
the start of the search.

To help prevent the cache thrashing with floods of requests for not currently
loaded files, any entry that has a suitably high number of hits over the recent
past (suitably high ... how many is that, and recent past ... how long is
that?) are not reused until no hits have occured within that period.  Hopefully
this prevents lots of unnecessary loads of one-offs at the expense of genuinely
frequently accessed files.

To prevent multiple loads of the same path/file, for instance if a subsequent
request demands the same file as a previous request is still currently loading,
any subsequent request will merely transfer the file, not concurrently load it
into the cache.


CACHE CONTENT VALIDATION
------------------------
The cache will automatically revalidate the data after a specified number of
seconds.  With file content this is done by comparing the original file
revision time to the current revision time.  If different the file contents
have changed and the cache contents declared invalid.  If found invalid the
file transfer then continues outside of the cache with the new contents being
concurrently reloaded into the cache. With non-file content the entry is just
purged and the full request processing is used to reload the content. Cache
validation is also always performed if the request uses "Pragma: no-cache"
(i.e. as with the Netscape Navigator reload function).  Hence there is no need
for any explicit flushing of the cache under normal operation.  If a document
does not immediately reflect and changes made to it (i.e. validation time has
not been reached) validation (and consequent reload) can be "forced" with a
browser reload.  There is a discretional "guard" period that can be set which
prevent forced reloading of cached data within the specified number of seconds
since last loaded or revalidated.  This period can be used to prevent a cache
entry or entries from constantly being reloaded through pragma directives
(Mozilla for instance has a user-option cache setting which causes the request
header always to contain a 'no-cache' indication causing reload).  The entire
cache may be purged of cached data either from the server administration menu
or using command line server control.


PERMANENT ENTRIES
-----------------
Permanent entries are indicated by a path mapping a SET rule.  Permanent
entries are designed to allow certain classes of (mainly file) entry, those
that are relatively static and/or frequently being used by requests, that once
loaded remain in the cache, never validated, never able to be reclaimed or
otherwise removed from the cache during routine server activity.  These
entries, along with the volatile ones, can of course be purged from the cache
either via the CLI /DO=CACHE=PURGE command or using Server Administration menu. 
Permanent entries do not use any of the data memory ([CacheTotalKBytesMax]) set
aside for the volatile cache.  They use memory from their own specific VM pool. 
Unlike for volatile entries there is no upper limit (apart from system and
server process virtual memory) on the memory that can be allocated for
permanent entries (be careful!).  Entry slots ([CacheEntriesMax]) are shared
between permanent and volatile entries (and can be a constraint on both).


VERSION HISTORY
---------------
15-MAY-2004  MGD  bugfix; content pointer needs to be NULLed
                  before first call to CacheNext()
24-APR-2004  MGD  extend cache hit accounting to 100-999 and 1000 plus
09-JUL-2003  MGD  rework for non-file caching requirements,
                  support byte-range requests on cached *files*
16-JUN-2003  MGD  bugfix; FileSetCharset() moved from FILE.C module
24-MAY-2003  MGD  permanent cache entries,
                  path specified maximum file size
11-MAR-2002  MGD  bugfix; ensure only one request revalidates a cache entry at
                  a time (multiple could cause eventual channel exhaustion)
22-NOV-2001  MGD  ensure there are reasonable cache minima
29-SEP-2001  MGD  instance support
04-AUG-2001  MGD  use MD5 hash to identify cache entries,
                  modify hash table as fixed 4096 entrie,
                  support module WATCHing
27-MAY-2000  MGD  BUGFIX; CacheEntryNotValid() linked list :^{
                  bugfix; CacheLoadBegin() #else before memcpy()
09-APR-2000  MGD  simplified cache search
04-MAR-2000  MGD  use NetWriteFaol(), et.al.
28-DEC-1999  MGD  support ODS-2 and ODS-5 using ODS module,
                  add a number of other WATCH points
26-SEP-1999  MGD  minor changes in line with RequestExecute(),
                  CacheReport() now only optionally reports cached files,
                  scavenge failure should not result in a sanity check exit
20-JAN-1999  MGD  report format refinenment
19-SEP-1998  MGD  improve granularity of cache operation,
                  add check for existing entry to CacheLoadBegin()
14-MAY-1998  MGD  request-specified content-type ("httpd=content&type=")
18-MAR-1998  MGD  use file's VBN and first free byte to check size changes
                  (allows variable record files to be cached more efficiently)
24-FEB-1998  MGD  if CacheAcpInfo() reports a problem
                  then let the file module handle/report it,
                  add file size check to entry validation (allow for extend)
10-JAN-1998  MGD  added a (much overdue) hash collision list,
                  fixed problem with cache purge (it mostly didn't :^)
22-NOV-1997  MGD  sigh, bugfix; need to proactively free memory at capacity
05-OCT-1997  MGD  initial development for v4.5
*/
/*****************************************************************************/

#ifdef WASD_VMS_V6
#undef _VMS_V6_SOURCE
#define _VMS_V6_SOURCE
#undef __VMS_VER
#define __VMS_VER 60000000
#undef __CRTL_VER
#define __CRTL_VER 60000000
#endif

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

/* VMS related header files */
#include <atrdef.h>
#include <descrip.h>
#include <fibdef.h>
#include <iodef.h>
#include <libdef.h>
#include <ssdef.h>
#include <stsdef.h>

/* application header files */
#include "wasd.h"
#include "md5.h"

#define WASD_MODULE "CACHE"

/******************/
/* global storage */
/******************/

BOOL  CacheEnabled,
      CacheHashTableInitialised;

/* this is run-time storage */
int  CacheChunkInBytes,
     CacheCurrentlyLoading,
     CacheCurrentlyLoadingInUse,
     CacheEntriesMax,
     CacheEntryCount,
     CacheEntryKBytesMax,
     CacheFrequentHits,
     CacheFrequentSeconds,
     CacheGuardSeconds,
     CacheHashTableMask,
     CacheMemoryInUse,
     CachePermEntryCount,
     CachePermMemoryInUse,
     CacheTotalKBytesMax,
     CacheValidateSeconds;

/* these counters may be zeroed when accounting is zeroed */
int  CacheHashTableCollsnCount,
     CacheHashTableCount,
     CacheHashTableHitCount,
     CacheHashTableMissCount,
     CacheHashTableCollsnHitCount,
     CacheHashTableCollsnMissCount,
     CacheHitCount,
     CacheHits0,
     CacheHits10,
     CacheHits100,
     CacheHits1000,
     CacheHits1000plus,
     CacheListHitCount,
     CacheLoadCount,
     CacheNotHitCount,
     CacheNoHitsCount,
     CacheReclaimCount,
     CacheSearchCount;

LIST_HEAD  CacheList;

/* the hash table index is the first byte of the MD5 hash */
#define CACHE_HASH_TABLE_ENTRIES 4096 /* 12 bits, 16384 bytes, 32 page(lets) */
struct GenericCacheEntry  *CacheHashTable [CACHE_HASH_TABLE_ENTRIES];

/********************/
/* external storage */
/********************/

extern BOOL  CliCacheEnabled,
             CliCacheDisabled;

extern int  HttpdTickSecond,
            OdsExtended,
            OutputBufferSize;

extern short  HttpdNumTime[];

extern char  ErrorSanityCheck[],
             HttpProtocol[],
             ServerHostPort[],
             SoftwareID[],
             Utility[];

extern ACCOUNTING_STRUCT  *AccountingPtr;
extern CONFIG_STRUCT  Config;
extern MSG_STRUCT  Msgs;
extern HTTPD_PROCESS  HttpdProcess;
extern WATCH_STRUCT  Watch;

/*****************************************************************************/
/*
Initialize cache run-time parameters at startup (even though caching might be
initially disabled).  Also allocate the hash table when appropriate.
*/ 

CacheInit (BOOL Startup)

{
   int  HashValue;

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

   if (WATCH_MODULE(WATCH_MOD_CACHE))
      WatchThis (NULL, FI_LI, WATCH_MOD_CACHE, "CacheInit()");

   if (Startup)
   {
      if (!CliCacheDisabled && (CliCacheEnabled || Config.cfCache.Enabled))
         CacheEnabled = true;
      else
         CacheEnabled = false;

      CacheChunkInBytes = CACHE_CHUNK_SIZE;
      CacheEntriesMax = Config.cfCache.EntriesMax;
      if (CacheEntriesMax < 256) CacheEntriesMax = 256;
      CacheEntryKBytesMax = Config.cfCache.FileKBytesMax;
      if (CacheEntryKBytesMax <= 0) CacheEntryKBytesMax = 65;
      CacheFrequentHits = Config.cfCache.FrequentHits;
      CacheFrequentSeconds = Config.cfCache.FrequentSeconds;
      CacheGuardSeconds = Config.cfCache.GuardSeconds;
      if (!CacheGuardSeconds) CacheGuardSeconds = 15;
      CacheTotalKBytesMax = Config.cfCache.TotalKBytesMax;
      if (CacheTotalKBytesMax < 1024) CacheTotalKBytesMax = 1024;
      CacheValidateSeconds = Config.cfCache.ValidateSeconds;
      if (!CacheValidateSeconds) CacheValidateSeconds = 300;

      CacheEntryCount = CacheMemoryInUse =
         CachePermEntryCount = CachePermMemoryInUse = 0;

      CacheZeroCounters ();
   }

   if (CacheEntriesMax && CacheEnabled)
   {
      /* 100% overhead for ambit memory allocations, fragmentation, etc. */
      VmCacheInit (CacheTotalKBytesMax * 2);

      /* the permanent cache psace will be created on the first perm entry */
      /** VmPermCacheInit (CacheTotalKBytesMax); **/

      for (HashValue = 0; HashValue < CACHE_HASH_TABLE_ENTRIES; HashValue++)
         CacheHashTable[HashValue] = NULL;

      CacheHashTableInitialised = true;
   }
}

/*****************************************************************************/
/*
Just zero the cache-associated counters :^)
*/ 

CacheZeroCounters ()

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

   if (WATCH_MODULE(WATCH_MOD_CACHE))
      WatchThis (NULL, FI_LI, WATCH_MOD_CACHE, "CacheZeroCounter()");

   CacheHitCount = CacheHashTableHitCount =
      CacheHashTableCollsnCount = CacheHashTableCollsnHitCount =
      CacheHashTableCollsnMissCount = CacheHashTableCount =
      CacheHashTableMissCount =
      CacheNoHitsCount = CacheHits0 = CacheHits10 =
      CacheHits100 = CacheHits1000 =
      CacheHits1000plus = CacheLoadCount = 
      CacheReclaimCount = CacheSearchCount = 0;
}

/*****************************************************************************/
/*
Look through the cache list for a resource hash (MD5) that matches the one in
the file task structure.  If one is found then call CacheBegin() and return
success, otherwise return error status to indicate the search was unsuccessful. 
CacheBegin() (actually using CacheAcoInfoAst()) may still AST back to FileEnd()
if there is a problem returning the cached contents (i.e. contents have changed
on-disk), it is the resposibility of that routine to continue processing the
request.

NOTE: as with CacheLoadBegin(), the path passed to this function MUST be the
mapped path, not the request path. With conditional mapping identical request
paths may be mapped to completely different virtual paths.
*/ 

int CacheSearch (REQUEST_STRUCT *rqptr)

{
   int  status,
        EntryCount,
        HashValue;
   char  *cptr, *sptr;
   FILE_CENTRY  *captr;
   LIST_ENTRY  *leptr;
   MD5_HASH  *md5ptr;

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_CACHE))
      WatchThis (rqptr, FI_LI, WATCH_MOD_CACHE,
                 "CacheSearch() file:!&B",
                 rqptr->FileTaskPtr && rqptr->FileTaskPtr->TaskInitialized);

   if (!CacheEnabled) return (SS$_NOSUCHFILE);

   if (!CacheHashTableInitialised) CacheInit (false);

   /* must using this for a cache load (e.g. during directory listing) */
   if (rqptr->rqCache.EntryPtr) return (SS$_NOSUCHFILE); 

   /* if not interested in anything with a query string */
   if (!rqptr->rqPathSet.CacheQuery &&
       rqptr->rqHeader.QueryStringLength) return (SS$_NOSUCHFILE);

   /* if not a GET request */
   if (rqptr->rqHeader.Method != HTTP_METHOD_GET) return (SS$_NOSUCHFILE);

   /* if a SET mapping rule has specified the path should not be cached */
   if (rqptr->rqPathSet.NoCache) return (SS$_NOSUCHFILE);

   /* server has specifically set this request not to be cached */
   if (rqptr->rqCache.DoNotCache) return (SS$_NOSUCHFILE); 

   if (rqptr->FileTaskPtr && rqptr->FileTaskPtr->TaskInitialized)
      md5ptr = &rqptr->FileTaskPtr->Md5Hash;
   else
      md5ptr = &rqptr->Md5HashPath;

   if (WATCHING(rqptr) && WATCH_CATEGORY(WATCH_RESPONSE))
      WatchThis (rqptr, FI_LI, WATCH_RESPONSE,
                 "CACHE search !&?file\rpath\r !16&H",
                 rqptr->FileTaskPtr && rqptr->FileTaskPtr->TaskInitialized,
                 md5ptr);

   /* check the hash table */
   CacheHashTableCount++;
   /* 12 bit hash value, 0..4095 from a fixed 3 bytes of the MD5 hash */
   HashValue = md5ptr->HashLong[0] & 0xfff;
   if (!(captr = CacheHashTable[HashValue]))
   {
      /*************/
      /* not found */
      /*************/

      /* nope, no pointer against that hash value */
      CacheHashTableMissCount++;
      return (SS$_NOSUCHFILE);
   }

   EntryCount = 0;
   for ( /*set above*/ ; captr; captr = captr->HashCollisionNextPtr)
   {
      if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_CACHE))
         WatchThis (rqptr, FI_LI, WATCH_MOD_CACHE,
"file:!&B valid:!&B revalidating:!&B !AZ !&?no-match\rmatch\r",
                    captr->FromFile,
                    captr->EntryValid, captr->EntryRevalidating,
                    captr->FileOds.ExpFileName,
                    captr->Md5Hash.HashLong[0] != md5ptr->HashLong[0] ||
                    captr->Md5Hash.HashLong[1] != md5ptr->HashLong[1] ||
                    captr->Md5Hash.HashLong[2] != md5ptr->HashLong[2] ||
                    captr->Md5Hash.HashLong[3] != md5ptr->HashLong[3]);

      /* note the first collision entry */
      if (++EntryCount == 2) CacheHashTableCollsnCount++;

      /* match each of the 4 sets of 4 bytes (longwords) in the MD5 hash */
      if (captr->Md5Hash.HashLong[0] != md5ptr->HashLong[0] ||
          captr->Md5Hash.HashLong[1] != md5ptr->HashLong[1] ||
          captr->Md5Hash.HashLong[2] != md5ptr->HashLong[2] ||
          captr->Md5Hash.HashLong[3] != md5ptr->HashLong[3])
         continue; 

      /* if it's a file entry then there has to be an associated file task */
      if (captr->FromFile &&
          !(rqptr->FileTaskPtr && rqptr->FileTaskPtr->TaskInitialized))
         return (SS$_NOSUCHFILE);

      /********/
      /* hit! */
      /********/

      /* first entry is always the hash table, after that the collision list */
      if (EntryCount == 1)
         CacheHashTableHitCount++;
      else
         CacheHashTableCollsnHitCount++;

      if (!captr->EntryValid ||
          captr->DataLoading ||
          captr->EntryRevalidating)
      {
         /**************/
         /* not usable */
         /**************/

         rqptr->rqCache.NotUsable = true;
         return (SS$_NOSUCHFILE);
      }

      /**********/
      /* usable */
      /**********/

      rqptr->rqCache.EntryPtr = captr;
      status = CacheBegin (rqptr);
      if (VMSnok (status)) rqptr->rqCache.EntryPtr = NULL;
      return (status);
   }

   /***********/
   /* not hit */
   /***********/

   CacheHashTableCollsnMissCount++;
   return (SS$_NOSUCHFILE);
}

/*****************************************************************************/
/*
Use this cached entry as the contents for the client.  If the entry needs to
be validated then generated an asynchronous ACP QIO to get the required file
details, other wise call the AST processing function directly.

NOTE: as with CacheLoadBegin(), the path passed to this function MUST be the
mapped path, not the request path.  With conditional mapping identical request
paths may be mapped to completely different virtual paths.
*/ 
 
int CacheBegin (REQUEST_STRUCT *rqptr)

{
   BOOL  RevalidateEntry;
   FILE_CENTRY  *captr;

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_CACHE))
      WatchThis (rqptr, FI_LI, WATCH_MOD_CACHE, "CacheBegin() !AZ",
                 rqptr->rqCache.EntryPtr->FileOds.ExpFileName);

   captr = rqptr->rqCache.EntryPtr;

   if (captr->EntryPermanent)
      RevalidateEntry = false;
   else
   if (rqptr->PragmaNoCache &&
       captr->GuardTickSecond < HttpdTickSecond)
      RevalidateEntry = true;
   else
   if (captr->ExpiresAfterPeriod == CACHE_EXPIRES_DAY)
      if (captr->ExpiresAfterTime != HttpdNumTime[2])
         RevalidateEntry = true;
      else
         RevalidateEntry = false;
   else
   if (captr->ExpiresAfterPeriod == CACHE_EXPIRES_HOUR)
      if (captr->ExpiresAfterTime != HttpdNumTime[3])
         RevalidateEntry = true;
      else
         RevalidateEntry = false;
   else
   if (captr->ExpiresAfterPeriod == CACHE_EXPIRES_MINUTE)
      if (captr->ExpiresAfterTime != HttpdNumTime[4])
         RevalidateEntry = true;
      else
         RevalidateEntry = false;
   else
   if (captr->ExpiresTickSecond < HttpdTickSecond)
      RevalidateEntry = true;
   else
      RevalidateEntry = false;

   if (!RevalidateEntry)
   {
      /* status of non-zero used to determine whether the ACPQIO was used */
      captr->FileOds.FileQio.IOsb.Status = 0;
      CacheAcpInfoAst (rqptr);
      return (SS$_NORMAL);
   }

   if (captr->FromFile)
   {
      if (WATCHING(rqptr) && WATCH_CATEGORY(WATCH_RESPONSE))
         WatchThis (rqptr, FI_LI, WATCH_RESPONSE, "CACHE revalidate !AZ",
                    captr->FileOds.ExpFileName);

      if (!rqptr->FileTaskPtr || !rqptr->FileTaskPtr->TaskInitialized)
         ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);
      captr->EntryRevalidating = true;
      captr->InUseCount++;
      captr->ValidatedCount++;
      OdsFileAcpInfo (&captr->FileOds, &CacheAcpInfoAst, rqptr); 
      return (SS$_NORMAL);
   }

   /* non-file entries that are invalid are purged and re-cached */
   if (WATCHING(rqptr) && WATCH_CATEGORY(WATCH_RESPONSE))
      WatchThis (rqptr, FI_LI, WATCH_RESPONSE, "CACHE stale !AZ",
                 captr->FileOds.ExpFileName);

   CacheRemoveEntry (captr, false);
   /* move it to the end of the cache list ready for reuse */
   ListMoveTail (&CacheList, captr);

   return (SS$_NOSUCHFILE);
}

/*****************************************************************************/
/*
Called either explicitly or as an AST from an ACP QIO in CacheBegin(). Check if
modified, if not then just reply with a 302 header.  If contents should be
transfered to the client then begin. If there is a problem at all then declare
the AST to continue processing the request and let it worry about it, otherwise
begin providing the file data from the cache entry.
*/ 
 
CacheAcpInfoAst (REQUEST_STRUCT *rqptr)

{
   BOOL  RangeValid;
   int  idx, status,
        ContentLength;
   unsigned short  Length;
   char  *cptr, *sptr, *zptr;
   RANGE_BYTE  *rbptr;
   REQUEST_AST AstFunction;
   FILE_CENTRY  *captr, *tcaptr;
   FILE_CONTENT  *fcptr;
   FILE_QIO  *fqptr;
   LIST_ENTRY  *leptr;

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_CACHE))
      WatchThis (rqptr, FI_LI, WATCH_MOD_CACHE,
                 "CacheAcpInfoAst() !&F !&S !%D !%D", &CacheAcpInfoAst,
                 rqptr->rqCache.EntryPtr->FileOds.FileQio.IOsb.Status,
                 &rqptr->rqCache.EntryPtr->RdtBinTime,
                 &rqptr->rqCache.EntryPtr->FileOds.FileQio.RdtBinTime);

   captr = rqptr->rqCache.EntryPtr;
   fqptr = &captr->FileOds.FileQio;

   /* proactively remove the association between the entry and the request */
   rqptr->rqCache.EntryPtr = NULL;

   /* status of non-zero used to determine whether the ACPQIO was used */
   if (fqptr->IOsb.Status)
   {
      /*********************/
      /* revalidating file */
      /*********************/

      /* finished getting the revalidate data */
      captr->EntryRevalidating = false;
      captr->InUseCount--;

      /* first deassign the channel allocated by OdsFileAcpInfo() */
      sys$dassgn (fqptr->AcpChannel);

      if (VMSnok (fqptr->IOsb.Status))
      {
         /***********************/
         /* file access problem */
         /***********************/

         /* entry no longer valid */
         CacheRemoveEntry (captr, false);
         /* move it to the end of the cache list ready for reuse */
         ListMoveTail (&CacheList, captr);

         FileEnd (rqptr);
         return;
      }

      fqptr->EndOfFileVbn = ((fqptr->RecAttr.fat$l_efblk & 0xffff) << 16) |
                            ((fqptr->RecAttr.fat$l_efblk & 0xffff0000) >> 16);
      fqptr->FirstFreeByte = fqptr->RecAttr.fat$w_ffbyte;

      if (captr->RdtBinTime[0] != fqptr->RdtBinTime[0] ||
          captr->RdtBinTime[1] != fqptr->RdtBinTime[1] ||
          captr->EndOfFileVbn != fqptr->EndOfFileVbn ||
          captr->FirstFreeByte != fqptr->FirstFreeByte)
      {
         /************************/
         /* data no longer valid */
         /************************/

         if (WATCHING(rqptr) && WATCH_CATEGORY(WATCH_RESPONSE))
            WatchThis (rqptr, FI_LI, WATCH_RESPONSE,
                       "CACHE stale !AZ !%D (!%D / !%D) (!UL,!UL / !UL,!UL)",
                       captr->FileOds.ExpFileName, &captr->CdtBinTime,
                       &captr->RdtBinTime, &fqptr->RdtBinTime,
                       captr->EndOfFileVbn, captr->FirstFreeByte,
                       fqptr->EndOfFileVbn, fqptr->FirstFreeByte);

         /* entry no longer valid */
         CacheRemoveEntry (captr, false);
         /* move it to the end of the cache list ready for reuse */
         ListMoveTail (&CacheList, captr);

         FileCacheStale (rqptr);
         return;
      }

      /********************/
      /* data still valid */ 
      /********************/

      /* note the time the cached data was last validated */
      memcpy (&captr->ValidateBinTime, &rqptr->rqTime.Vms64bit, 8);

      if (captr->ExpiresAfterPeriod == CACHE_EXPIRES_DAY)
         captr->ExpiresAfterTime = HttpdNumTime[2];
      else
      if (captr->ExpiresAfterPeriod == CACHE_EXPIRES_HOUR)
         captr->ExpiresAfterTime = HttpdNumTime[3];
      else
      if (captr->ExpiresAfterPeriod == CACHE_EXPIRES_MINUTE)
         captr->ExpiresAfterTime = HttpdNumTime[4];
      else
      if (captr->ExpiresAfterPeriod)
         captr->ExpiresTickSecond = HttpdTickSecond + captr->ExpiresAfterPeriod;
      else
         captr->ExpiresTickSecond = HttpdTickSecond + CacheValidateSeconds;

      if (captr->GuardSeconds)
         captr->GuardTickSecond = HttpdTickSecond + captr->GuardSeconds;
      else
         captr->GuardTickSecond = HttpdTickSecond + CacheGuardSeconds;
   }

   if (WATCHING(rqptr) && WATCH_CATEGORY(WATCH_RESPONSE))
      WatchThis (rqptr, FI_LI, WATCH_RESPONSE,
                 "CACHE hit !&?permanent\rvolatile\r !AZ",
                 captr->EntryPermanent, captr->FileOds.ExpFileName);

   /**************************************/
   /* request to be fulfilled from cache */
   /**************************************/

   /* cancel any no-such-file callback, we've obviously got it! */
   if (rqptr->FileTaskPtr && rqptr->FileTaskPtr->TaskInitialized)
      rqptr->FileTaskPtr->NoSuchFileFunction = NULL;

   memcpy (&captr->HitBinTime, &rqptr->rqTime.Vms64bit, 8);
   captr->FrequentTickSecond = HttpdTickSecond + CacheFrequentSeconds;

   CacheHitCount++;
   if (!captr->HitCount) CacheHits0--;
   captr->HitCount++;
   if (captr->HitCount < 10)
      CacheHits10++;
   else
   if (captr->HitCount < 100)
      CacheHits100++;
   else
   if (captr->HitCount < 1000)
      CacheHits1000++;
   else
      CacheHits1000plus++;

   rqptr->AccountingDone =
      InstanceGblSecIncrLong (&AccountingPtr->CacheHitCount);

   /* if this entry has more hits move it to the head of the cache list */
   leptr = CacheList.HeadPtr;
   tcaptr = (FILE_CENTRY*)leptr;
   if (captr->HitCount > tcaptr->HitCount)
      ListMoveHead (&CacheList, captr);
   else
   {
      /* if this entry has more hits than the one "above" it swap them */
      leptr = captr;
      if (leptr->PrevPtr)
      {
         leptr = leptr->PrevPtr;
         tcaptr = (FILE_CENTRY*)leptr;
         if (captr->HitCount > tcaptr->HitCount)
         {
            ListRemove (&CacheList, captr);
            ListAddBefore (&CacheList, leptr, captr);
         }
      }
   }

   if (captr->ContentHandlerFunction)
   {
      /***********************************/
      /* this file has a content handler */
      /***********************************/

      if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_CACHE))
         WatchThis (rqptr, FI_LI, WATCH_MOD_CACHE, "!&X",
                    captr->ContentHandlerFunction);

      /* allow one extra character for a terminating null */
      rqptr->FileContentPtr = fcptr = (FILE_CONTENT*)
         VmGetHeap (rqptr, sizeof(FILE_CONTENT) + captr->ContentLength+1);
      fcptr->ContentSize = captr->ContentLength+1;

      /* buffer space immediately follows the structured storage */
      fcptr->ContentPtr = (char*)fcptr + sizeof(FILE_CONTENT);
      memcpy (fcptr->ContentPtr,
              captr->ContentPtr,
              fcptr->ContentLength = captr->ContentLength);
      /* we always terminate these buffers with a null - it's usually text! */
      fcptr->ContentPtr[fcptr->ContentLength] = '\0';

      /* populate the file contents structure with some file data */
      zptr = (sptr = fcptr->FileName) + sizeof(fcptr->FileName);
      for (cptr = captr->FileOds.ExpFileName;
           *cptr && sptr < zptr;
           *sptr++ = *cptr++);
      if (sptr >= zptr)
      {
         ErrorGeneralOverflow (rqptr, FI_LI);
         FileEnd (rqptr);
         return;
      }
      *sptr = '\0';
      fcptr->FileNameLength = sptr - fcptr->FileName;

      memcpy (&fcptr->CdtBinTime, &captr->CdtBinTime, 8);
      memcpy (&fcptr->RdtBinTime, &captr->RdtBinTime, 8);

      fcptr->UicGroup = captr->UicGroup;
      fcptr->UicMember = captr->UicMember;
      fcptr->Protection = captr->Protection;

      /* set the content structure handler so it gets control at FileEnd() */
      rqptr->FileContentPtr->ContentHandlerFunction =
         captr->ContentHandlerFunction;

      FileEnd (rqptr);
      return;
   }

   if (!rqptr->rqResponse.HeaderPtr)
   {
      /**************************/
      /* full response required */
      /**************************/

      if (captr->FromFile)
      {
         /********/
         /* file */
         /********/

         if (rqptr->rqHeader.RangeBytePtr &&
             rqptr->rqHeader.RangeBytePtr->Total)
         {
            /**************/
            /* byte-range */
            /**************/

            RangeValid = true;
            rbptr = rqptr->rqHeader.RangeBytePtr;
            for (idx = 0; idx < rbptr->Total; idx++)
            {
               if (!rbptr->Last[idx])
               {
                  /* last byte not specified, set at EOF */
                  rbptr->Last[idx] = captr->ContentLength - 1;
               }
               else
               if (rbptr->Last[idx] < 0)
               {
                  /* first byte a negative offset from end, last byte at EOF */
                  rbptr->First[idx] = captr->ContentLength + rbptr->Last[idx];
                  rbptr->Last[idx] = captr->ContentLength - 1;
               }
               else
               if (rbptr->Last[idx] >= captr->ContentLength)
               {
                  /* if the last byte is ambit make it at the EOF */
                  rbptr->Last[idx] = captr->ContentLength - 1;
               }
               /* if the range still does not make sense then back out now */
               if (rbptr->First[idx] > rbptr->Last[idx])
               {
                  RangeValid = false;
                  rbptr->Length = 0;
               }
               else
                  rbptr->Length = rbptr->Last[idx] - rbptr->First[idx] + 1;

               if (WATCHING(rqptr) && WATCH_CATEGORY(WATCH_RESPONSE))
                  WatchThis (rqptr, FI_LI, WATCH_RESPONSE,
                             "RANGE !UL !UL-!UL !UL byte!%s!&? INVALID\r\r",
                             idx+1, rbptr->First[idx], rbptr->Last[idx],
                             rbptr->Length, !rbptr->Length);
            }
            if (RangeValid) rbptr->Count = idx;
         }

         if (rqptr->rqPathSet.CharsetPtr)
         {
            cptr = captr->FileOds.ResFileName;
            if (!*cptr) cptr = captr->FileOds.ExpFileName;
            rqptr->rqPathSet.CharsetPtr = FileSetCharset (rqptr, cptr);
         }

         status = FileResponseHeader (rqptr, captr->ContentType,
                                      captr->ContentLength, &captr->RdtBinTime,
                                      rqptr->rqHeader.RangeBytePtr);
         if (VMSnok (status))
         {
            if (status == LIB$_NEGTIM)
            {
               InstanceGblSecIncrLong (&AccountingPtr->
                                       CacheHitNotModifiedCount);
               captr->HitNotModifiedCount++;
            }
            FileEnd (rqptr);
            return;
         }
      }
      else
      {
         /************/
         /* non-file */
         /************/

         if (captr->ContentType[0])
         {
            /* if any retained CGI header fields add these to the header */
            if (captr->CgiHeaderLength)
            {
               cptr = captr->ContentPtr;
               ContentLength = captr->ContentLength -
                               captr->CgiHeaderLength - 1;
            }
            else
            {
               cptr = NULL;
               ContentLength = captr->ContentLength;
            }
            ResponseHeader (rqptr, 200, captr->ContentType,
                            ContentLength, &captr->RdtBinTime, cptr);
         }
         else
         {
            /* when no associated content-type it's an NPH script */
            rqptr->rqResponse.HttpStatus = 200;
         }
      }

      /* quit here if the HTTP method was HEAD */
      if (rqptr->rqHeader.Method == HTTP_METHOD_HEAD)
      {
         FileEnd (rqptr);
         return;
      }
   }

   /************/
   /* transfer */
   /************/

   /* 'CacheInUse' keeps track of whether the entry is in use or not */
   captr->InUseCount++;

   /* initialize this for start-of-transfer detection in CacheNext() */
   rqptr->rqCache.ContentPtr = NULL;

   /* reassociated the cache entry with this request */
   rqptr->rqCache.EntryPtr = captr;

   /* network writes are checked for success, fudge the first one! */
   rqptr->rqNet.WriteIOsb.Status = SS$_NORMAL;

   /* begin the transfer */
   if (rqptr->rqOutput.BufferCount)
   {
      /* after ensuring the current contents are output */
      NetWriteFullFlush (rqptr, &CacheNext);
   }
   else
      CacheNext (rqptr);
}

/*****************************************************************************/
/*
Write the next (or first) block of data from the cache buffer to the client.
*/ 

CacheNext (REQUEST_STRUCT *rqptr)

{
   int  status,
        DataLength;
   unsigned short  Length;
   unsigned char  *DataPtr;
   FILE_CENTRY  *captr;
   RANGE_BYTE  *rbptr;
   REQUEST_AST AstFunction;

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_CACHE))
      WatchThis (rqptr, FI_LI, WATCH_MOD_CACHE, "CacheNext() !&F !&S",
                 &CacheNext, rqptr->rqNet.WriteIOsb.Status);

   if (VMSnok (rqptr->rqNet.WriteIOsb.Status))
   {
      /* network write has failed (as AST), bail out now */
      CacheEnd (rqptr);
      return;
   }

   captr = rqptr->rqCache.EntryPtr;

   if (!rqptr->rqCache.ContentPtr)
   {
      /* first read, initialize content pointer and length */
      if (rqptr->rqHeader.RangeBytePtr &&
          rqptr->rqHeader.RangeBytePtr->Count)
      {
         /* returning a byte range within the file (partial content) */
         rbptr = rqptr->rqHeader.RangeBytePtr;
         rbptr->Length = rbptr->Last[rbptr->Index] -
                         rbptr->First[rbptr->Index] + 1;
         if (rbptr->Count > 1)
         {
            /* returning 'multipart/byteranges' range content */
            char Buffer [256];
            WriteFao (Buffer, sizeof(Buffer), &Length,
"!AZ--!AZ\r\n\
Content-Type: !AZ\r\n\
Range: bytes !UL-!UL/!UL\r\n\
\r\n",
                      rbptr->Index ? "\r\n" : "",
                      rqptr->rqResponse.MultipartBoundaryPtr,
                      captr->ContentType,
                      rbptr->First[rbptr->Index],
                      rbptr->Last[rbptr->Index],
                      captr->ContentLength);
            /* synchronous network write (just for the convenience of it!) */
            NetWrite (rqptr, NULL, Buffer, Length);
         }
         rqptr->rqCache.ContentPtr = captr->ContentPtr +
                                     rbptr->First[rbptr->Index];
         rqptr->rqCache.ContentLength = rbptr->Length;
      }
      else
      {
         rqptr->rqCache.ContentPtr = captr->ContentPtr;
         rqptr->rqCache.ContentLength = captr->ContentLength;
      }
      if (captr->CgiHeaderLength)
      {
         /* adjust body of response for any retained CGI header fields */
         rqptr->rqCache.ContentPtr += captr->CgiHeaderLength + 1;
         rqptr->rqCache.ContentLength -= captr->CgiHeaderLength + 1;
      }
   }

   if (rqptr->rqCache.ContentLength > OutputBufferSize)
   {
      DataPtr = rqptr->rqCache.ContentPtr;
      DataLength = OutputBufferSize;
      rqptr->rqCache.ContentPtr += DataLength;
      rqptr->rqCache.ContentLength -= DataLength;

      if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_CACHE))
      {
         WatchThis (rqptr, FI_LI, WATCH_MOD_CACHE, "CACHE !UL/!UL",
                    DataLength, rqptr->rqCache.ContentLength);
         WatchDataDump (DataPtr, DataLength);
      }

      NetWrite (rqptr, &CacheNext, DataPtr, DataLength);
      return;
   }
   else
   {
      DataPtr = rqptr->rqCache.ContentPtr;
      DataLength = rqptr->rqCache.ContentLength;
      rqptr->rqCache.ContentLength = 0;

      if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_CACHE))
      {
         WatchThis (rqptr, FI_LI, WATCH_MOD_CACHE, "CACHE !UL/!UL",
                    DataLength, rqptr->rqCache.ContentLength);
         WatchDataDump (DataPtr, DataLength);
      }

      NetWrite (rqptr, &CacheEnd, DataPtr, DataLength);
      return;
   }
}

/*****************************************************************************/
/*
End of transfer to client using cached contents.
*/

CacheEnd (REQUEST_STRUCT *rqptr)

{
   char  *cptr, *sptr, *zptr;
   FILE_CENTRY  *captr;

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_CACHE))
      WatchThis (rqptr, FI_LI, WATCH_MOD_CACHE, "CacheEnd() !&F", &CacheEnd);

   captr = rqptr->rqCache.EntryPtr;

   if (rqptr->rqHeader.RangeBytePtr &&
       rqptr->rqHeader.RangeBytePtr->Count)
   {
      /* transfering byte-range(s) */
      rqptr->rqHeader.RangeBytePtr->Index++;
      if (rqptr->rqHeader.RangeBytePtr->Index <
          rqptr->rqHeader.RangeBytePtr->Count)
      {
         /* multiple byte ranges, restart with next range */
         rqptr->rqCache.ContentPtr = NULL;
         rqptr->rqCache.ContentLength = 0;
         SysDclAst (&CacheNext, rqptr);
         return;
      }
      if (rqptr->rqHeader.RangeBytePtr->Count > 1)
      {
         /* end of multiple byte ranges, provide final boundary */
         char Buffer [64];
         zptr = (sptr = Buffer) + sizeof(Buffer)-1;
         for (cptr = "\r\n--"; *cptr && sptr < zptr; *sptr++ = *cptr++);
         for (cptr = rqptr->rqResponse.MultipartBoundaryPtr;
              *cptr && sptr < zptr;
              *sptr++ = *cptr++);
         for (cptr = "--\r\n"; *cptr && sptr < zptr; *sptr++ = *cptr++);
         *sptr = '\0';
         /* synchronous network write (for the convenience of it!) */
         NetWrite (rqptr, NULL, Buffer, sptr-Buffer);
      }
   }

   /* this cache entry is no longer associated with this request */
   rqptr->rqCache.EntryPtr = NULL;

   /* this cache entry is no longer in use (if now zero) */
   captr->InUseCount--;

   if (captr->Purge && !captr->InUseCount)
   {
      if (captr->PurgeCompletely)
         CacheRemoveEntry (captr, true);
      else
      {
         CacheRemoveEntry (captr, false);
         /* move it to the end of the cache list ready for reuse */
         ListMoveTail (&CacheList, captr);
      }
   }

   if (captr->FromFile)
      FileEnd (rqptr);
   else
      RequestEnd (rqptr);
} 

/*****************************************************************************/
/*
Check that we're interested in caching this particular data and that the
requested size is allowed to be cached.  Find/create a cache structure ready to
contain the data buffered.  This also blocks other concurrent loads of the same
resource.  It is entirely possible, given that the maximum number of cache
entries has been reached and all are currently in use (either for data transfer
or being loaded) - though this if fairly unlikely, that there will be no cache
entry available for this request to use and the load will fail.  If available
allocate an appropriate chunk of either volatile or permanent cache memory and
use that to load the to-be-cached data.
*/ 

BOOL CacheLoadBegin
(
REQUEST_STRUCT *rqptr,
int SizeInBytes,
char *ContentTypePtr
)
{
   int  MaxKBytes;

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_CACHE))
      WatchThis (rqptr, FI_LI, WATCH_MOD_CACHE,
                 "CacheLoadBegin() !SL bytes !&Z !&Z http:!UL in-use:!&B",
                 SizeInBytes, ContentTypePtr, rqptr->rqHeader.QueryStringPtr,
                 rqptr->rqResponse.HttpStatus, rqptr->rqCache.ContentPtr);

   rqptr->rqCache.LoadCheck = true;

   /* could be disabled between search and load beginning */
   if (!CacheEnabled) return (false);

   /* only interested in HTTP success status */
   if (rqptr->rqResponse.HttpStatus &&
       rqptr->rqResponse.HttpStatus != 200) return (false);

   /* if this cache structure is already in use (e.g. directory listing) */
   if (rqptr->rqCache.EntryPtr) return (false);

   /* if not interested in query strings */
   if (!rqptr->rqPathSet.CacheQuery &&
       rqptr->rqHeader.QueryStringLength) return (false);

   /* if a SET mapping rule has specified the path should not be cached */
   if (rqptr->rqPathSet.NoCache) return (false);

   /* if not a GET request */
   if (rqptr->rqHeader.Method != HTTP_METHOD_GET) return (false);

   /* server has specifically set this request not to be cached */
   if (rqptr->rqCache.DoNotCache) return (false); 

   MaxKBytes = 0;
   if (rqptr->rqCgi.ScriptControlCacheMaxKBytes)
   {
      /* start off with any script specified value */
      MaxKBytes = rqptr->rqCgi.ScriptControlCacheMaxKBytes;
      if (rqptr->rqPathSet.CacheMaxKBytes)
      {
         /* any path setting should limit anything script supplied */
         if (MaxKBytes > rqptr->rqPathSet.CacheMaxKBytes)
            MaxKBytes = rqptr->rqPathSet.CacheMaxKBytes;
      }
      else
      {
         /* configuration setting should limit anything script supplied */
         if (MaxKBytes > CacheEntryKBytesMax) MaxKBytes = CacheEntryKBytesMax;
      }
   }
   else 
   if (rqptr->rqPathSet.CacheMaxKBytes)
   {
      /* path specified maximum overrides configuration maximum */
      MaxKBytes = rqptr->rqPathSet.CacheMaxKBytes;
   }
   /* fall back to using the configuration setting */
   if (!MaxKBytes) MaxKBytes = CacheEntryKBytesMax;

   /* zero indicates exact size is unknown so start with an ambit maximum */
   if (SizeInBytes <= 0) SizeInBytes = MaxKBytes << 10;

   /* if it's larger than the maximum allowed */
   if ((SizeInBytes >> 10) > MaxKBytes)
   {
      if (WATCHING(rqptr) && WATCH_CATEGORY(WATCH_RESPONSE))
         WatchThis (rqptr, FI_LI, WATCH_RESPONSE,
                    "CACHE load fail, too large (!ULkB>!ULkB)",
                    SizeInBytes >> 10, MaxKBytes);
      return (false);
   }

   /* if we can't get one then just forget it! */
   if (!CacheAllocateEntry (rqptr)) return (false);

   /* calculate the cache chunk */
   if (SizeInBytes % CacheChunkInBytes) SizeInBytes += CacheChunkInBytes;
   SizeInBytes = SizeInBytes / CacheChunkInBytes;
   SizeInBytes *= CacheChunkInBytes;

   if (rqptr->rqPathSet.CachePermanent)
      rqptr->rqCache.ContentPtr = VmGetPermCache (SizeInBytes);
   else
      rqptr->rqCache.ContentPtr = VmGetCache (SizeInBytes);

   rqptr->rqCache.CurrentPtr = rqptr->rqCache.ContentPtr;
   rqptr->rqCache.ContentRemaining = SizeInBytes;
   CacheCurrentlyLoading++;
   CacheCurrentlyLoadingInUse += SizeInBytes;
   rqptr->rqCache.ContentBufferSize = SizeInBytes;
   rqptr->rqCache.ContentLength = rqptr->rqCache.RecordBlockLength = 0;
   rqptr->rqCache.ContentTypePtr = ContentTypePtr;
   rqptr->rqCache.Loading = true;
   rqptr->rqCache.LoadStatus = 0;

   return (true);
}

/*****************************************************************************/
/*
Check the success of the load.  This is indicated using an end-of-file status. 
If the load failed or finding a cache entry failed just discard the loaded data
and return the memory to the cache pool.
*/ 

CacheLoadEnd (REQUEST_STRUCT *rqptr)

{
   int  status,
        ReclaimEntryBytes,
        ReclaimEntryCount,
        SizeInBytes;
   char  *cptr, *sptr, *zptr;
   FILE_CENTRY  *captr, *lcaptr;
   FILE_TASK  *ftkptr;
   LIST_ENTRY  *leptr;
   MD5_HASH  *md5ptr;

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_CACHE))
      WatchThis (rqptr, FI_LI, WATCH_MOD_CACHE,
                 "CacheLoadEnd() file:!&B !UL !&S %!&M",
                 rqptr->FileTaskPtr && rqptr->FileTaskPtr->TaskInitialized,
                 rqptr->rqResponse.HttpStatus, rqptr->rqCache.LoadStatus,
                 rqptr->rqCache.LoadStatus);

   /* better check, caching could be enabled between ASTs since search */
   if (!CacheHashTableInitialised) CacheInit (false);

   if (!rqptr->rqCache.Loading)
      ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);

   captr = rqptr->rqCache.EntryPtr;
   /* entry is no longer associated with this request */
   rqptr->rqCache.EntryPtr = NULL;

   captr->InUseCount--;

   if (captr->FromFile = rqptr->rqCache.LoadFromFile) cptr = "FILE";
   if (captr->FromNet = rqptr->rqCache.LoadFromNet) cptr = "NET";
   if (captr->FromScript = rqptr->rqCache.LoadFromCgi) cptr = "CGI";

   rqptr->rqCache.Loading =
      rqptr->rqCache.LoadFromCgi =
      rqptr->rqCache.LoadFromFile =
      rqptr->rqCache.LoadFromNet = false;

   CacheCurrentlyLoading--;
   CacheCurrentlyLoadingInUse -= rqptr->rqCache.ContentBufferSize;

   /* not interested in anything but successful responses */
   if ((rqptr->rqResponse.HttpStatus &&
        rqptr->rqResponse.HttpStatus != 200) ||
       /* an early HTTP success status but an error during processing */
       rqptr->rqResponse.ErrorReportPtr)
      rqptr->rqCache.LoadStatus = SS$_CANCEL;

   /* end-of-file is used to indicate successful cache data load */
   if (rqptr->rqCache.LoadStatus != RMS$_EOF &&
       rqptr->rqCache.LoadStatus != SS$_ENDOFFILE)
   {
      /*********************/
      /* load unsuccessful */
      /*********************/

      if (WATCHING(rqptr) && WATCH_CATEGORY(WATCH_RESPONSE))
         WatchThis (rqptr, FI_LI, WATCH_RESPONSE,
                    "CACHE load from !AZ fail, data %!&M",
                    cptr, rqptr->rqCache.LoadStatus);

      CacheRemoveEntry (captr, false);
      /* move it to the end of the cache list ready for reuse */
      ListMoveTail (&CacheList, captr);

      return;
   }

   if (WATCHING(rqptr) && WATCH_CATEGORY(WATCH_RESPONSE))
      WatchThis (rqptr, FI_LI, WATCH_RESPONSE,
                 "CACHE load from !AZ complete, !UL bytes", 
                 cptr, rqptr->rqCache.ContentLength);

   /**********************/
   /* check memory usage */
   /**********************/

   /* calculate the required cache chunk */
   SizeInBytes = rqptr->rqCache.ContentLength;
   if (SizeInBytes % CacheChunkInBytes) SizeInBytes += CacheChunkInBytes;
   SizeInBytes = SizeInBytes / CacheChunkInBytes;
   SizeInBytes *= CacheChunkInBytes;
   if (!SizeInBytes) SizeInBytes = CacheChunkInBytes;

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_CACHE))
      WatchThis (rqptr, FI_LI, WATCH_MOD_CACHE, "!UL < !UL !&B",
                 SizeInBytes, rqptr->rqCache.ContentBufferSize,
                 SizeInBytes < rqptr->rqCache.ContentBufferSize);

   if (SizeInBytes < rqptr->rqCache.ContentBufferSize)
   {
      /* actually required cache chunk is smaller than original ambit chunk */
      cptr = rqptr->rqCache.ContentPtr;
      if (rqptr->rqPathSet.CachePermanent)
      {
         sptr = VmGetPermCache (SizeInBytes);
         memcpy (sptr, cptr, rqptr->rqCache.ContentLength);
         VmFreePermCache (cptr, FI_LI);
      }
      else
      {
         sptr = VmGetCache (SizeInBytes);
         memcpy (sptr, cptr, rqptr->rqCache.ContentLength);
         VmFreeCache (cptr, FI_LI);
      }
      rqptr->rqCache.ContentPtr = sptr;
      rqptr->rqCache.ContentBufferSize = SizeInBytes;
   }

   /************************/
   /* populate cache entry */
   /************************/

   InstanceGblSecIncrLong (&AccountingPtr->CacheLoadCount);
   CacheLoadCount++;
   CacheHits0++;

   /* move entry to the head of the cache list */
   ListMoveHead (&CacheList, captr);

   if (rqptr->rqPathSet.CachePermanent)
   {
      /* permanent cache entry */
      captr->EntryPermanent = true;
      CachePermEntryCount++;
      CacheEntryCount--;
      CachePermMemoryInUse += rqptr->rqCache.ContentBufferSize;
   }
   else
      CacheMemoryInUse += rqptr->rqCache.ContentBufferSize;

   captr->ContentPtr = rqptr->rqCache.ContentPtr;
   captr->EntrySize = rqptr->rqCache.ContentBufferSize;
   captr->ContentLength = rqptr->rqCache.ContentLength;
   rqptr->rqCache.ContentPtr = NULL;

   captr->HitCount = 0;
   captr->DataLoading = captr->EntryReclaimed = false;
   captr->EntryValid = true;

   /* quadword time file was loaded/validated, created, last modified */
   memcpy (&captr->LoadBinTime, &rqptr->rqTime.Vms64bit, 8);
   memcpy (&captr->ValidateBinTime, &rqptr->rqTime.Vms64bit, 8);

   /* some file details (if applicable) */
   ftkptr = rqptr->FileTaskPtr;
   if (ftkptr && ftkptr->TaskInitialized)
   {
      captr->EndOfFileVbn = ftkptr->FileOds.FileQio.EndOfFileVbn;
      captr->FirstFreeByte = ftkptr->FileOds.FileQio.FirstFreeByte;
      captr->UicGroup = (ftkptr->FileOds.FileQio.AtrUic & 0x0fff0000) >> 16;
      captr->UicMember = (ftkptr->FileOds.FileQio.AtrUic & 0x0000ffff);
      captr->Protection = ftkptr->FileOds.FileQio.AtrFpro;
      memcpy (&captr->CdtBinTime, &ftkptr->FileOds.FileQio.CdtBinTime, 8);
      memcpy (&captr->RdtBinTime, &ftkptr->FileOds.FileQio.RdtBinTime, 8);
      /* copy the entire on-disk structure from file to cache */
      OdsCopyStructure (&captr->FileOds, &ftkptr->FileOds);
      /* the cache entry will reuse the original content handler (if any) */
      captr->ContentHandlerFunction = ftkptr->ContentHandlerFunction;
      /* drop through to buffer the content-type */
      cptr = ftkptr->ContentTypePtr;
   }
   else
   {
      captr->FirstFreeByte = captr->EndOfFileVbn =
         captr->UicGroup = captr->UicMember = captr->Protection = 0;
      memset (&captr->FileOds, 0, sizeof(captr->FileOds));
      memcpy (&captr->CdtBinTime, &rqptr->rqTime.Vms64bit, 8);
      memcpy (&captr->RdtBinTime, &rqptr->rqTime.Vms64bit, 8);
      captr->ContentHandlerFunction = NULL;
      /* add the path purely for cache report purposes */
      zptr = (sptr = captr->FileOds.ExpFileName) +
             sizeof(captr->FileOds.ExpFileName)-1;
      for (cptr = rqptr->ServicePtr->ServerHostPort;
           *cptr && sptr < zptr;
           *sptr++ = *cptr++);
      for (cptr = rqptr->rqHeader.RequestUriPtr;
           *cptr && *cptr != '?' && sptr < zptr;
           *sptr++ = *cptr++);
      if (rqptr->rqPathSet.CacheQuery)
      {
         /* those cached regardless include any request query string */
         while (*cptr && sptr < zptr) *sptr++ = *cptr++;
      }
      *sptr = '\0';
      /* drop through to buffer the content-type */
      cptr = rqptr->rqCache.ContentTypePtr;
   }

   /* note if the cached content contains any CGI header fields */
   if (captr->FromScript && rqptr->rqCgi.HeaderLength)
      captr->CgiHeaderLength = rqptr->rqCgi.HeaderLength;

   /* buffer the content type up until any ";charset=", etc. */
   if (!cptr) cptr = "";
   zptr = (sptr = captr->ContentType) + sizeof(captr->ContentType)-1;
   while (*cptr && *cptr != ';' && !ISLWS(*cptr) && sptr < zptr)
      *sptr++ = *cptr++;
   *sptr = '\0';

   /********************/
   /* set entry expiry */
   /********************/

   captr->ExpiresAfterPeriod = rqptr->rqPathSet.CacheExpiresAfter;
   if (captr->ExpiresAfterPeriod == CACHE_EXPIRES_DAY)
      captr->ExpiresAfterTime = HttpdNumTime[2];
   else
   if (captr->ExpiresAfterPeriod == CACHE_EXPIRES_HOUR)
      captr->ExpiresAfterTime = HttpdNumTime[3];
   else
   if (captr->ExpiresAfterPeriod == CACHE_EXPIRES_MINUTE)
      captr->ExpiresAfterTime = HttpdNumTime[4];
   else
   if (captr->ExpiresAfterPeriod)
      captr->ExpiresTickSecond = HttpdTickSecond + captr->ExpiresAfterPeriod;
   else
      captr->ExpiresTickSecond = HttpdTickSecond + CacheValidateSeconds;

   captr->GuardSeconds = rqptr->rqPathSet.CacheGuardSeconds;
   if (captr->GuardSeconds)
      captr->GuardTickSecond = HttpdTickSecond + captr->GuardSeconds;
   else
      captr->GuardTickSecond = HttpdTickSecond + CacheGuardSeconds;

   /* if we're using too much cache memory */
   if ((CacheMemoryInUse >> 10) > CacheTotalKBytesMax)
   {
      /******************/
      /* reclaim memory */
      /******************/

      CacheReclaimCount++;
      ReclaimEntryCount = ReclaimEntryBytes = 0;

      /* mark the current one just so *it* won't be reclaimed */
      captr->InUseCount++;

      /* process the cache entry list from least to most recent */
      for (leptr = CacheList.TailPtr; leptr; leptr = leptr->PrevPtr)
      {
         /* remember, use a separate pointer or we get very confused :^) */
         lcaptr = (FILE_CENTRY*)leptr;

         if (WATCHING(rqptr) &&
             WATCH_MODULE(WATCH_MOD_CACHE) &&
             WATCH_MODULE(WATCH_MOD__DETAIL))
            WatchThis (NULL, FI_LI, WATCH_MOD_CACHE,
                       "!&Z !UL !UL !&B !&B !UL",
                       captr->FileOds.ExpFileName,
                       captr->EntrySize, captr->ContentLength,
                       captr->EntryValid, captr->EntryRevalidating,
                       captr->InUseCount);

         /* if it's permanent or in use in some way then just continue */
         if (lcaptr->EntryPermanent || lcaptr->InUseCount) continue;

         ReclaimEntryCount++;
         ReclaimEntryBytes += lcaptr->EntrySize;

         /* entry no longer valid */
         CacheRemoveEntry (lcaptr, false);
         lcaptr->EntryReclaimed = true;
 
         /* if we've reclaimed enough memory */
         if ((CacheMemoryInUse >> 10) <= CacheTotalKBytesMax) break;
      }

      /* remove the reclaim prophylactic */
      captr->InUseCount--;

      if (WATCHING(rqptr) && WATCH_CATEGORY(WATCH_RESPONSE))
         WatchThis (rqptr, FI_LI, WATCH_RESPONSE,
                    "CACHE load, reclaim !UL entries, !UL kBytes",
                    ReclaimEntryCount, ReclaimEntryBytes >> 10);
   }
}

/*****************************************************************************/
/*
Copy the referenced data into the cache pre-allocated buffer (if there's still
space available).
*/ 

int CacheLoadData
(
REQUEST_STRUCT *rqptr,
char *DataPtr,
int DataLength
)
{
   /*********/
   /* begin */
   /*********/

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_CACHE))
      WatchThis (rqptr, FI_LI, WATCH_MOD_CACHE,
"CacheLoadData() file:!&B cgi:!&B net:!&B !UL+!UL=!UL !UL-!UL=!UL",
                 rqptr->rqCache.LoadFromFile,
                 rqptr->rqCache.LoadFromCgi,
                 rqptr->rqCache.LoadFromNet,
                 rqptr->rqCache.ContentLength, DataLength,
                 rqptr->rqCache.ContentLength + DataLength,
                 rqptr->rqCache.ContentBufferSize,
                 rqptr->rqCache.ContentLength + DataLength,
                 rqptr->rqCache.ContentBufferSize -
                    rqptr->rqCache.ContentLength - DataLength);

   if (DataLength <= rqptr->rqCache.ContentRemaining)
   {
      memcpy (rqptr->rqCache.CurrentPtr, DataPtr, DataLength);
      rqptr->rqCache.CurrentPtr += DataLength;
      rqptr->rqCache.ContentLength += DataLength;
      rqptr->rqCache.ContentRemaining -= DataLength;
      return (SS$_NORMAL);
   }

   return (rqptr->rqCache.LoadStatus = SS$_BUFFEROVF_ERROR);
}

/*****************************************************************************/
/*
Allocate a cache entry ready for data loading.  Returns true if entry
available, false if not.  Sets 'rqCache.CacheEntry' to point to the allocated
entry.
*/ 

BOOL CacheAllocateEntry (REQUEST_STRUCT *rqptr)

{
   int  HashValue;
   FILE_CENTRY  *captr, *lcaptr;
   LIST_ENTRY  *leptr;
   MD5_HASH  *md5ptr;

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_CACHE))
      WatchThis (rqptr, FI_LI, WATCH_MOD_CACHE, "CacheAllocateEntry() !16&H",
                 rqptr->FileTaskPtr && rqptr->FileTaskPtr->TaskInitialized ?
                    &rqptr->FileTaskPtr->Md5Hash : &rqptr->Md5HashPath);

   if (rqptr->FileTaskPtr && rqptr->FileTaskPtr->TaskInitialized)
      md5ptr = &rqptr->FileTaskPtr->Md5Hash;
   else
      md5ptr = &rqptr->Md5HashPath;

   /**********************************/
   /* check it doesn't already exist */
   /**********************************/

   /* 12 bit hash value, 0..4095 from a fixed 3 bytes of the MD5 hash */
   HashValue = md5ptr->HashLong[0] & 0xfff;
   for (captr = CacheHashTable[HashValue];
        captr;
        captr = captr->HashCollisionNextPtr)
   {
      if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_CACHE))
         WatchThis (rqptr, FI_LI, WATCH_MOD_CACHE,
"file:!&B valid:!&B revalidating:!&B !AZ !&?no-match\rmatch\r",
                    captr->FromFile, captr->EntryValid,
                    captr->EntryRevalidating, captr->FileOds.ExpFileName,
                    captr->Md5Hash.HashLong[0] != md5ptr->HashLong[0] ||
                    captr->Md5Hash.HashLong[1] != md5ptr->HashLong[1] ||
                    captr->Md5Hash.HashLong[2] != md5ptr->HashLong[2] ||
                    captr->Md5Hash.HashLong[3] != md5ptr->HashLong[3]);

      /* match each of the 4 sets of 4 bytes (longwords) in the MD5 hash */
      if (captr->Md5Hash.HashLong[0] != md5ptr->HashLong[0] ||
          captr->Md5Hash.HashLong[1] != md5ptr->HashLong[1] ||
          captr->Md5Hash.HashLong[2] != md5ptr->HashLong[2] ||
          captr->Md5Hash.HashLong[3] != md5ptr->HashLong[3])
         continue; 

      if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_CACHE))
         WatchThis (rqptr, FI_LI, WATCH_MOD_CACHE, "entry exists");

      return (false);
   }

   /**********************/
   /* find a cache entry */
   /**********************/

   captr = (FILE_CENTRY*)CacheList.TailPtr;
   if (captr && !captr->EntryValid && !captr->InUseCount)
   {
      /****************************/
      /* reuse invalid tail entry */
      /****************************/

      if (WATCHING(rqptr) && WATCH_CATEGORY(WATCH_RESPONSE))
         WatchThis (rqptr, FI_LI, WATCH_RESPONSE, "CACHE load, reuse entry");
   }
   else
   if (CacheEntryCount + CachePermEntryCount >= CacheEntriesMax)
   {
      /***********************/
      /* reuse a cache entry */
      /***********************/

      /* process the cache entry list from least to most recent */
      for (leptr = CacheList.TailPtr; leptr; leptr = leptr->PrevPtr)
      {
         captr = (FILE_CENTRY*)leptr;

         if (WATCHING(rqptr) &&
             WATCH_MODULE(WATCH_MOD_CACHE) && WATCH_MODULE(WATCH_MOD__DETAIL))
            WatchThis (NULL, FI_LI, WATCH_MOD_CACHE,
                       "!&Z !UL !UL !&B !&B !UL",
                       captr->FileOds.ExpFileName,
                       captr->EntrySize, captr->ContentLength,
                       captr->EntryValid, captr->EntryRevalidating,
                       captr->InUseCount);

         /* if it's permanent or in use in some way then just continue */
         if (captr->EntryPermanent || captr->InUseCount) continue;

         /* if it can be considered frequently hit */
         if (captr->EntryValid &&
             CacheFrequentHits &&
             captr->HitCount > CacheFrequentHits &&
             captr->FrequentTickSecond > HttpdTickSecond) continue;

         /* entry no longer valid */
         CacheRemoveEntry (captr, false);

         if (WATCHING(rqptr) && WATCH_CATEGORY(WATCH_RESPONSE))
            WatchThis (rqptr, FI_LI, WATCH_RESPONSE,
                       "CACHE load, reuse entry");
         break;
      }

      /* if we got to the end of the list */
      if (!leptr)
      {
         /**********************/
         /* all entries in use */
         /**********************/

         if (WATCHING(rqptr) && WATCH_CATEGORY(WATCH_RESPONSE))
            WatchThis (rqptr, FI_LI, WATCH_RESPONSE,
                       "CACHE load, all entries in use");

         return (false);
      }
   }
   else
   {
      /*******************/
      /* new cache entry */
      /*******************/

      captr = VmGet (sizeof(FILE_CENTRY));
      CacheEntryCount++;

      /* add it to the list */
      ListAddTail (&CacheList, captr);

      if (WATCHING(rqptr) && WATCH_CATEGORY(WATCH_RESPONSE))
         WatchThis (rqptr, FI_LI, WATCH_RESPONSE, "CACHE load, new entry");
   }

   /**************/
   /* init entry */
   /**************/

   captr->EntryReclaimed =
      captr->EntryRevalidating =
      captr->EntryValid =
      captr->FromScript =
      captr->FromFile =
      captr->FromNet =
      captr->Purge =
      captr->PurgeCompletely = false;
   captr->ContentPtr = NULL;
   captr->CgiHeaderLength = captr->EntrySize = 0;
   if (rqptr->FileTaskPtr && rqptr->FileTaskPtr->TaskInitialized)
      captr->FromFile = true;
   else
      captr->FromFile = false;
   captr->DataLoading = true;
   captr->InUseCount++;

   /*************************/
   /* add to the hash table */
   /*************************/

   memcpy (&captr->Md5Hash, md5ptr, sizeof(MD5_HASH));
   /* 12 bit hash value, 0..4095 from a fixed 3 bytes of the MD5 hash */
   HashValue = md5ptr->HashLong[0] & 0xfff;
   if (!CacheHashTable[HashValue])
   {
      /* set hash table index */
      CacheHashTable[HashValue] = captr;
      captr->HashCollisionPrevPtr = captr->HashCollisionNextPtr = NULL;
   }
   else
   {
      /* add to head of hash-collision list */
      lcaptr = CacheHashTable[HashValue];
      lcaptr->HashCollisionPrevPtr = captr;
      captr->HashCollisionPrevPtr = NULL;
      captr->HashCollisionNextPtr = lcaptr;
      CacheHashTable[HashValue] = captr;
   }

   rqptr->rqCache.EntryPtr = captr;
   return (true);
}

/*****************************************************************************/
/*
Purge a cache entry, either just the data, or completely from the cache list.
*/

CacheRemoveEntry
(
FILE_CENTRY *captr,
BOOL Completely
)
{
   int  HashValue;

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

   if (WATCH_MODULE(WATCH_MOD_CACHE))
      WatchThis (NULL, FI_LI, WATCH_MOD_CACHE,
                 "CacheRemoveEntry() !&B", Completely);

   if (captr->InUseCount)
   {
      if (Completely)
         captr->Purge = captr->PurgeCompletely = true;
      else
         captr->Purge = true;
      return;
   }

   /* note the number of entries loaded but never subsequently hit */
   if (captr->EntryValid && !captr->HitCount) CacheNoHitsCount++;

   if (captr->EntryPermanent)
   {
      /* permanent entry reverts to volatile when purged */
      captr->EntryPermanent = false;
      if (captr->ContentPtr)
      {
         VmFreePermCache (captr->ContentPtr, FI_LI);
         CachePermMemoryInUse -= captr->EntrySize;
         captr->ContentPtr = NULL;
         captr->EntrySize = 0;
      }
      CacheEntryCount++;
      CachePermEntryCount--;
   }
   else
   if (captr->ContentPtr)
   {
      VmFreeCache (captr->ContentPtr, FI_LI);
      CacheMemoryInUse -= captr->EntrySize;
      captr->ContentPtr = NULL;
      captr->EntrySize = 0;
   }

   captr->DataLoading =
      captr->EntryValid =
      captr->Purge =
      captr->PurgeCompletely = false;

   /* 12 bit hash value, 0..4095 from a fixed 3 bytes of the MD5 hash */
   HashValue = captr->Md5Hash.HashLong[0] & 0xfff;
   if ((FILE_CENTRY*)(CacheHashTable[HashValue]) == captr)
   {
      /* must be at the head of any collision list */
      CacheHashTable[HashValue] = captr->HashCollisionNextPtr;
      if (captr->HashCollisionNextPtr)
         captr->HashCollisionNextPtr->HashCollisionPrevPtr =
            captr->HashCollisionPrevPtr;
   }
   else
   {
      /* if somewhere along the collision list */
      if (captr->HashCollisionPrevPtr)
         captr->HashCollisionPrevPtr->HashCollisionNextPtr =
            captr->HashCollisionNextPtr;
      /*
         *** SITE OF ONE OF MY MOST STUPID AND COSTLY PROGRAMMING ERRORS ***
         If this isn't an argument against coding for speed and efficiency
         instead of with tested and common code routines I don't know what is!
      */
      if (captr->HashCollisionNextPtr)
         captr->HashCollisionNextPtr->HashCollisionPrevPtr =
            captr->HashCollisionPrevPtr;
   }
   captr->HashCollisionPrevPtr = captr->HashCollisionNextPtr = NULL;

   if (Completely)
   {
      ListRemove (&CacheList, captr);
      VmFree (captr, FI_LI);
   }
} 

/*****************************************************************************/
/*
Scan through the cache list.  If a cache entry is currently not in use then
free the data memory associated with it.  If purge completely then also remove
the entry from the list and free it's memory.  If the entry is currently in use
then mark it for purge and if necessary for complete removal.
*/

CachePurge
(
BOOL Completely,
int *PurgeCountPtr,
int *MarkedForPurgeCountPtr
)
{
   int  PurgeCount,
        MarkedForPurgeCount;
   FILE_CENTRY  *captr;
   LIST_ENTRY  *leptr;

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

   if (WATCH_MODULE(WATCH_MOD_CACHE))
      WatchThis (NULL, FI_LI, WATCH_MOD_CACHE,
                 "CachePurge() !&B", Completely);

   CacheZeroCounters ();

   PurgeCount = MarkedForPurgeCount = 0;

   /* do it backwards! seeing they are pushed to the tail of the list */
   leptr = CacheList.TailPtr;
   while (leptr)
   {
      captr = (FILE_CENTRY*)leptr;

      /* now, before stuffing around with the entry get the next one */
      leptr = leptr->PrevPtr;

      if (WATCH_MODULE(WATCH_MOD_CACHE) && WATCH_MODULE(WATCH_MOD__DETAIL))
         WatchThis (NULL, FI_LI, WATCH_MOD_CACHE,
                    "!&Z !UL !UL !&B !&B !UL",
                    captr->FileOds.ExpFileName,
                    captr->EntrySize, captr->ContentLength,
                    captr->EntryValid, captr->EntryRevalidating,
                    captr->InUseCount);

      if (captr->InUseCount)
      {
         if (Completely)
            captr->Purge = captr->PurgeCompletely = true;
         else
            captr->Purge = true;
         MarkedForPurgeCount++;
      }
      else
      {
         if (captr->EntrySize)
         {
            CacheRemoveEntry (captr, Completely);
            PurgeCount++;
         }
      }
   }

   if (PurgeCountPtr) *PurgeCountPtr = PurgeCount;
   if (MarkedForPurgeCountPtr)
      *MarkedForPurgeCountPtr = MarkedForPurgeCount;
} 

/*****************************************************************************/
/*
Return a report on cache usage.  This function blocks while executing.
*/ 

CacheReport
(
REQUEST_STRUCT *rqptr,
REQUEST_AST NextTaskFunction,
BOOL IncludeEntries
)
{
   static char BeginPageFao [] =
"<P><TABLE CELLPADDING=3 CELLSPACING=0 BORDER=1>\n\
<TR><TH>Configuration</TH><TH>Activity</TH><TH>Entries</TH></TR>\n\
\
<TR><TD VALIGN=top>\n\
<TABLE CELLPADDING=1 CELLSPACING=5 BORDER=0>\n\
<TR><TH ALIGN=right>Caching:</TH>\
<TD ALIGN=left COLSPAN=2>!AZ</TD></TR>\n\
<TR><TH ALIGN=right>Memory&nbsp;&nbsp;/Permanent:</TH>\
<TD ALIGN=right>!UL</TD><TD><FONT SIZE=-1>kB</FONT></TD></TR>\n\
<TR><TH ALIGN=right>/Volatile:</TH>\
<TD ALIGN=right>!UL</TD><TD><FONT SIZE=-1>kB</FONT></TD></TR>\n\
<TR><TH ALIGN=right>/Max:</TH>\
<TD ALIGN=right>!UL</TD><TD><FONT SIZE=-1>kB</FONT></TD></TR>\n\
<TR><TH ALIGN=right>Entries&nbsp;&nbsp;/Permanent:</TH>\
<TD ALIGN=right>!UL</TD></TR>\n\
<TR><TH ALIGN=right>/Volatile:</TH>\
<TD ALIGN=right>!UL</TD></TR>\n\
<TR><TH ALIGN=right>/Max:</TH>\
<TD ALIGN=right>!UL</TD></TR>\n\
<TR><TH ALIGN=right>Max File Size:</TH>\
<TD ALIGN=right>!UL</TD><TD><FONT SIZE=-1>kB</FONT></TD></TR>\n\
<TR><TH ALIGN=right>Guard:</TH>\
<TD ALIGN=right>!UL</TD><TD><FONT SIZE=-1>seconds</FONT></TD></TR>\n\
<TR><TH ALIGN=right>Validate:</TH>\
<TD ALIGN=right>!UL</TD><TD><FONT SIZE=-1>seconds</FONT></TD></TR>\n\
<TR><TH ALIGN=right>Frequent&nbsp;&nbsp;/Hits:</TH>\
<TD ALIGN=right>!UL</TD><TD><FONT SIZE=-1>hits</FONT></TD></TR>\n\
<TR><TH ALIGN=right>/Within:</TH>\
<TD ALIGN=right>!UL</TD><TD><FONT SIZE=-1>seconds</FONT></TD></TR>\n\
</TABLE>\n\
\
</TD><TD VALIGN=top>\n\
<TABLE CELLPADDING=0 CELLSPACING=5 BORDER=0>\n\
<TR><TH ALIGN=right>Search:</TH><TD>!UL</TD></TR>\n\
<TR><TH ALIGN=right><U>Hash Table</U></TH></TR>\n\
<TR><TH ALIGN=right>Hit:</TH><TD>!UL</TD><TD ALIGN=right>!UL%</TD></TR>\n\
<TR><TH ALIGN=right>Miss:</TH><TD>!UL</TD><TD ALIGN=right>!UL%</TD></TR>\n\
<TR><TH ALIGN=right><U>Collision</U></TH></TR>\n\
<TR><TH ALIGN=right>Total:</TH><TD>!UL</TD><TD ALIGN=right>!UL%</TD></TR>\n\
<TR><TH ALIGN=right>Hit:</TH><TD>!UL</TD><TD ALIGN=right>!UL%</TD></TR>\n\
<TR><TH ALIGN=right>Miss:</TH><TD>!UL</TD><TD ALIGN=right>!UL%</TD></TR>\n\
<TR><TH ALIGN=right><U>Memory</U></TH></TR>\n\
<TR><TH ALIGN=right>Loading:</TH>\
<TD>!UL</TD><TD>!UL&nbsp;<FONT SIZE=-1>MB</FONT></TD>\
<TR><TH ALIGN=right>Reclaim:</TH><TD>!UL</TD><TD ALIGN=right>!UL%</TD></TR>\n\
<TR HEIGHT=5></TR>\n\
</TABLE>\n\
\
</TD><TD VALIGN=top>\n\
<TABLE CELLPADDING=0 CELLSPACING=5 BORDER=0>\n\
<TR><TH ALIGN=right>Load:</TH><TD>!UL</TD></TR>\n\
<TR><TH ALIGN=right>Not Hit:</TH><TD>!UL</TD><TD ALIGN=right>!UL%</TD></TR>\n\
<TR><TH ALIGN=right>Total Hit:</TH><TD>!UL</TD><TD ALIGN=right>!UL%</TD></TR>\n\
<TR><TH ALIGN=right>1-9:</TH><TD>!UL</TD><TD ALIGN=right>!UL%</TD></TR>\n\
<TR><TH ALIGN=right>10-99:</TH><TD>!UL</TD><TD ALIGN=right>!UL%</TD></TR>\n\
<TR><TH ALIGN=right>100-999:</TH><TD>!UL</TD><TD ALIGN=right>!UL%</TD></TR>\n\
<TR><TH ALIGN=right>&gt;1000:</TH><TD>!UL</TD><TD ALIGN=right>!UL%</TD></TR>\n\
</TABLE>\n\
\
</TD></TR>\n\
</TABLE>\n\
!&@";

   /* the final column just adds a little white-space on the page far right */
   static char  EntriesFao [] =
"<P><TABLE CELLPADDING=1 CELLSPACING=0 BORDER=0>\n\
<TR>\
<TH></TH>\
<TH ALIGN=left><U>Flags</U>&nbsp;&nbsp;</TH>\
<TH ALIGN=right><U>Size</U>&nbsp;&nbsp;</TH>\
<TH ALIGN=right><U>Length</U>&nbsp;&nbsp;</TH>\
<TH ALIGN=right><U><NOBR>In-Use</NOBR></U>&nbsp;&nbsp;</TH>\
<TH ALIGN=right><U>Hash</U>&nbsp;&nbsp;</TH>\
<TH ALIGN=left><U>Revised</U>&nbsp;&nbsp;</TH>\
<TH ALIGN=left><U>Validated</U>&nbsp;&nbsp;</TH>\
<TH ALIGN=left><U>Loaded</U>&nbsp;&nbsp;</TH>\
<TH COLSPAN=2 ALIGN=left><U>Hit&nbsp;/&nbsp;304</U></TH>\
<TH></TH>\
<TH>&nbsp;&nbsp;</TH>\
</TR>\n\
<TR HEIGHT=5></TR>\n";

   /* the empty 99% column just forces the rest left with long request URIs */
   static char  CacheFao [] =
"<TR>\
<TD ALIGN=right><B><A HREF=\"?entry=!16&H\">!#ZL</A></B>&nbsp;&nbsp;</TD>\
<TD ALIGN=left>\
!&?P\rV\r\
!&?F\r\r!&?N\r\r!&?S\r\r\
!&?C\r<STRIKE>C</STRIKE>\r\
!&?T\r<STRIKE>T</STRIKE>\r\
&nbsp;&nbsp;</TD>\
<TD ALIGN=right>!UL&nbsp;&nbsp;</TD>\
<TD ALIGN=right>!UL&nbsp;&nbsp;</TD>\
<TD ALIGN=right>!UL&nbsp;&nbsp;</TD>\
<TD ALIGN=right>!UL&nbsp;/&nbsp;!UL&nbsp;&nbsp;</TD>\
<TD ALIGN=left><NOBR>!&@&nbsp;&nbsp;</NOBR></TD>\
<TD ALIGN=left><NOBR>!&@&nbsp;&nbsp;</NOBR></TD>\
<TD ALIGN=left><NOBR>!20%D&nbsp;&nbsp;</NOBR></TD>\
<TD ALIGN=left><NOBR>!20%D&nbsp;&nbsp;</NOBR></TD>\
<TD ALIGN=right>!UL&nbsp;/&nbsp;!UL</TD>\
!AZ\
</TR>\n\
<TR><TD></TD>\
<TD COLSPAN=11 ALIGN=left BGCOLOR=\"#eeeeee\"><NOBR><TT>!AZ</TT></NOBR></TD>\
</TR>\n";

   static char EmptyCacheFao [] =
"<TR><TH><B>000</B>&nbsp;&nbsp;</TH>\
<TD COLSPAN=10 BGCOLOR=\"#eeeeee\"><I>empty</I></TD><TR>\n";

   static char  EntriesButtonFao [] =
"</TABLE>\n\
<P><FONT SIZE=+1>[<A HREF=\"!AZ\">Entries</A>]</FONT>\n\
</BODY>\n\
</HTML>\n";

   static char  EndPageFao [] =
"</TABLE>\n\
<HR SIZE=1 NOSHADE WIDTH=60% ALIGN=left>\n\
</BODY>\n\
</HTML>\n";

   int  cnt,
        status,
        Count,
        HashCollisionListLength;
   unsigned long  FaoVector [64];
   unsigned long  *vecptr;
   char  *cptr, *sptr,
         *LastColPtr;
   FILE_CENTRY  *captr;
   LIST_ENTRY  *leptr;

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_CACHE))
      WatchThis (rqptr, FI_LI, WATCH_MOD_CACHE,
                 "CacheReport() !&A", NextTaskFunction);

   rqptr->rqResponse.PreExpired = PRE_EXPIRE_ADMIN;
   RESPONSE_HEADER_200_HTML (rqptr);
   AdminPageTitle (rqptr, "Cache Report");

   vecptr = FaoVector;

   if (CacheEnabled)
      *vecptr++ = "[enabled]";
   else
      *vecptr++ = "<FONT COLOR=\"#ff000\">[disabled]</FONT>";

   *vecptr++ = CachePermMemoryInUse >> 10;
   *vecptr++ = CacheMemoryInUse >> 10;
   *vecptr++ = CacheTotalKBytesMax;
   *vecptr++ = CachePermEntryCount;
   *vecptr++ = CacheEntryCount;
   *vecptr++ = CacheEntriesMax;
   *vecptr++ = CacheEntryKBytesMax;
   *vecptr++ = CacheGuardSeconds;
   *vecptr++ = CacheValidateSeconds;
   *vecptr++ = CacheFrequentHits;
   *vecptr++ = CacheFrequentSeconds;

   *vecptr++ = CacheHashTableCount;
   *vecptr++ = CacheHashTableHitCount;
   if (CacheHashTableCount)
      *vecptr++ = CacheHashTableHitCount * 100 / CacheHashTableCount;
   else
      *vecptr++ = 0;
   *vecptr++ = CacheHashTableMissCount;
   if (CacheHashTableCount)
      *vecptr++ = CacheHashTableMissCount * 100 / CacheHashTableCount;
   else
      *vecptr++ = 0;
   *vecptr++ = CacheHashTableCollsnCount;
   if (CacheHashTableCount)
      *vecptr++ = CacheHashTableCollsnCount * 100 / CacheHashTableCount;
   else
      *vecptr++ = 0;
   *vecptr++ = CacheHashTableCollsnHitCount;
   if (CacheHashTableCount)
      *vecptr++ = CacheHashTableCollsnHitCount * 100 / CacheHashTableCount;
   else
      *vecptr++ = 0;
   *vecptr++ = CacheHashTableCollsnMissCount;
   if (CacheHashTableCount)
      *vecptr++ = CacheHashTableCollsnMissCount * 100 / CacheHashTableCount;
   else
      *vecptr++ = 0;

   *vecptr++ = CacheCurrentlyLoading;
   *vecptr++ = CacheCurrentlyLoadingInUse >> 10;
   *vecptr++ = CacheReclaimCount;
   if (CacheLoadCount)
      *vecptr++ = CacheReclaimCount * 100 / CacheLoadCount;
   else
      *vecptr++ = 0;

   *vecptr++ = CacheLoadCount;
   *vecptr++ = CacheHits0;
   if (CacheLoadCount)
      *vecptr++ = CacheHits0 * 100 / CacheLoadCount;
   else
      *vecptr++ = 0;
   *vecptr++ = CacheHitCount;
   if (CacheLoadCount)
      *vecptr++ = CacheHitCount * 100 / CacheLoadCount;
   else
      *vecptr++ = 0;
   *vecptr++ = CacheHits10;
   if (CacheHitCount)
      *vecptr++ = CacheHits10 * 100 / CacheHitCount;
   else
      *vecptr++ = 0;
   *vecptr++ = CacheHits100;
   if (CacheHitCount)
      *vecptr++ = CacheHits100 * 100 / CacheHitCount;
   else
      *vecptr++ = 0;
   *vecptr++ = CacheHits1000;
   if (CacheHitCount)
      *vecptr++ = CacheHits1000 * 100 / CacheHitCount;
   else
      *vecptr++ = 0;
   *vecptr++ = CacheHits1000plus;
   if (CacheHitCount)
      *vecptr++ = CacheHits1000plus * 100 / CacheHitCount;
   else
      *vecptr++ = 0;

   if (CacheEnabled && !IncludeEntries)
   {
      *vecptr++ = EntriesButtonFao;
      *vecptr++ = ADMIN_REPORT_CACHE_ENTRIES;
   }
   else
      *vecptr++ = "";

   status = NetWriteFaol (rqptr, BeginPageFao, &FaoVector);
   if (VMSnok (status)) ErrorNoticed (status, "NetWriteFaol()", FI_LI);

   if (!CacheEnabled || !IncludeEntries)
   {
      SysDclAst (NextTaskFunction, rqptr);
      return;
   }

   /*****************/
   /* cache entries */
   /*****************/

   status = NetWriteFaol (rqptr, EntriesFao, NULL);
   if (VMSnok (status)) ErrorNoticed (status, "NetWriteFaol()", FI_LI);

   Count = 0;

   if (WATCHING(rqptr) &&
       WATCH_MODULE(WATCH_MOD_CACHE) && WATCH_MODULE(WATCH_MOD__DETAIL))
       WatchThis (NULL, FI_LI, WATCH_MOD_CACHE, "!&X !&X",
                  CacheList.HeadPtr, CacheList.TailPtr);

   /* process the cache entry list from most to least recently hit */
   for (leptr = CacheList.HeadPtr; leptr; leptr = leptr->NextPtr)
   {
      captr = (FILE_CENTRY*)leptr;

      if (WATCHING(rqptr) &&
          WATCH_MODULE(WATCH_MOD_CACHE) && WATCH_MODULE(WATCH_MOD__DETAIL))
          WatchThis (NULL, FI_LI, WATCH_MOD_CACHE, "!&X<-!&X->!&X !&X",
                     leptr->PrevPtr, leptr, leptr->NextPtr, captr);

      HashCollisionListLength = 0;
      /* count further down the list */
      while (captr->HashCollisionNextPtr)
      {
         captr = captr->HashCollisionNextPtr;
         HashCollisionListLength++;
      }
      captr = (FILE_CENTRY*)leptr;
      /* count further up the list */
      while (captr->HashCollisionPrevPtr)
      {
         captr = captr->HashCollisionPrevPtr;
         HashCollisionListLength++;
      }
      captr = (FILE_CENTRY*)leptr;

      vecptr = FaoVector;

      *vecptr++ = &captr->Md5Hash;
      if (CacheEntriesMax >= 10000)
         *vecptr++ = 5;
      else
      if (CacheEntriesMax >= 1000)
         *vecptr++ = 4;
      else
         *vecptr++ = 3;
      *vecptr++ = ++Count;
      *vecptr++ = captr->EntryPermanent;
      *vecptr++ = captr->FromFile;
      *vecptr++ = captr->FromNet;
      *vecptr++ = captr->FromScript;
      *vecptr++ = captr->CgiHeaderLength;
      *vecptr++ = captr->ContentType[0];
      *vecptr++ = captr->EntrySize;
      *vecptr++ = captr->ContentLength;
      *vecptr++ = captr->InUseCount;
      /* 12 bit hash value, 0..4095 from a fixed 3 bytes of the MD5 hash */
      *vecptr++ = captr->Md5Hash.HashLong[0] & 0xfff;
      *vecptr++ = HashCollisionListLength;

      if (captr->FromFile)
      {
         *vecptr++ = "!20%D";
         *vecptr++ = &captr->RdtBinTime;
      }
      else
         *vecptr++ = "n/a";

      if (captr->FromFile &&
          captr->EntryValid)
      {
         *vecptr++ = "!20%D&nbsp;!UL";
         *vecptr++ = &captr->ValidateBinTime;
         *vecptr++ = captr->ValidatedCount;
      }
      else
      {
         if (captr->EntryReclaimed)
            *vecptr++ = "RECLAIMED";
         else
         if (!captr->EntryValid)
            *vecptr++ = "INVALID";
         else
         if (!captr->FromFile)
            *vecptr++ = "n/a";
         else
            *vecptr++ = "?";
      }

      *vecptr++ = &captr->LoadBinTime;
      *vecptr++ = &captr->HitBinTime;
      *vecptr++ = captr->HitCount;
      *vecptr++ = captr->HitNotModifiedCount;
      if (rqptr->rqHeader.AdminNetscapeGold)
         *vecptr++ = "<TD></TD>";
      else
         *vecptr++ = "<TD WIDTH=99%></TD>";
      *vecptr++ = captr->FileOds.ExpFileName;

      status = NetWriteFaol (rqptr, CacheFao, &FaoVector);
      if (VMSnok (status)) ErrorNoticed (status, "NetWriteFaol()", FI_LI);
   }
   if (!CacheList.HeadPtr)
   {
      status = NetWriteFaol (rqptr, EmptyCacheFao, NULL);
      if (VMSnok (status)) ErrorNoticed (status, "NetWriteFaol()", FI_LI);
   }

   status = NetWriteFaol (rqptr, EndPageFao, NULL);
   if (VMSnok (status)) ErrorNoticed (status, "NetWriteFaol()", FI_LI);

   SysDclAst (NextTaskFunction, rqptr);
}

/*****************************************************************************/
/*
Return a report on a single cache entry.
*/ 

CacheReportEntry
(
REQUEST_STRUCT *rqptr,
REQUEST_AST NextTaskFunction,
char *Md5HashHexString
)
{
   static char BeginPageFao [] =
"<P><TABLE CELLPADDING=3 CELLSPACING=0 BORDER=1>\n\
<TR><TD VALIGN=top>\n\
<TABLE CELLPADDING=0 CELLSPACING=5 BORDER=0>\n\
<TR><TH ALIGN=right>Resource:</TH>\
<TD ALIGN=left><NOBR><TT>!&;AZ</TT></NOBR></TD></TR>\n\
<TR><TH ALIGN=right>Flags:</TH><TD ALIGN=left>\
!&?Permanent\rVolatile\r, \
!&?File\r\r!&?Network\r\r!&?Script\r\r, \
!&?CGI-fields\r<STRIKE>CGI-fields</STRIKE>\r, \
!&?Content-Type\r<STRIKE>Content-Type</STRIKE>\r</TD></TR>\n\
<TR><TH ALIGN=right>Size:</TH><TD ALIGN=left>!UL byte!%s</TD></TR>\n\
<TR><TH ALIGN=right>Length:</TH><TD ALIGN=left>!UL byte!%s</TD></TR>\n\
<TR><TH ALIGN=right>CGI&nbsp;Fields:</TH>\
<TD ALIGN=left>!UL byte!%s</TD></TR>\n\
<TR><TH ALIGN=right>Content-Type:</TH><TD ALIGN=left>!&;AZ</TD></TR>\n\
<TR><TH ALIGN=right><NOBR>In-Use:</NOBR></TH><TD ALIGN=left>!UL</TD></TR>\n\
<TR><TH ALIGN=right><NOBR>Hash/Idx/Colsn:</NOBR></TH>\
<TD ALIGN=left>!16&H&nbsp;/&nbsp;!UL&nbsp;/&nbsp;!UL</TD></TR>\n\
<TR><TH ALIGN=right>Revised:</TH><TD ALIGN=left>!&@</TD></TR>\n\
<TR><TH ALIGN=right>Validated:</TH><TD ALIGN=left>!&@</TD></TR>\n\
<TR><TH ALIGN=right>Loaded:</TH><TD ALIGN=left>!20%D</TD></TR>\n\
<TR><TH ALIGN=right>Hit:</TH>\
<TD ALIGN=left>!20%D, !UL time!%s, 304 response !UL time!%s</TD></TR>\n\
</TABLE>\n\
</TD></TR>\n\
</TABLE>\n\
<P><PRE>";

   static char  EndPageFao [] =
"</PRE>\n\
!AZ\
</BODY>\n\
</HTML>\n";

   int  idx, status,
        ItemCount,
        HashCollisionListLength;
   unsigned long  FaoVector [64];
   unsigned long  *vecptr;
   char  *cptr;
   MD5_HASH  Md5Hash;
   FILE_CENTRY  *captr;
   LIST_ENTRY  *leptr;

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_CACHE))
      WatchThis (rqptr, FI_LI, WATCH_MOD_CACHE,
                 "CacheReportEntry() !&A !&Z",
                 NextTaskFunction, Md5HashHexString);

   /* convert hex string back into binary hash */
   cptr = Md5HashHexString;
   for (idx = 0; idx < sizeof(Md5Hash); idx++)
   {
      if (*cptr >= '0' && *cptr <= '9')
         Md5Hash.HashChar[idx] = (*cptr - '0') << 4;
      else
      if (toupper(*cptr) >= 'A' && toupper(*cptr) <= 'F')
         Md5Hash.HashChar[idx] = (*cptr - '7') << 4;
      if (*cptr) cptr++;
      if (*cptr >= '0' && *cptr <= '9')
         Md5Hash.HashChar[idx] |= (*cptr - '0') & 0xf;
      else
      if (toupper(*cptr) >= 'A' && toupper(*cptr) <= 'F')
         Md5Hash.HashChar[idx] |= (*cptr - '7') & 0xf;
      if (*cptr) cptr++;
   }

   captr = NULL;
   ItemCount = 0;
   for (leptr = CacheList.HeadPtr; leptr; leptr = leptr->NextPtr)
   {
      captr = (FILE_CENTRY*)leptr;
      if (memcmp (&captr->Md5Hash, &Md5Hash, sizeof(Md5Hash))) continue;
      break;
   }
   if (!leptr)
   {
      rqptr->rqResponse.HttpStatus = 400;
      ErrorGeneral (rqptr, "Entry not found.", FI_LI);
      SysDclAst (NextTaskFunction, rqptr);
      return;
   }

   rqptr->rqResponse.PreExpired = PRE_EXPIRE_ADMIN;
   RESPONSE_HEADER_200_HTML (rqptr);
   AdminPageTitle (rqptr, "Cache Entry Report");

   HashCollisionListLength = 0;
   /* count further down the list */
   while (captr->HashCollisionNextPtr)
   {
         captr = captr->HashCollisionNextPtr;
         HashCollisionListLength++;
   }
   captr = (FILE_CENTRY*)leptr;
   /* count further up the list */
   while (captr->HashCollisionPrevPtr)
   {
      captr = captr->HashCollisionPrevPtr;
      HashCollisionListLength++;
   }
   captr = (FILE_CENTRY*)leptr;

   vecptr = FaoVector;

   *vecptr++ = captr->FileOds.ExpFileName;
   *vecptr++ = captr->EntryPermanent;
   *vecptr++ = captr->FromFile;
   *vecptr++ = captr->FromNet;
   *vecptr++ = captr->FromScript;
   *vecptr++ = captr->CgiHeaderLength;
   *vecptr++ = captr->ContentType[0];
   *vecptr++ = captr->EntrySize;
   *vecptr++ = captr->ContentLength;
   *vecptr++ = captr->CgiHeaderLength;
   if (captr->ContentType[0])
      *vecptr++ = captr->ContentType;
   else
      *vecptr++ = "n/a";
   *vecptr++ = captr->InUseCount;
   *vecptr++ = &captr->Md5Hash.HashLong;
   /* 12 bit hash value, 0..4095 from a fixed 3 bytes of the MD5 hash */
   *vecptr++ = captr->Md5Hash.HashLong[0] & 0xfff;
   *vecptr++ = HashCollisionListLength;

   if (captr->FromFile)
   {
      *vecptr++ = "!20%D";
      *vecptr++ = &captr->RdtBinTime;
   }
   else
      *vecptr++ = "n/a";

   if (captr->FromFile &&
       captr->EntryValid)
   {
      *vecptr++ = "!20%D, !UL time!%s";
      *vecptr++ = &captr->ValidateBinTime;
      *vecptr++ = captr->ValidatedCount;
   }
   else
   {
      if (captr->EntryReclaimed)
         *vecptr++ = "RECLAIMED";
      else
      if (!captr->EntryValid)
         *vecptr++ = "INVALID";
      else
      if (!captr->FromFile)
         *vecptr++ = "n/a";
      else
         *vecptr++ = "?";
   }

   *vecptr++ = &captr->LoadBinTime;
   *vecptr++ = &captr->HitBinTime;
   *vecptr++ = captr->HitCount;
   *vecptr++ = captr->HitNotModifiedCount;

   status = NetWriteFaol (rqptr, BeginPageFao, &FaoVector);
   if (VMSnok (status)) ErrorNoticed (status, "NetWriteFaol()", FI_LI);

   vecptr = FaoVector;
   if (captr->ContentPtr)
   {
      CacheDumpData (rqptr, captr->ContentPtr, captr->ContentLength);
      *vecptr++ = "<HR SIZE=1 NOSHADE WIDTH=60% ALIGN=left>\n";
   }
   else
      *vecptr++ = "";

   status = NetWriteFaol (rqptr, EndPageFao, &FaoVector);
   if (VMSnok (status)) ErrorNoticed (status, "NetWriteFaol()", FI_LI);

   SysDclAst (NextTaskFunction, rqptr);
}

/*****************************************************************************/
/*
Dump data a la WATCH.
*/ 

CacheDumpData
(
REQUEST_STRUCT *rqptr,
char *DataPtr,
int DataLength
)
{
/* 32 bytes by 128 lines comes out to 4096 bytes, the default buffer-full */
#define MAX_LINES 128
#define BYTES_PER_LINE 32
#define BYTES_PER_GROUP 4
#define GROUPS_PER_LINE (BYTES_PER_LINE / BYTES_PER_GROUP)
#define CHARS_PER_LINE ((BYTES_PER_LINE * 3) + GROUPS_PER_LINE + 1)
#define HTML_ESCAPE_SPACE 1024

   static char  HexDigits [] = "0123456789ABCDEF";
   static char  Woops [] = "ERROR: Buffer overflow!";

   int  ByteCount,
        CurrentDataCount,
        DataCount;
   char  *cptr, *sptr, *zptr,
         *CurrentDataPtr,
         *CurrentDumpPtr;
   char  DumpBuffer [(CHARS_PER_LINE * MAX_LINES)+HTML_ESCAPE_SPACE+1];

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_CACHE))
      WatchThis (rqptr, FI_LI, WATCH_MOD_CACHE,
                 "CacheDumpData() !&A !UL", DataPtr, DataLength);

   if (!DataPtr) return;

   zptr = (sptr = DumpBuffer) + sizeof(DumpBuffer)-1;
   cptr = DataPtr;
   DataCount = DataLength;

   while (DataCount)
   {
      CurrentDumpPtr = sptr;
      CurrentDataPtr = cptr;
      CurrentDataCount = DataCount;

      ByteCount = BYTES_PER_LINE;
      while (ByteCount && DataCount)
      {
         if (sptr < zptr) *sptr++ = HexDigits[*(unsigned char*)cptr >> 4];
         if (sptr < zptr) *sptr++ = HexDigits[*(unsigned char*)cptr & 0xf];
         cptr++;
         DataCount--;
         ByteCount--;
         if (!(ByteCount % BYTES_PER_GROUP) && sptr < zptr)  *sptr++ = ' ';
      }
      while (ByteCount)
      {
         if (sptr < zptr) *sptr++ = ' ';
         if (sptr < zptr) *sptr++ = ' ';
         ByteCount--;
         if (!(ByteCount % BYTES_PER_GROUP) && sptr < zptr) *sptr++ = ' ';
      }

      cptr = CurrentDataPtr;
      DataCount = CurrentDataCount;

      ByteCount = BYTES_PER_LINE;
      while (ByteCount && DataCount)
      {
         if (isalnum(*cptr) || ispunct(*cptr) || *cptr == ' ')
         {
            if (*cptr == '<')
            {
               if (sptr < zptr) *sptr++ = '&';
               if (sptr < zptr) *sptr++ = 'l';
               if (sptr < zptr) *sptr++ = 't';
               if (sptr < zptr) *sptr++ = ';';
               cptr++;
            }
            else
            if (*cptr == '&')
            {
               if (sptr < zptr) *sptr++ = '&';
               if (sptr < zptr) *sptr++ = 'a';
               if (sptr < zptr) *sptr++ = 'm';
               if (sptr < zptr) *sptr++ = 'p';
               if (sptr < zptr) *sptr++ = ';';
               cptr++;
            }
            else
            {
               if (sptr < zptr) *sptr++ = *cptr;
               cptr++;
            }
         }
         else
         {
            if (sptr < zptr) *sptr++ = '.';
            cptr++;
         }
         DataCount--;
         ByteCount--;
      }
      /* ensure there is a right margin using a couple of spaces */
      if (sptr < zptr) *sptr++ = ' ';
      if (sptr < zptr) *sptr++ = ' ';
      if (sptr < zptr) *sptr++ = '\n';
      if (sptr >= zptr)
      {
         NetWriteBuffered (rqptr, NULL, Woops, sizeof(Woops)-1);
         return;
      }

      if (!DataCount || !ByteCount)
      {
         *sptr = '\0';
         NetWriteBuffered (rqptr, NULL, DumpBuffer, sptr - DumpBuffer);
         zptr = (sptr = DumpBuffer) + sizeof(DumpBuffer)-1;
      }
   }
}

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

