/*****************************************************************************/
/*
                                 File.c

This module implements a full multi-threaded, AST-driven, asynchronous file 
send.  The AST-driven nature makes the code a little more difficult to follow, 
but creates a powerful, event-driven, multi-threaded server.  All of the 
necessary functions implementing this module are designed to be non-blocking. 

It can operate in one of four modes.

1) File direct to network.
   ~~~~~~~~~~~~~~~~~~~~~~
A smallish, standard output buffer is allocated if variable length record file,
successive records are read into the buffer until it fills, then written to the
network as a single block.  Records always have a newline character added to
each record (variable length record files are invariably text).  If a stream,
fixed or undefined record format the buffer is filled in a single virtual block
read and then immediately written to the network.

It uses the same buffer space as, and interworks with, NetWriteBuffered().  If 
there is already data (text) buffered in this area the file module will, for 
record-oriented, non-HTML-escaped transfers, continue to fill the area (using 
its own buffering function), flushing when and if necessary.  At end-of-file 
explicitly flush the buffer only if escaping HTML-forbidden characters, 
otherwise allow subsequent processing to do it as necessary.  For block-mode 
files the buffer is explicitly flushed before commencing the file transfer. 

2) File into file cache buffer, simultaneously to network.
   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A file cache buffer of sufficient size to contain the whole file is allocated. 
If reading in record mode this is filled with successive reads (as per above)
until a chunk the size of a standard output buffer is filled, at which time it
is written to the network as a block.  In block mode each group of blocks read
is output to the network.

3) File into contents buffer.
   ~~~~~~~~~~~~~~~~~~~~~~~~~
A file contents buffer of sufficient size to contain the whole file is
allocated.  When reading in either record or block mode the buffer is just
filled with the file reads - NO NETWORK OUTPUT.  When complete the request gets
control of the contents buffer for subsequent processing.

4) File into file cache as well as into contents buffer.
   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Both file cache and contents buffers of sufficient size are allocated.  Records
and blocks fill both buffers during the reads - NO NETWORK OUTPUT.  When
complete the file is cached and the contents buffer filled.  When complete the
request gets control of the contents buffer for subsequent processing. 
(Subsequent access to the file can then be serviced from the file cache.)


BYTE RANGES
-----------
The "Range:" request header is supported for non variable length record files
(i.e. those that can be considered to contain 'binary' content, e.g. stream-LF,
stream-CR, stream, fixed, undefined).  The cache module also supports
byte-ranges for cached file content (though without the restriction - all cache
content is 'binary' in nature).  The majority of files that byte ranges might
be applied to are probably large and having 'binary' content (e.g. restarting
ZIP archive transfers, accessing 'linearized' PDF documents, etc.)  The $READs
of virtual blocks used to access these files allow a relatively simple
algorithm to access these ranges and return 206 (partial content) responses. 
No effort is made to support this for variable record length files.  If a
byte-range is invalid or cannot be applied to the particular file type it is
just ignored and a 200 full transfer is performed instead.


MD5 DIGEST
----------
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.  See the
CACHE.C module for further detail.


FILE LANGUAGE VARIANTS
----------------------
If the path is SET to ACCEPT=LANG then this module attempts to find
language-specific variants of the file.  The format of the file names for these
variants is <file-name>.<file-type>_<language> where 'language' is one of the
ISO language abbreviations, e.g. "en" for English, "fr" for French, "de" for
German, "ru" for Russian, etc.  Hence if the basic file name is EXAMPLE.HTML
then a specifically English version would be EXAMPLE.HTML_EN, a French version
EXAMPLE.HTML_FR, etc.  Language variants may be provided for any file type the
WASD_CONFIG_GLOBAL directive [AddType] specifies as content-type "text/..".

The language variant code behaves as follows.  When FileBegin() is called with
a path SET to ACCEPT=LANG and a default language is specified (for those files
without the language variant abbreviation) this is checked against the
request's accepted languages to see if the default would be the request's first
choice.  If so then there is no need for further accept language processing. 
If not then a series of functions search using the basic specification for
files matching "EXAMPLE.HTML_*".  All files matching this wildcard have the '*'
portion (e.g. "EN", "FR", "DE", "RU") added to a list of matching variants. 
When the search is complete a final function compares each of the request's
"Accept-Language:" list to each of the searched-for variants.  If one matches
the contents of that file are returned.  If none are matched the original
EXAMPLE.HTML would be returned.

Example behaviour.  A directory contains

  EXAMPLE.HTML
  EXAMPLE.HTML_FR
  EXAMPLE.HTML_DE
  EXAMPLE.HTML_RU

and a request specifies

  Accept-Language: en,fr,de

then the EXAMPLE.HTML_FR file will be returned.  For a request specifying

  Accept-Language: ru

then the EXAMPLE.HTML_RU file is returned, and if

  Accept-Language: en

EXAMPLE.HTML returned (without having searched for any other variants).

One or other file is always returned, with the default, non-language file
always the fallback source of data.  If it does not exist and no other
language variant is selected the request returns a 404 file-not-found error.


IGNORED CONTENT-TYPE
--------------------
Some browsers and/or some operating systems and/or some versions of both insist
on ignoring the response header specified content-type and instead seem to
second-guess (wrongly) on the file name extension.  A common example is the
content of DCL procedures on Windows and up-until-fairly-recent versions of
Internet Explorer.  I note that versions 9 and 10 seem more compliant with the
the returned header.  Notwithstanding, if a '$' and then a second extension is
added to the URI this is often sufficient to coerce the browser into accepting
(and display) the content with the type associated with that extension.  In the
case of the DCL procedure /wasd_root/src/build_all.com making the URI into
/wasd_root/src/build_all.com$.txt, etc.  Any suitable additional file extension
that will be accepted can be used.


GENERAL COMMENTS
----------------
Uses the ACP-QIO interface detailed in the "OpenVMS I/O User's Reference
Manual" to retrieve file record attributes, revision date/time and size.

Use QIOs to access and transfer disk blocks (just a bit more efficient and
flexible than RMS).  RMS structures are used to parse the file name and obtain
the DID and FID uses to QIO access the file.  Some record types are considered
binary content and can be served without 'massaging' the data.  These are
STREAM (CR,LF), FIXED-512 (or where the record size falls on a block boundary),
and UNDEFINED.  Those record types that have an internal structure and must be
'massaged' to get the data into the stream format the Web is so fond of have
functions for doing just that.  These are VARIABLE, VFC and
FIXED-non-span-non-512 (where the record size is not on a block boundary and is
not allowed to span block boundaries - who the hell uses this stuff anyway?!) 
VARIABLE and VFC record formats have newline carriage control added to records.

When not buffering to cache or contents the module can encapsulate plain-text
and escape HTML-forbidden characters.

Works in conjunction with the CACHE module.  A file read can be simultaneously
used to send the data to the client and load a cache buffer. The request
structure fields 'CacheContentPtr' and 'CacheContentCount' being used in both
block I/O and record mode access to track through the available buffer space,
for this located in the cache structure. For non-cache-load reads uses the
standard buffer space pointed to by 'OutputBufferPtr' and for record mode
access tracked using 'OutputBufferCurrentPtr' and 'OutputBufferRemaining'
fields.

Implements defacto HTTP/1.0 persistent connections.  Only provide "keep-alive"
response if we're sending a file that has 'binary content' (e.g. stream, fixed
512) and know its precise length.  An accurate "Content-Length:" field is vital
for the client's correct reception of the transmitted data.  Currently the only
other time a "keep-alive" response can be provided is when a "304 not-modified"
header is returned (it has a content length of precisely zero!).  October 1997:
noted that Netscape Navigator 3.n seems to pay no attention to a
"Content-Length: 0" with a "Keep-Alive:" connection, it just sits there waiting
until the keep-alive time closes the connection, after which it reports
"document contains no data". (IE 3.02 seems to behave correctly ;^)  I have
therefore disabled persistent connections for zero-length files.


VERSION HISTORY
---------------
24-OCT-2024  MGD  bugfix; FileNextBlocks() StrDscBegin()
01-SEP-2023  MGD  bugfix; FileAcpInfoAst() 64 bit file size
29-JUN-2021  MGD  bugfix; FileBegin() ERROR_REPORTED() free file task
20-MAR-2021  MGD  FILE_VAR_ASIS provides exactly what is on the disk
12-NOV-2020  MGD  file ->SizeInBytes64 now 64 bit datum
04-JAN-2019  MGD  bugfix; AuthCompleted()
13-AUG-2018  MGD  FileVariableRecord() implement SET mapping carriage control
02-MAY-2016  MGD  bugfix; FileParseAst() regression with search list file
21-JUN-2014  MGD  bugfix; FileParseAst() allow for non-dir .DIR files
21-SEP-2013  MGD  FileAcpInfoAst() '$.' file extension kludge
07-SEP-2013  MGD  bugfix; ensure "?httpd=content&type=" is URL-decoded
01-JUN-2013  MGD  bugfix; non-ODS_EXTENDED platforms must OdsParse()
                    NAM$M_NOCONCEAL before OdsSearchNoConceal()
30-JAN-2012  MGD  bugfix; FileAcpInfoAst() SS$_BADPARAM >2GB <4GB (per JPP)
06-NOV-2010  MGD  FileInfoAcpAst() if directory listing README ignore NOPRIV
23-MAY-2010  MGD  FileNextBlocks() change QIO file size from long to quad
                  to cater for files greater than 4GB (4GB+ is limited to
                  file serving only, no ranges, etc.)
05-OCT-2009  MGD  use OutputFileBufferSize to maximise file transfer
19-AUG-2009  MGD  bugfix; FileAcpInfoAst() byte-range limit negative offset
11-JUN-2009  MGD  "*.*__WASDAV;" reports not found
09-JUN-2007  MGD  use STR_DSC
                  FileGenerateEntityTag()
20-OCT-2005  MGD  bugfix; FileNextBlocks() ensure VARiable record format
                  files have records read on word (even byte) boundaries
28-MAY-2005  MGD  bugfix; if none-match entity and IfModifiedSince() logic
29-NOV-2004  MGD  bugfix; FileVariableRecord() memset only if positive
19-SEP-2004  MGD  bugfix; even number of bytes on a disk $QIO READVBLK
20-JUL-2004  MGD  HTTP/1.1 compliance,
                  HttpIfUnModifiedSince() check,
                  calculate content-length for byte range response header
26-APR-2004  MGD  major changes to eliminate RMS from file access
                  (WASD's doing all the content conversion work anyway!)
                  by using ACP/QIOs and massaging record content explicitly
                  (outgrowth of returns from 18-FEB-2004 changes)
18-FEB-2004  MGD  read variable record format files using block IO and then
                  explicitly process the those records to produce a stream-LF
                  block of data in their place!
                  (provides in excess of 400% throughput boost!!! :^)
                  bugfix; rare RECTOOBIG on variable record length file where
                  longest record exceeded 'OutputBufferSize' so initialize
                  buffer to maximum of 'OutputBufferSize' or file lrl (use
                  'rqptr->rqOutput.BufferSize' instead of 'OutputBufferSize')
21-AUG-2003  MGD  support byte-range requests on non-VAR files
12-AUG-2003  MGD  access to HTA or HTL file now reports not found
09-JUL-2003  MGD  rework for new caching requirements
16-JUN-2003  MGD  bugfix; FileSetCharset() following initial CacheSearch()
                  moved to CACHE.C module (ACCVIO if entry NULLed)
05-OCT-2002  MGD  no sneaky getting directory contents by downloading files!
                  refine VMS security profile usage
03-JUN-2002  MGD  bugfix; (well sort of) it would appear that after NO_CONCEAL
                  searching and a sys$open() you must sys$close() *before* the
                  SYNCHCK sys$parse() release of resources otherwise a channel
                  to the device is left assigned!!
27-APR-2002  MGD  make SYSPRV enabled ASTs asynchronous
19-MAR-2002  MGD  bugfix; OdsParse() for VMS authenticated request
15-MAR-2002  MGD  bugfix; FileNextRecordAst() VAR file into contents buffer
18-NOV-2001  MGD  FileAcceptLang..()
23-OCT-2001  MGD  bugfix; FileNextBlocksAst() 'ContentRemaining'
04-AUG-2001  MGD  modifications in line with changes in the handling
                  of file and cache (now MD5 hash based) processing,
                  block I/O complete if _rsz is less than _usz
                  support module WATCHing
10-MAY-2001  MGD  bugfix; FileNextRecordAst() buffer flush
05-MAR-2001  MGD  bugfix; FileNextBlocks() 32767 to 0xfe00 (65024)
29-DEC-2000  MGD  allow a file's contents to be read into a buffer
23-DEC-2000  MGD  allow access to an HTL if it is authorized
06-DEC-2000  MGD  make a search list DNF appear as a FNF
01-SEP-2000  MGD  add optional, local path authorization
                  (for calls from the likes of SSI.C)
09-JUN-2000  MGD  search-list processing refined
04-MAR-2000  MGD  use NetWriteInit(), et.al.
27-DEC-1999  MGD  support ODS-2 and ODS-5 using ODS module
10-OCT-1999  MGD  "scrunched" (in fact all) SSI files, prevent streamLFing
17-SEP-1999  MGD  bugfix; sys$parse() NAM$M_NOCONCEAL for search lists
23-MAY-1999  MGD  do not allow "?httpd=content" requests to be cached
05-FEB-1999  MGD  bugfix; FileNextRecord() zero '_usz'
07-NOV-1998  MGD  WATCH facility
19-SEP-1998  MGD  improve granularity of file open, connect, close, ACP
14-MAY-1998  MGD  request-specified content-type ("httpd=content&type=")
19-MAR-1998  MGD  buffer VBN and first free byte for use by cache module
19-JAN-1998  MGD  new NetWriteBuffered() and structures
07-JAN-1998  MGD  groan, bugfix; record processing for files > 4096 bytes
                  completely brain-dead ... sorry
22-NOV-1997  MGD  sigh, bugfix; heap corruption by file cache
05-OCT-1997  MGD  file cache,
                  keep-alive now not used if the content-length is zero
17-SEP-1997  MGD  if block-mode open is locked retry open in record-mode
17-AUG-1997  MGD  message database,
                  SYSUAF-authenticated users security-profile,
                  addressed potential problem with FIXed and odd-byte records
08-JUN-1997  MGD  if request "Pragma: no-cache" then always return 
27-FEB-1997  MGD  delete on close for "temporary" files
01-FEB-1997  MGD  HTTPd version 4
01-AUG-1996  MGD  Variable to stream-LF file conversion "on-the-fly"
12-APR-1996  MGD  determine read method (record or binary) from record format;
                  implemented persistent connections ("keep-alive")
01-DEC-1995  MGD  HTTPd version 3
27-SEP-1995  MGD  added If-Modified-Since: functionality;
                  changed carriage-control on records from <CR><LF> to single
                  <LF> ('\n' ... newline), to better comply with some browsers
                  (Netscape was spitting on X-bitmap files, for example!)
07-AUG-1995  MGD  ConfigcfReport.MetaInfoEnabled to allow physical file
                  specification included as commentary within an HTML file
13-JUL-1995  MGD  bugfix; occasionally a record was re-read after flushing
                  the records accumulated in the buffer NOT due to RMS$_RTB
20-DEC-1994  MGD  initial development for multi-threaded daemon
*/
/*****************************************************************************/

#ifdef WASD_VMS_V7
#undef _VMS__V6__SOURCE
#define _VMS__V6__SOURCE
#undef __VMS_VER
#define __VMS_VER 70000000
#undef __CRTL_VER
#define __CRTL_VER 70000000
#endif

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

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

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

#define WASD_MODULE "FILE"

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

char ErrorBufferFileMaxBytes [] =
  "Exceeds reasonable limit on the size of this type of file.";

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

#ifdef DBUG
extern BOOL Debug;
#else
#define Debug 0 
#endif

extern BOOL  CacheEnabled,
             OdsExtended,
             WebDavEnabled;

extern int  EfnWait,
            EfnNoWait,
            HttpdTickSecond,
            OutputFileBufferSize;

extern ulong  SysPrvMask[];

extern char  ConfigContentTypeSsi[],
             ConfigContentTypeUnknown[],
             ConfigDefaultFileContentType[],
             ErrorSanityCheck[],
             HttpProtocol[],
             SoftwareID[];

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

/*****************************************************************************/
/*
Initalize the file task structure.
NOTE: FILE PATHS ARE ALWAYS AUTHORIZED UNLESS EXPLICITLY TURNED OFF.
*/ 
 
FILE_TASK* FileNewTask (REQUEST_STRUCT *rqptr)

{
   FILE_TASK  *tkptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE, "FileNewTask()");

   if (tkptr = rqptr->FileTaskPtr)
      ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);

   rqptr->FileTaskPtr = tkptr =
      (FILE_TASK*)VmGetHeap (rqptr, sizeof(FILE_TASK));

   tkptr->AuthorizePath = true;

   return (tkptr);
}

/*****************************************************************************/
/*
Authorize the path to FileBegin() 'FileName' before accessing.
NOTE: FILE PATHS ARE ALWAYS AUTHORIZED UNLESS EXPLICITLY TURNED OFF.
*/ 
 
void FileSetAuthorizePath
(
REQUEST_STRUCT *rqptr,
BOOL YesNo
)
{
   FILE_TASK  *tkptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "FileSetAuthorizePath() !&B", YesNo);

   /* if not previously initialised */
   if (!(tkptr = rqptr->FileTaskPtr)) tkptr = FileNewTask (rqptr);

   tkptr->AuthorizePath = YesNo;
}

/*****************************************************************************/
/*
Read the file into memory pointed to by 'rqptr->FileContentPtr->ContentPtr'
and 'rqptr->FileContentPtr->ContentLength' in length.  File is null-terminated
in case it is text of some sort (this null is not counted in the length).
*/ 
 
void FileSetContentHandler
(
REQUEST_STRUCT *rqptr,
REQUEST_AST ContentHandlerFunction,
int SizeMax
)
{
   FILE_TASK  *tkptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "FileSetContentHandler() !&X !UL",
                 ContentHandlerFunction, SizeMax);

   /* if not previously initialised */
   if (!(tkptr = rqptr->FileTaskPtr)) tkptr = FileNewTask (rqptr);

   /* setting this non-NULL indicates the file contents should be buffered */
   tkptr->ContentHandlerFunction = ContentHandlerFunction;
   tkptr->FileContentsSizeMax = SizeMax;
}

/*****************************************************************************/
/*
File allowed to be cached?
*/ 
 
void FileSetCacheAllowed
(
REQUEST_STRUCT *rqptr,
BOOL YesNo
)
{
   FILE_TASK  *tkptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "FileSetCacheAllowed() !&B", YesNo);

   /* if not previously initialised */
   if (!(tkptr = rqptr->FileTaskPtr)) tkptr = FileNewTask (rqptr);

   tkptr->CacheAllowed = YesNo;
}

/*****************************************************************************/
/*
Escape HTML-forbidden characters (e.g. '<') during file output.
*/ 
 
void FileSetEscapeHtml
(
REQUEST_STRUCT *rqptr,
BOOL YesNo
)
{
   FILE_TASK  *tkptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "FileSetEscapeHtml() !&B", YesNo);

   /* if not previously initialised */
   if (!(tkptr = rqptr->FileTaskPtr)) tkptr = FileNewTask (rqptr);

   tkptr->EscapeHtml = YesNo;
   if (!rqptr->rqCache.DoNotCache) rqptr->rqCache.DoNotCache = YesNo;
}

/*****************************************************************************/
/*
Enclose the file output with <pre>...</pre> tags.
*/ 
 
void FileSetPreTag
(
REQUEST_STRUCT *rqptr,
BOOL YesNo
)
{
   FILE_TASK  *tkptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "FileSetPreTag() !&B", YesNo);

   /* if not previously initialised */
   if (!(tkptr = rqptr->FileTaskPtr)) tkptr = FileNewTask (rqptr);

   tkptr->PreTagFileContents = YesNo;
   if (!rqptr->rqCache.DoNotCache) rqptr->rqCache.DoNotCache = YesNo;
}

/*****************************************************************************/
/*
Begin to transfer a file.  
*/ 
 
FileBegin
(
REQUEST_STRUCT *rqptr,
REQUEST_AST NextTaskFunction,
REQUEST_AST NoSuchFileFunction,
MD5_HASH *Md5HashPtr,
char *FileName,
char *ContentTypePtr
)
{
   int  status,
        FilePathLength;
   char  *cptr, *sptr, *zptr;
   FILE_TASK  *tkptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "FileBegin() !&F !&B !&A !&A !16&H !&Z !&Z",
                 &FileBegin, ERROR_REPORTED(rqptr),
                 NextTaskFunction, NoSuchFileFunction,
                 Md5HashPtr, FileName, ContentTypePtr);

   if (ERROR_REPORTED (rqptr))
   {
      /* previous error, cause threaded processing to unravel */
      if (rqptr->FileTaskPtr)
      {
         VmFreeFromHeap (rqptr, rqptr->FileTaskPtr, FI_LI);
         rqptr->FileTaskPtr = NULL;
      }
      SysDclAst (NextTaskFunction, rqptr);
      return;
   }

   InstanceGblSecIncrLong (&AccountingPtr->DoFileModuleCount);

   /* if not previously initialised */
   if (!(tkptr = rqptr->FileTaskPtr)) tkptr = FileNewTask (rqptr);

   tkptr->ContentTypePtr = ContentTypePtr;
   tkptr->NextTaskFunction = NextTaskFunction;
   tkptr->NoSuchFileFunction = NoSuchFileFunction;

   if (rqptr->rqPathSet.AcceptLangChar &&
       ((ContentTypePtr && strsame (ContentTypePtr, "text/", 5)) ||
        rqptr->rqContentInfo.TypeText ||
        rqptr->rqPathSet.AcceptLangWildcard))
   {
      /* if a default language specified and default is satisfactory */
      if (!rqptr->rqPathSet.AcceptLangPtr || !FileAcceptLangDefault (rqptr))
      {
         /* try and resolve a language-specific document */
         if (!FileName)
         {
            /* must be a cache search, not interested at this stage */
            FileEnd (rqptr);
            return;
         }
         FileAcceptLangBegin (rqptr, FileName);
         return;
      }
   }

   if (!(cptr = FileName)) cptr = "";
   zptr = (sptr = tkptr->FileName) + sizeof(tkptr->FileName);
   while (*cptr && sptr < zptr) *sptr++ = *cptr++;
   if (sptr >= zptr)
   {
      ErrorGeneralOverflow (rqptr, FI_LI);
      FileEnd (rqptr);
      return;
   }
   *sptr = '\0';
   tkptr->FileNameLength = sptr - tkptr->FileName;

   if (Md5HashPtr)
   {
      /* buffer the supplied resource hash in the task structure */
      memcpy (&tkptr->Md5Hash, Md5HashPtr, sizeof(MD5_HASH));
   }
   else
   {
      /* generate a hash representing the file name being accessed */
      Md5Digest (tkptr->FileName, tkptr->FileNameLength, &tkptr->Md5Hash);
   }

   /* no file name supplied indicates only searching the cache */
   if (tkptr->AuthorizePath && tkptr->FileNameLength)
   {
      /***********************/
      /* check authorization */
      /***********************/

      cptr = MapVmsPath (tkptr->FileName, rqptr);
      if (!*cptr)
      {
         /* MAPURL errors are returned with a leading null (historical ;^) */
         ErrorGeneral (rqptr, cptr+1, FI_LI);
         FileEnd (rqptr);
         return;
      }
      Authorize (rqptr, cptr, -1, NULL, 0, &FileAuthorizationAst);
      if (VMSnok (rqptr->rqAuth.FinalStatus))
      {
         /* if asynchronous authentication is not underway */
         if (rqptr->rqAuth.FinalStatus != AUTH_PENDING) FileEnd (rqptr);
         return;
      }
   }

   /* not to-be-authorized, or authorized ... just carry on regardless! */
   FileAuthorizationAst (rqptr);
} 

/*****************************************************************************/
/*
There is a default language set against the path.  Compare the request header
"Accept-Language:" first language (if any) to the default language.  If the
same return true, otherwise false.
*/ 
 
BOOL FileAcceptLangDefault (REQUEST_STRUCT *rqptr)

{
   char  *cptr, *sptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE, "FileAcceptLangDefault()");

   if (WATCHING (rqptr, WATCH_RESPONSE))
      WatchThis (WATCHITM(rqptr), WATCH_RESPONSE,
                 "LANGUAGE default:!AZ accept:!AZ",
                 rqptr->rqPathSet.AcceptLangPtr,
                 rqptr->rqHeader.AcceptLangPtr ?
                    rqptr->rqHeader.AcceptLangPtr : "(none)");

   if (!(cptr = rqptr->rqPathSet.AcceptLangPtr)) return (true);
   if (!(sptr = rqptr->rqHeader.AcceptLangPtr)) return (true);
   if (!*sptr || *sptr == '*') return (true);
   while (*cptr && *sptr && *sptr != ',' && *sptr != ';' &&
          to_lower(*cptr) == to_lower(*sptr))
   {
      cptr++;
      sptr++;
   }
   if (!*cptr && (!*sptr || *sptr == ',' || *sptr == ';'))
      return (true);
   else
      return (false);
} 

/*****************************************************************************/
/*
This series if functions attempts to resolve a language-specific document based
on the file name originally supplied.  A seachable file specification is
contructed and used to find all possible language variants of the file
originally specified.
*/ 
 
FileAcceptLangBegin
(
REQUEST_STRUCT *rqptr,
char *FileName
)
{
   BOOL  LowerCaseHit;
   int  cnt, status;
   char  *cptr, *sptr, *zptr;
   FILE_TASK  *tkptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "FileAcceptLangBegin() !&Z", FileName);

   if (WATCHING (rqptr, WATCH_RESPONSE))
      WatchThis (WATCHITM(rqptr), WATCH_RESPONSE, "LANGUAGE !AZ", FileName);

   tkptr = rqptr->FileTaskPtr;

   cptr = FileName;
   zptr = (sptr = tkptr->FileName) + sizeof(tkptr->FileName);
   /* copy the file name until the end of directory */
   while (*cptr && sptr < zptr)
   {
      if (*cptr == ']' && !SAME2(cptr,'][')) break;
      *sptr++ = *cptr++;
   }
   LowerCaseHit = false;

   /* copy the file name to the type delimiting period */
   while (*cptr && sptr < zptr)
   {
#ifdef ODS_EXTENDED
      if (OdsExtended)
      {
         if (*cptr == '^' && SAME2(cptr,'^.'))
         {
            *sptr++ = *cptr++;
            if (sptr < zptr) *sptr++ = *cptr++;
            continue;
         }
      }
      if (*cptr == '.') break;
      if (islower(*cptr)) LowerCaseHit = true;
      *sptr++ = *cptr++;
#else /* ODS_EXTENDED */
      if (*cptr == '.') break;
      *sptr++ = *cptr++;
#endif /* ODS_EXTENDED */
   }

#ifdef ODS_EXTENDED
   /* introduce an escaping '^' for the original type delimiting period */
   if (OdsExtended &&
       *cptr && rqptr->rqPathSet.AcceptLangChar == '.' &&
       sptr < zptr)
      *sptr++ = '^';
#endif /* ODS_EXTENDED */

   if (rqptr->rqPathSet.AcceptLangTypeVariant)
   {
      /*********************/
      /* variant file type */
      /*********************/

      /* if there is no existing period add one */
      if (!*cptr && sptr < zptr) *sptr++ = '.';

      /* copy file type to end of string */
      while (*cptr && sptr < zptr)
      {
#ifdef ODS_EXTENDED
         if (islower(*cptr)) LowerCaseHit = true;
#endif /* ODS_EXTENDED */
         *sptr++ = *cptr++;
      }

      /* note the point at which we begin to add wildcarded variants */
      tkptr->AcceptLangVariantPtr = sptr;

      /* append the variant delimiting character and a wildcard */
      if (sptr < zptr) *sptr++ = rqptr->rqPathSet.AcceptLangChar;
      if (sptr < zptr) *sptr++ = '*';
   }
   else
   {
      /*********************/
      /* variant file name */
      /*********************/

      /* note the point at which we begin to add wildcarded variants */
      tkptr->AcceptLangVariantPtr = sptr;

      /* insert the variant delimiting character, then wildcard(s :^) */
      if (sptr < zptr) *sptr++ = rqptr->rqPathSet.AcceptLangChar;
      /*
         Bit of a shonky here.
         I'm reserving space in the filename for the largest possible
         ISO language string which (as far as I know) is 5 characters
         (e.g. "fr-BE", "fr-CA").  This makes reusing the file buffer
         space in FileAcceptLangSelect() much easier (I'm getting
         lazier faster than I'm actually getting older).  The multiple
         wildcards used here (seem to) make no difference for RMS.
      */
      for (cnt = FILE_ACCEPT_LANG_VARIANT_MAX; cnt && sptr < zptr; cnt--)
         *sptr++ = '*';

      /* copy file type to end of string */
      while (*cptr && sptr < zptr)
      {
#ifdef ODS_EXTENDED
         if (islower(*cptr)) LowerCaseHit = true;
#endif /* ODS_EXTENDED */
         *sptr++ = *cptr++;
      }
   }

   if (sptr >= zptr)
   {
      ErrorGeneralOverflow (rqptr, FI_LI);
      FileEnd (rqptr);
      return;
   }
   *sptr = '\0';
   tkptr->FileNameLength = sptr - tkptr->FileName;

   tkptr->AcceptLangLowerCase = LowerCaseHit;

   OdsStructInit (&tkptr->FileOds, true);

   OdsParse (&tkptr->FileOds,
             tkptr->FileName, tkptr->FileNameLength, NULL, 0,
             0, &FileAcceptLangParseAst, rqptr);
} 

/*****************************************************************************/
/*
AST called from FileAcceptLangBegin() when asynchronous parse completes.  Check
the status and if OK begin the search.
*/

FileAcceptLangParseAst (REQUEST_STRUCT *rqptr)

{
   int  status;
   FILE_TASK  *tkptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "FileAcceptLangParseAst() !&F sts:!&S stv:!&S",
                 &FileAcceptLangParseAst,
                 rqptr->FileTaskPtr->FileOds.Fab.fab$l_sts,
                 rqptr->FileTaskPtr->FileOds.Fab.fab$l_stv);

   tkptr = rqptr->FileTaskPtr;

   if (VMSnok (status = tkptr->FileOds.Fab.fab$l_sts))
   {
      /*****************/
      /* error parsing */
      /*****************/

      /* ensure only the original file name appears in any error messages */
      tkptr->FileName[tkptr->FileNameLength] = '\0';

      /* if its a search list treat directory not found as if file not found */
      if ((tkptr->FileOds.Nam_fnb & NAM$M_SEARCH_LIST) && status == RMS$_DNF)
         status = RMS$_FNF;

      if (tkptr->NoSuchFileFunction)
      {
         if (WATCHING (rqptr, WATCH_RESPONSE))
            WatchThis (WATCHITM(rqptr), WATCH_RESPONSE,
                       "FILE !&S", status);
         rqptr->HomePageStatus = status;
         FileEnd (rqptr);
         return;
      }

      rqptr->rqResponse.ErrorTextPtr = MapVmsPath (tkptr->FileName, rqptr);
      rqptr->rqResponse.ErrorOtherTextPtr = tkptr->FileName;
      ErrorVmsStatus (rqptr, status, FI_LI);
      FileEnd (rqptr);
      return;
   }

   OdsSearch (&tkptr->FileOds, &FileAcceptLangSearchAst, rqptr);
}

/*****************************************************************************/
/*
AST called from FileAcceptLangParseAst() and then from
FileAcceptLangSearchAst() when asynchronous search completes.  Check the status
and if OK buffer the resolved language component in a comma-separated list. 
The continue the search.  Once no-more-files status occurs call
FileAcceptLangSelect() to assess the language variants (if any).
*/

FileAcceptLangSearchAst (REQUEST_STRUCT *rqptr)

{
   int  status;
   char  *cptr, *sptr, *zptr;
   FILE_TASK  *tkptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "FileAcceptLangSearchAst() !&F sts:!&S stv:!&S",
                 &FileAcceptLangSearchAst,
                 rqptr->FileTaskPtr->FileOds.Fab.fab$l_sts,
                 rqptr->FileTaskPtr->FileOds.Fab.fab$l_stv);

   tkptr = rqptr->FileTaskPtr;

   if (VMSnok (status = tkptr->FileOds.Fab.fab$l_sts))
   {
      if (status == RMS$_FNF || status == RMS$_NMF)
      {
         /***************************/
         /* end of directory search */
         /***************************/

         tkptr->FileOds.ParseInUse = false;
         FileAcceptLangSelect (rqptr);
         return;
      }

      /**********************/
      /* sys$search() error */
      /**********************/

      /* ensure only the original file name appears in any error messages */
      tkptr->FileName[tkptr->FileNameLength] = '\0';

      rqptr->rqResponse.ErrorTextPtr = MapVmsPath (tkptr->FileName, rqptr);
      rqptr->rqResponse.ErrorOtherTextPtr = tkptr->FileName;
      ErrorVmsStatus (rqptr, status, FI_LI);
      FileEnd (rqptr);
      return;
   }

   /****************/
   /* add language */
   /****************/

   if (!tkptr->AcceptLangTypesSize)
   {
      tkptr->AcceptLangTypesPtr = VmGetHeap (rqptr, FILE_ACCEPT_LANG_SIZE);
      tkptr->AcceptLangTypesSize = FILE_ACCEPT_LANG_SIZE;
   }
   zptr = (sptr = tkptr->AcceptLangTypesPtr) + tkptr->AcceptLangTypesSize;
   sptr += tkptr->AcceptLangTypesLength;
   if (tkptr->AcceptLangTypesLength && sptr < zptr) *sptr++ = ',';

   if (rqptr->rqPathSet.AcceptLangTypeVariant)
   {
      /* variant file type */
      cptr = tkptr->FileOds.NamVersionPtr;
      while (cptr > tkptr->FileOds.NamTypePtr &&
            *cptr != rqptr->rqPathSet.AcceptLangChar) cptr--;
      if (*cptr) cptr++;
      while (cptr < tkptr->FileOds.NamVersionPtr && sptr < zptr)
         *sptr++ = *cptr++;
   }
   else
   {
      /* variant file name */
      cptr = tkptr->FileOds.NamTypePtr;
      while (cptr > tkptr->FileOds.NamNamePtr &&
            *cptr != rqptr->rqPathSet.AcceptLangChar) cptr--;
      if (*cptr) cptr++;
      while (cptr < tkptr->FileOds.NamTypePtr && sptr < zptr)
         *sptr++ = *cptr++;
   }

   /* check the length of the (potential) variant (may not be) */
   if ((sptr - tkptr->AcceptLangTypesPtr) - tkptr->AcceptLangTypesLength <=
       FILE_ACCEPT_LANG_VARIANT_MAX)
   {
      if (sptr >= zptr)
      {
         /* more than FILE_ACCEPT_LANG_SIZE / _VARIANT (256/5~=51!!) */
         ErrorNoticed (rqptr, SS$_BUGCHECK, ErrorSanityCheck, FI_LI);
         ErrorGeneral (rqptr, ErrorSanityCheck, FI_LI);
         FileEnd (rqptr);
         return;
      }
      *sptr = '\0';
      tkptr->AcceptLangTypesLength = sptr - tkptr->AcceptLangTypesPtr;
   }

   OdsSearch (&tkptr->FileOds, &FileAcceptLangSearchAst, rqptr);
}

/*****************************************************************************/
/*
Explicitly called by FileAcceptLangSearchAst() when no-more-files status occurs
to assess the comma-separated list of language components generated (if any).
By comparing each "Accept-Language:" entry against each of the file language
components select the first to match (if any).  If one matches then adjust the
original file name to include the language component with the type.  If none
matches revert to the original file name.  Commence file processing.
*/

FileAcceptLangSelect (REQUEST_STRUCT *rqptr)

{
   BOOL  AcceptLangVariant;
   int  status;
   char  *cptr, *sptr, *tptr, *zptr;
   FILE_TASK  *tkptr;

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

   tkptr = rqptr->FileTaskPtr;

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "FileAcceptLangSelect() !&F !&Z !&Z",
                 &FileAcceptLangSelect, tkptr->AcceptLangTypesPtr,
                 rqptr->rqHeader.AcceptLangPtr);

   if (WATCHING (rqptr, WATCH_RESPONSE))
      WatchThis (WATCHITM(rqptr), WATCH_RESPONSE,
                 "LANGUAGE variants:!AZ accept:!AZ",
                 tkptr->AcceptLangTypesPtr ?
                    tkptr->AcceptLangTypesPtr : "(none)",
                 rqptr->rqHeader.AcceptLangPtr ?
                    rqptr->rqHeader.AcceptLangPtr : "(none)");

   AcceptLangVariant = false;
   if (tkptr->AcceptLangTypesLength)
   {
      /*********************************/
      /* at least one possible variant */
      /*********************************/

      cptr = rqptr->rqHeader.AcceptLangPtr;
      while (*cptr)
      {
         while (*cptr && ISLWS(*cptr)) cptr++;
         if (!*cptr) break;
         sptr = tkptr->AcceptLangTypesPtr;
         while (*sptr)
         {
            tptr = cptr;
            while (*tptr && *tptr != ',' && *tptr != ';' &&
                   *sptr && *sptr != ',' &&
                   to_lower(*tptr) == to_lower(*sptr))
            {
               tptr++;
               sptr++;
            }
            if ((!*tptr || *tptr == ',' || *tptr == ';') &&
                (!*sptr || *sptr == ','))
            {
               AcceptLangVariant = true;
               break;
            }
            while (*sptr && *sptr != ',') sptr++;
            if (*sptr) sptr++;
         }
         if (AcceptLangVariant) break;
         while (*cptr && *cptr != ',') cptr++;
         if (*cptr) cptr++;
      }
   }

   if (AcceptLangVariant)
   {
      /**********************************/
      /* add language variant component */
      /**********************************/

      /*
         This handles both the post-file-type language variant, where the
         wildcard has just been appended to the file type, as well as the
         post-file-name language variant, where the wildcard was inserted
         between the end of the file name and the type delimiting period
         (in this case it reserved space using 5 wildcards).
      */
      zptr = tkptr->FileName + sizeof(tkptr->FileName);
      /* step over any period-escaping character (would be EFS only) */
      if (*(sptr = tkptr->AcceptLangVariantPtr) == '^') sptr++;
      /* step over the variant delimiting character */
      sptr++;
      /* 'cptr' has been left pointing at the matching language */
      tptr = cptr;
      if (tkptr->AcceptLangLowerCase)
         while (*cptr && *cptr != ',' && *cptr != ';' && sptr < zptr)
            *sptr++ = to_lower(*cptr++);
      else
         while (*cptr && *cptr != ',' && *cptr != ';' && sptr < zptr)
            *sptr++ = to_upper(*cptr++);
      if (sptr >= zptr)
      {
         ErrorGeneralOverflow (rqptr, FI_LI);
         FileEnd (rqptr);
         return;
      }
      /* absorb any remaining wildcards */
      for (cptr = sptr; *cptr == '*'; cptr++);
      while (*cptr && sptr < zptr) *sptr++ = *cptr++;
      if (sptr >= zptr)
      {
         ErrorGeneralOverflow (rqptr, FI_LI);
         FileEnd (rqptr);
         return;
      }
      *sptr = '\0';
      tkptr->FileNameLength = sptr - tkptr->FileName;

      if (rqptr->rqPathSet.AcceptLangTypeVariant)
      {
         /* (re)determine the (possibly) new content-type */
         while (sptr > tkptr->FileName && *sptr != '.') sptr--;
         ConfigContentType (&rqptr->rqContentInfo, sptr);
         tkptr->ContentTypePtr = rqptr->rqContentInfo.ContentTypePtr;
      }
   }
   else
   {
      /*******************************/
      /* no language variant, revert */
      /*******************************/

      /* eliminate any period-escaping character (would be EFS only) */
      if (*(sptr = cptr = tkptr->AcceptLangVariantPtr) == '^') cptr++;
      /* eliminate the variant delimiting character and the wildcard(s) */
      cptr++;
      while (*cptr && *cptr == '*') cptr++;
      while (*cptr) *sptr++ = *cptr++;
      *sptr = '\0';
      tkptr->FileNameLength = sptr - tkptr->FileName;
   }

   if (WATCHING (rqptr, WATCH_RESPONSE))
      WatchThis (WATCHITM(rqptr), WATCH_RESPONSE,
                 "LANGUAGE !AZ", tkptr->FileName);

   /*************************/
   /* begin processing file */
   /*************************/

   /* generate a hash representing the file name being accessed */
   Md5Digest (tkptr->FileName, tkptr->FileNameLength, &tkptr->Md5Hash);

   if (WATCHING (rqptr, WATCH_RESPONSE))
      WatchThis (WATCHITM(rqptr), WATCH_RESPONSE,
                 "FILE !16&H !AZ", &tkptr->Md5Hash, tkptr->FileName);

   /* no file name supplied indicates only searching the cache */
   if (tkptr->AuthorizePath && tkptr->FileNameLength)
   {
      /***********************/
      /* check authorization */
      /***********************/

      cptr = MapVmsPath (tkptr->FileName, rqptr);
      if (!*cptr)
      {
         /* MAPURL errors are returned with a leading null (historical ;^) */
         ErrorGeneral (rqptr, cptr+1, FI_LI);
         FileEnd (rqptr);
         return;
      }
      Authorize (rqptr, cptr, -1, NULL, 0, &FileAuthorizationAst);
      if (VMSnok (rqptr->rqAuth.FinalStatus))
      {
         /* if asynchronous authentication is not underway */
         if (rqptr->rqAuth.FinalStatus != AUTH_PENDING) FileEnd (rqptr);
         return;
      }
   }

   /* not to-be-authorized, or authorized ... just carry on regardless! */
   FileAuthorizationAst (rqptr);
} 

/*****************************************************************************/
/*
This function provides an AST target is Authorize()ation ended up being done
asynchronously, otherwise it is just called directly to continue the modules
processing.
*/

FileAuthorizationAst (REQUEST_STRUCT *rqptr)

{
   int  status;
   char  *cptr;
   FILE_TASK  *tkptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "FileAuthorizationAst() !&F !&S",
                 &FileAuthorizationAst, rqptr->rqAuth.FinalStatus);

   /* may have been delivered asynchronously */
   rqptr->rqAuth.AstFunction = NULL;

   if (VMSnok (rqptr->rqAuth.FinalStatus))
   {
      FileEnd (rqptr);
      return;
   }

   tkptr = rqptr->FileTaskPtr;

   if (!tkptr->ContentTypePtr)
      tkptr->ContentTypePtr = ConfigContentTypeUnknown;
   if (ConfigSameContentType (tkptr->ContentTypePtr,
                              ConfigContentTypeUnknown, -1))
   {
      /* if the content-type is not known then use the default */
      if (Config.cfContent.ContentTypeDefaultPtr[0])
         tkptr->ContentTypePtr = Config.cfContent.ContentTypeDefaultPtr;
      else
         tkptr->ContentTypePtr = ConfigDefaultFileContentType;
   }

   if (!rqptr->rqCache.DoNotCache)
   {
      status = CacheSearch (rqptr);
      if (WATCHMOD (rqptr, WATCH_MOD_FILE))
         WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                    "CacheSearch() !&S", status);

      /* success status indicates the file is being supplied from cache */
      if (VMSok (status)) return;

      /* no file name supplied indicates only searching the cache */
      if (!tkptr->FileNameLength)
      {
         FileEnd (rqptr);
         return;
      }
   }

   FileParse (rqptr);
} 

/*****************************************************************************/
/*
Parse the file task's file name.
*/

FileParse (REQUEST_STRUCT *rqptr)

{
   FILE_TASK  *tkptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "FileParse() !&F", &FileParse);

   tkptr = rqptr->FileTaskPtr;

   AuthAccessEnable (rqptr, tkptr->FileName, AUTH_ACCESS_READ);

   OdsParse (&tkptr->FileOds, tkptr->FileName, tkptr->FileNameLength,
             ".", 1, 0, &FileParseAst, rqptr);

   AuthAccessEnable (rqptr, 0, 0);
} 

/*****************************************************************************/
/*
AST when FileParse() asynchronous parsecompletes.  If status OK set up and
queue an ACP QIO to get file size and revision date/time, ASTing to
FileAcpInfoAst().
*/

FileParseAst (REQUEST_STRUCT *rqptr)

{
   int  status;
   char  *cptr;
   FILE_TASK  *tkptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "FileParseAst() !&F sts:!&S stv:!&S", &FileParseAst,
                 rqptr->FileTaskPtr->FileOds.Fab.fab$l_sts,
                 rqptr->FileTaskPtr->FileOds.Fab.fab$l_stv);

#if WATCH_MOD
   HttpdCheckPriv (FI_LI);
#endif /* WATCH_MOD */

   tkptr = rqptr->FileTaskPtr;

   if (VMSok (status = tkptr->FileOds.Fab.fab$l_sts))
   {
      /**************/
      /* exclusions */
      /**************/

      if (MATCH5 (tkptr->FileOds.NamTypePtr, ".DIR;") ||
          MATCH5 (tkptr->FileOds.NamTypePtr, ".dir;"))
      {
         /* no sneaky getting directory contents by downloading files! */
         status = OdsReallyADir (rqptr, &tkptr->FileOds);
         if (VMSok (status)) status = SS$_NOPRIV;
      }
      else
      if (rqptr->RemoteUser[0] &&
          strsame (tkptr->FileOds.NamTypePtr,
                    HTL_FILE_TYPE, sizeof(HTL_FILE_TYPE)-1) &&
          *(tkptr->FileOds.NamTypePtr+sizeof(HTL_FILE_TYPE)-1) == ';')
      {
         /* access to an HTL is OK *IF* it is authorized!! (for admin) */
         tkptr->ContentTypePtr = "text/plain";
      }
      else
      if ((strsame (tkptr->FileOds.NamTypePtr,
                    HTA_FILE_TYPE, sizeof(HTA_FILE_TYPE)-1) &&
           *(tkptr->FileOds.NamTypePtr+sizeof(HTA_FILE_TYPE)-1) == ';') ||
          (strsame (tkptr->FileOds.NamTypePtr,
                    HTL_FILE_TYPE, sizeof(HTL_FILE_TYPE)-1) &&
           *(tkptr->FileOds.NamTypePtr+sizeof(HTL_FILE_TYPE)-1) == ';'))
      {
         /* attempt to retrieve an HTA/HTL authorization file, scotch that! */
         status = SS$_NOSUCHFILE;
      }
      else
      if (DavMetaFile (&tkptr->FileOds))
         status = SS$_NOSUCHFILE;
   }

   /* if a wildcard the ACP function will return the first matching file! */
   if (tkptr->FileOds.Nam_fnb & NAM$M_WILDCARD) status = RMS$_WLD;

   if (status == SS$_ABORT &&
       (MATCH5 (tkptr->FileOds.NamTypePtr, ".DIR;") ||
        MATCH5 (tkptr->FileOds.NamTypePtr, ".dir;")))
      status = SS$_NORMAL;

   if (VMSok (status))
   {
      if (WebDavEnabled)
      {
         /* WebDAV metadata (sub)directory? */
         if (rqptr->rqPathSet.WebDavRead ||
             rqptr->rqPathSet.WebDavWrite ||
             rqptr->rqPathSet.WebDavProfile ||
             rqptr->rqPathSet.WebDavServer)
            if (DavMetaDir (rqptr, &tkptr->FileOds))
               status = RMS$_DNF;
      }
   }

   if (VMSnok (status))
   {
      /*****************/
      /* error parsing */
      /*****************/

      /* if its a search list treat directory not found as if file not found */
      if ((tkptr->FileOds.Nam_fnb & NAM$M_SEARCH_LIST) && status == RMS$_DNF)
         status = RMS$_FNF;

      if (tkptr->NoSuchFileFunction)
      {
         if (WATCHING (rqptr, WATCH_RESPONSE))
            WatchThis (WATCHITM(rqptr), WATCH_RESPONSE, "FILE !&S", status);
         rqptr->HomePageStatus = status;
         FileEnd (rqptr);
         return;
      }

      rqptr->rqResponse.ErrorTextPtr = MapVmsPath (tkptr->FileName, rqptr);
      rqptr->rqResponse.ErrorOtherTextPtr = tkptr->FileName;
      ErrorVmsStatus (rqptr, status, FI_LI);
      FileEnd (rqptr);
      return;
   }

   if (tkptr->FileOds.Nam_fnb & NAM$M_SEARCH_LIST &&
       !tkptr->SearchListCount++)
   {
      /*******************************/
      /* search to get actual device */
      /*******************************/

      if (WATCHMOD (rqptr, WATCH_MOD_FILE))
         WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE, "SEARCH-LIST");

#ifndef ODS_EXTENDED
      /* for no-conceal search to work */
      OdsParse (&tkptr->FileOds,
                tkptr->FileName, tkptr->FileNameLength,
                ".", 1, NAM$M_NOCONCEAL,
                NULL, 0);
#endif

      AuthAccessEnable (rqptr, tkptr->FileName, AUTH_ACCESS_READ);
      OdsSearchNoConceal (&tkptr->FileOds, &FileParseAst, rqptr);
      AuthAccessEnable (rqptr, 0, 0);
      return;
   }

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

   AuthAccessEnable (rqptr, tkptr->FileName, AUTH_ACCESS_READ);

   OdsFileAcpInfo (&tkptr->FileOds, &FileAcpInfoAst, rqptr); 

   AuthAccessEnable (rqptr, 0, 0);
}

/****************************************************************************/
/*
AST called from FileParseAst() when ACP QIO completes.  If status indicates no
such file then call any file open error processing function originally
supplied, otherwise report the error.  If status OK 'open' the file.
*/

FileAcpInfoAst (REQUEST_STRUCT *rqptr)

{
   static $DESCRIPTOR (FibDsc, "");

   BOOL  RangeValid;
   int  idx, status,
        BufferSize;
   char  *cptr, *sptr, *zptr,
         *rfmptr;
   FILE_CONTENT  *fcptr;
   FILE_QIO  *fqptr;
   FILE_TASK  *tkptr;
   RANGE_BYTE  *rbptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "FileAcpInfoAst() !&F !&S", &FileAcpInfoAst,
                 rqptr->FileTaskPtr->FileOds.FileQio.IOsb.Status);

#if WATCH_MOD
   HttpdCheckPriv (FI_LI);
#endif /* WATCH_MOD */

   tkptr = rqptr->FileTaskPtr;
   fqptr = &tkptr->FileOds.FileQio;

   if ((status = fqptr->IOsb.Status) == SS$_NOSUCHFILE) status = RMS$_FNF;

   if (status == RMS$_FNF)
   {
      /*************************/
      /* file extension kludge */
      /*************************/

      zptr = (cptr = rqptr->rqHeader.PathInfoPtr) +
             rqptr->rqHeader.PathInfoLength;
      while (zptr > cptr && *zptr != '$' && *zptr != '/') zptr--;
      if (zptr > cptr && *(USHORTPTR)zptr == '$.')
      {
         /* hit what looks like the sentinal */
         while (zptr > cptr && *zptr != '.' && *zptr != '/') zptr--;
         if (zptr > cptr && *zptr == '.')
         {
            /* a second period (and perhaps file name extension) */
            while (*zptr != '$') zptr++;
            ResponseLocation (rqptr, cptr, zptr-cptr);
            FileEnd (rqptr);
            return;
         }
      }
   }

   if (status == RMS$_FNF && tkptr->NoSuchFileFunction)
   {
      if (WATCHING (rqptr, WATCH_RESPONSE))
         WatchThis (WATCHITM(rqptr), WATCH_RESPONSE, "FILE !&S", status);
      rqptr->HomePageStatus = status;
      FileEnd (rqptr);
      return;
   }
   else
   {
      /* the last point at which no-such-file could have been reported */
      tkptr->NoSuchFileFunction = NULL;

      /* if it's a directory listing README then ignore NOPRIV */
      if (tkptr->NextTaskFunction == &DirHeading && status == SS$_NOPRIV)
         status = SS$_NORMAL;

      if (VMSnok (status))
      {
         if (!rqptr->AccountingDone++)
            InstanceGblSecIncrLong (&AccountingPtr->DoFileCount);
         rqptr->rqResponse.ErrorTextPtr = MapVmsPath (tkptr->FileName, rqptr);
         rqptr->rqResponse.ErrorOtherTextPtr = tkptr->FileName;
         ErrorVmsStatus (rqptr, status, FI_LI);
         FileEnd (rqptr);
         return;
      }
   }

   /***************/
   /* file exists */
   /***************/

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "did:!UL,!UL,!UL fid:!UL,!UL,!UL {!UL}!-!#AZ",
                 fqptr->Fib.fib$w_did[0], fqptr->Fib.fib$w_did[1],
                 fqptr->Fib.fib$w_did[2], fqptr->Fib.fib$w_fid[0],
                 fqptr->Fib.fib$w_fid[1], fqptr->Fib.fib$w_fid[2],
                 fqptr->FileNameDsc.dsc$w_length,
                 fqptr->FileNameDsc.dsc$a_pointer);

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

   if ((uint)fqptr->EndOfFileVbn <= 1)
      fqptr->SizeInBytes64 = (uint64)fqptr->FirstFreeByte;
   else
   {
      fqptr->SizeInBytes64 = (uint64)fqptr->EndOfFileVbn - 1;
      fqptr->SizeInBytes64 *= 512;
      fqptr->SizeInBytes64 += fqptr->FirstFreeByte;
   }

   if (WATCHING (rqptr, WATCH_RESPONSE))
   {
      switch (fqptr->RecAttr.fat$b_rtype)
      {
         case FAT$C_VARIABLE :  rfmptr = "VAR"; break;
         case FAT$C_VFC :       rfmptr = "VFC"; break;
         case FAT$C_FIXED :     rfmptr = "FIX"; break;
         case FAT$C_STREAM :    rfmptr = "STM"; break;
         case FAT$C_STMLF :     rfmptr = "STMLF"; break;
         case FAT$C_STMCR :     rfmptr = "STMCR"; break;
         case FAT$C_UNDEFINED : rfmptr = "UDF"; break;
         default : rfmptr = "?";
      }
      WatchThis (WATCHITM(rqptr), WATCH_RESPONSE,
#ifdef ODS_EXTENDED   
"FILE !AZ ODS:!UL rfm:!AZ ebk:!UL ffb:!UL (!AZ!@UQ bytes!AZ) rdt:!%D",
                 tkptr->FileName, rqptr->PathOds,
#else
"FILE !AZ rfm:!AZ ebk:!UL ffb:!UL (!AZ!@UQ bytes!AZ) rdt:!%D",
                 tkptr->FileName,
#endif /* ODS_EXTENDED */
                 rfmptr, fqptr->EndOfFileVbn, fqptr->FirstFreeByte,
                 fqptr->RecAttr.fat$b_rtype == FAT$C_VARIABLE ||
                    fqptr->RecAttr.fat$b_rtype == FAT$C_VFC ? "~" : "",
                 &fqptr->SizeInBytes64,
                 fqptr->SizeInBytes64 > __UINT32_MAX ? " >4GB!" : "",
                 &fqptr->RdtTime64);
   }

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

   if (rqptr->rqHeader.RangeBytePtr &&
       rqptr->rqHeader.RangeBytePtr->Total &&
       !rqptr->rqResponse.HeaderGenerated &&
       !tkptr->ContentHandlerFunction &&
       fqptr->RecAttr.fat$b_rtype != FAT$C_VARIABLE &&
       fqptr->RecAttr.fat$b_rtype != FAT$C_VFC)
   {
      /******************************/
      /* byte-range on non-VAR file */
      /******************************/

      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] = fqptr->SizeInBytes64 - 1;
         }
         else
         if (rbptr->Last[idx] < 0)
         {
            /* first byte a negative offset from end, last byte at EOF */
            rbptr->First[idx] = fqptr->SizeInBytes64 + rbptr->Last[idx];
            if (rbptr->First[idx] < 0) rbptr->First[idx] = 0;
            rbptr->Last[idx] = fqptr->SizeInBytes64 - 1;
         }
         else
         if (rbptr->Last[idx] >= fqptr->SizeInBytes64)
         {
            /* if the last byte is ambit make it at the EOF */
            rbptr->Last[idx] = fqptr->SizeInBytes64 - 1;
         }
         /* if the range still does not make sense then back out now */
         if (rbptr->Last[idx] <= rbptr->First[idx])
         {
            RangeValid = false;
            rbptr->Length = 0;
         }
         else
            rbptr->Length = rbptr->Last[idx] - rbptr->First[idx] + 1;

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

   if (Config.cfMisc.EntityTag)
   {
      FileGenerateEntityTag (tkptr->EntityTag, fqptr);
      if (rqptr->rqResponse.HttpVersion == HTTP_VERSION_1_1)
         strzcpy (rqptr->rqResponse.EntityTag, tkptr->EntityTag,
                  sizeof(rqptr->rqResponse.EntityTag));
   }

   if (!rqptr->rqResponse.HeaderGenerated &&
       !tkptr->ContentHandlerFunction)
   {
      /**************************/
      /* full response required */
      /**************************/

      if (!rqptr->AccountingDone++)
         InstanceGblSecIncrLong (&AccountingPtr->DoFileCount);

      /* variable-record length file size cannot be accurately determined */
      if (fqptr->RecAttr.fat$b_rtype == FAT$C_VARIABLE ||
          fqptr->RecAttr.fat$b_rtype == FAT$C_VFC)
         status = FileResponseHeader (rqptr, tkptr->ContentTypePtr,
                                      -1, &fqptr->RdtTime64);
      else
         status = FileResponseHeader (rqptr, tkptr->ContentTypePtr,
                                      fqptr->SizeInBytes64,
                                      &fqptr->RdtTime64);
      /* here status is a boolean */
      if (!status)
      {
         InstanceGblSecIncrLong (&AccountingPtr->DoFileNotModifiedCount);
         FileEnd (rqptr);
         return;
      }

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

   /******************************/
   /* stream-LF file conversion? */
   /******************************/

   if (Config.cfMisc.StreamLfConversionMaxKbytes &&
       (fqptr->RecAttr.fat$b_rtype == FAT$C_VARIABLE ||
        fqptr->RecAttr.fat$b_rtype == FAT$C_VFC) &&
       rqptr->rqPathSet.StmLF &&
       ConfigSameContentType (tkptr->ContentTypePtr, "text/", 5) &&
       !ConfigSameContentType (tkptr->ContentTypePtr, ConfigContentTypeSsi, -1))
   {
      /* divide by two to get the number of kilobytes (1024) in the file */
      if (!fqptr->SizeInBytes64 && (fqptr->SizeInBytes64 >> 10) <=
          Config.cfMisc.StreamLfConversionMaxKbytes)
         StmLfBegin (MapVmsPath(tkptr->FileName, rqptr),
                     tkptr->FileName,
                     tkptr->FileNameLength,
                     rqptr->PathOdsExtended,
                     rqptr->rqAuth.VmsUserProfileLength);
   }

   /********************/
   /* begin processing */
   /********************/

   /*
      Cache load is not initiated if this is not a stand-alone file request
      (i.e. not part of some other activity, e.g. a directory read-me file,
      if it is already being loaded via another request, if a "temporary"
      file ('DeleteOnClose'), or via requests with a VMS authentication
      profile attached.
   */
   if (CacheEnabled &&
       tkptr->CacheAllowed &&
       !rqptr->rqPathSet.NoCache &&
       !rqptr->rqPathSet.CacheNoFile &&
       !rqptr->rqCache.LoadCheck &&
       !rqptr->rqCache.NotUsable &&
       !tkptr->EscapeHtml &&
       !rqptr->DeleteOnClose &&
       !rqptr->rqAuth.VmsUserProfileLength &&
       (!rqptr->rqHeader.RangeBytePtr ||
        !rqptr->rqHeader.RangeBytePtr->Count) &&
       fqptr->SizeInBytes64 <= __INT32_MAX)
   {
      /* begin caching during file content read */
      rqptr->rqCache.LoadFromFile =
         CacheLoadBegin (rqptr, (int)fqptr->SizeInBytes64, NULL);
   }

   if (tkptr->ContentHandlerFunction)
   {
      /************************/
      /* buffer file contents */
      /************************/

      if (WATCHMOD (rqptr, WATCH_MOD_FILE))
         WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE, "!@UQ !UL",
                    &fqptr->SizeInBytes64, tkptr->FileContentsSizeMax);

      if (fqptr->SizeInBytes64 > tkptr->FileContentsSizeMax)
      {
         rqptr->rqResponse.HttpStatus = 500;
         ErrorGeneral (rqptr, ErrorBufferFileMaxBytes, FI_LI);
         FileEnd (rqptr);
         return;
      }
      /* make buffer size a multiple of 512 byte blocks (for block I/O) */
      BufferSize = (fqptr->SizeInBytes64 / 512);
      if (fqptr->SizeInBytes64 % 512) BufferSize++;
      BufferSize *= 512;

      /* allocate a buffer */
      rqptr->FileContentPtr = fcptr = (FILE_CONTENT*)
         VmGetHeap (rqptr, sizeof(FILE_CONTENT) + BufferSize);
      fcptr->ContentSize = BufferSize;
      /* buffer space immediately follows the structured storage */
      fcptr->ContentPtr = (char*)fcptr + sizeof(FILE_CONTENT);

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

      fcptr->CdtTime64 = fqptr->CdtTime64;
      fcptr->RdtTime64 = fqptr->RdtTime64;

      fcptr->UicGroup = (fqptr->AtrUic & 0x0fff0000) >> 16;
      fcptr->UicMember = (fqptr->AtrUic & 0x0000ffff);
      fcptr->Protection = fqptr->AtrFpro;

      /* set the content structure handler to the supplied function */
      rqptr->FileContentPtr->ContentHandlerFunction = 
         tkptr->ContentHandlerFunction;

      /* none of these little tricks, just get the raw file! */
      tkptr->PreTagFileContents = tkptr->EscapeHtml = false;
   }
   else
   {
      /* network writes are checked for success, fudge the first one! */
      rqptr->NetIoPtr->WriteStatus = SS$_NORMAL;
   }

   /*******************/
   /* 'open' the file */
   /*******************/

   /*
      Fill out the FIB, override any exclusive locking - requires SYSPRV.
      Access to this file without SYSPRV has already been established via
      the ACP QIO that effectively performs the protection checks.
   */
   fqptr->Fib.fib$l_acctl = FIB$M_SEQONLY | FIB$M_NOLOCK;
   fqptr->Fib.fib$w_nmctl = FIB$M_FINDFID;

   sys$setprv (1, &SysPrvMask, 0, 0);

   status = sys$qio (EfnNoWait, fqptr->AcpChannel,
                     IO$_ACCESS | IO$M_ACCESS,
                     &fqptr->IOsb, &FileAccessAst, rqptr,
                     &fqptr->FibDsc, 0, 0, 0, 0, 0);

   sys$setprv (0, &SysPrvMask, 0, 0);

   if (VMSnok (status))
   {
      /* let the AST routine handle it! */
      fqptr->IOsb.Status = status;
      SysDclAst (FileAccessAst, rqptr);
   }
} 

/*****************************************************************************/
/*
Match the supplied file name (generally needs to be a NOCONCEAL sys$search()
result file name as provided by OdsSearchNoConceal() with the 'name=value' pair
'name' string of a "SET /path/* charset=(pattern,charset)" rule.  If a
successful match return the corresponding 'value' otherwise return NULL.
*/

char* FileSetCharset
(
REQUEST_STRUCT *rqptr,
char *FileName
)
{
   static char  Charset [64];

   int  status;
   char  *cptr;
   char  FileSpec [128];

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

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "FileSetCharset() !&Z !&Z",
                 rqptr->rqPathSet.CharsetPtr, FileName);

   if (!(cptr = rqptr->rqPathSet.CharsetPtr)) return (NULL);
   if (cptr[0] != '(') return (cptr);
   if (!FileName) return (NULL);

   for (;;)
   {
      status = StringParseNameValue (&cptr, false,
                                     FileSpec, sizeof(FileSpec),
                                     Charset, sizeof(Charset));
      if (VMSnok (status)) return (NULL);
      if (StringMatch (NULL, FileName, FileSpec)) return (Charset);
   }
}

/****************************************************************************/
/*
Generate a response header suitable for the file being returned.
This may be a 304 (not modified) if appropriate.
This function is also used by the CACHE.C module.
Return /true/ to continue to send the file, /false/ if not to.
*/

BOOL FileResponseHeader
(
REQUEST_STRUCT *rqptr,
char *ContentType,
int64 ContentLength,
ulong *RdtTime64Ptr
)
{
   int  idx, status;
   int64  RangeContentLength;
   ushort  Length;
   char  *cptr, *sptr, *zptr,
         *ContentTypePtr;
   char  Buffer [256],
         TypeBuffer [256];
   RANGE_BYTE  *rbptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE, "FileResponseHeader()");

   /****************/
   /* entity match */
   /****************/

   /* if entity precondition fails (generated 412) then just return */
   if (!ResponseEntityMatch (rqptr, rqptr->rqResponse.EntityTag))
      return (true);

   if (rqptr->rqTime.IfModifiedSinceTime64 &&
       !rqptr->rqHeader.PragmaNoCache &&
       !rqptr->DeleteOnClose)
   {
      /*********************/
      /* if modified since */
      /*********************/

      if (!HttpIfModifiedSince (rqptr, RdtTime64Ptr, ContentLength))
      {
         if (WATCHING (rqptr, WATCH_RESPONSE))
             WatchThis (WATCHITM(rqptr), WATCH_RESPONSE, "NOT modified");
         return (false);
      }
   }

   if (rqptr->rqTime.IfUnModifiedSinceTime64 &&
       !rqptr->DeleteOnClose)
   {
      /*************************/
      /* if NOT modified since */
      /*************************/

      if (HttpIfUnModifiedSince (rqptr, RdtTime64Ptr))
      {
         if (WATCHING (rqptr, WATCH_RESPONSE))
             WatchThis (WATCHITM(rqptr), WATCH_RESPONSE, "NOT modified");
         return (false);
      }
   }

   rbptr = rqptr->rqHeader.RangeBytePtr;

   if (rqptr->rqHeader.QueryStringLength &&
       to_lower(rqptr->rqHeader.QueryStringPtr[0]) == 'h' &&
       strsame (rqptr->rqHeader.QueryStringPtr, "httpd=content", 13))
   {
      /* request-specified content-type (default to plain-text) */
      if (rbptr) rbptr->Count = 0;  /* cancel any byte range */
      if (strsame (rqptr->rqHeader.QueryStringPtr+13, "&type=", 6))
      {
         zptr = (sptr = TypeBuffer) + sizeof(TypeBuffer)-1;
         for (cptr = rqptr->rqHeader.QueryStringPtr + 19;
              *cptr && *cptr != '&' && sptr < zptr;
              *sptr++ = *cptr++);
         *sptr = '\0';
         StringUrlDecode (ContentTypePtr = TypeBuffer);
      }
      else
         ContentTypePtr = "text/plain";
   }
   else
      ContentTypePtr = ContentType;

   if (rbptr && rbptr->Count &&
       rqptr->rqTime.IfRangeBeginTime64 &&
       !rqptr->NotFromCache &&
       !rqptr->DeleteOnClose)
   {
      /*********************/
      /* if range modified */
      /*********************/

      /* cancels range data in the request header if file has been modified */
      HttpIfRange (rqptr, RdtTime64Ptr);
   }

   if (!rbptr || !rbptr->Count)
   {
      /*********************/
      /* standard response */
      /*********************/

      ResponseHeader (rqptr, 200, ContentTypePtr,
                      ContentLength, RdtTime64Ptr, NULL);
      return (true);
   }

   /***********************/
   /* byte range response */
   /***********************/

   if (rbptr->Count == 1)
   {
      /* single byte-range requested */
      FaoToBuffer (Buffer, sizeof(Buffer), NULL,
                   "Content-Range: bytes !@UQ-!@UQ/!@UQ\r\n",
                   &rbptr->First[0], &rbptr->Last[0], &ContentLength);
      ResponseHeader (rqptr, 206, ContentTypePtr,
                      rbptr->Last[0] - rbptr->First[0] + 1,
                      RdtTime64Ptr, Buffer);
      return (true);
   }

   /* multiple byte-ranges requested */
   rqptr->rqResponse.MultipartBoundaryPtr = VmGetHeap (rqptr, 32+1);
   FaoToBuffer (rqptr->rqResponse.MultipartBoundaryPtr, 32+1, &Length,
                "!32&H", &rqptr->Md5HashPath);

   /* calculate the content-length */
   RangeContentLength = 0;
   for (idx = 0; idx < rbptr->Count; idx++)
   {
      FaoToBuffer (Buffer, sizeof(Buffer), &Length,
"!AZ--!AZ\r\n\
Content-Type: !AZ\r\n\
Range: bytes !@UQ-!@UQ/!@UQ\r\n\
\r\n",
                   idx ? "\r\n" : "",
                   rqptr->rqResponse.MultipartBoundaryPtr, ContentTypePtr,
                   &rbptr->First[idx], &rbptr->Last[idx], &ContentLength);
      RangeContentLength += Length + rbptr->Last[idx] - rbptr->First[idx] + 1;
   }
   RangeContentLength += 32 + 8;

   FaoToBuffer (Buffer, sizeof(Buffer), NULL,
                "multipart/byteranges; boundary=!AZ",
                rqptr->rqResponse.MultipartBoundaryPtr);
   ResponseHeader (rqptr, 206, Buffer, RangeContentLength,
                   RdtTime64Ptr, NULL);

   return (true);
} 

/*****************************************************************************/
/*
End of file transfer, successful or otherwise.
*/

FileEnd (REQUEST_STRUCT *rqptr)

{
   int  status;
   FILE_QIO  *fqptr;
   FILE_TASK  *tkptr;
   FILE_CONTENT  *fcptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "FileEnd() !&F !&A", &FileEnd, rqptr->FileTaskPtr);

   tkptr = rqptr->FileTaskPtr;

   fqptr = &tkptr->FileOds.FileQio;

   if (rqptr->DeleteOnClose && (fqptr->AcpChannel || fqptr->QioChannel))
   {
      /* delete-on-close happens VERY infrequently */
      sys$setprv (1, &SysPrvMask, 0, 0);
      status = sys$qiow (EfnWait,
                         fqptr->AcpChannel ? fqptr->AcpChannel :
                                             fqptr->QioChannel,
                         IO$_DELETE | IO$M_DELETE,
                         &fqptr->IOsb, 0, 0,
                         &fqptr->FibDsc, &fqptr->FileNameDsc, 0, 0, 0, 0);
      sys$setprv (0, &SysPrvMask, 0, 0);
      if (WATCHMOD (rqptr, WATCH_MOD_FILE))
         WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE, "IO$_DELETE !&S !&S",
                    status, fqptr->IOsb.Status);
      if (VMSok (status)) status = fqptr->IOsb.Status;
      if (VMSnok (status)) ErrorNoticed (rqptr, status, NULL, FI_LI);
   }

   if (fqptr->AcpChannel)
   {
      /* the file has only had it's attributes read */
      sys$dassgn (fqptr->AcpChannel);
      fqptr->AcpChannel = 0;
   }
   else
   if (fqptr->QioChannel)
   {
      /* file has been accessed (contents 'open'ed and read) */
      status = sys$qiow (EfnWait, fqptr->QioChannel, IO$_DEACCESS,
                         &fqptr->IOsb, 0, 0,
                         &fqptr->FibDsc, 0, 0, 0, 0, 0);
      if (WATCHMOD (rqptr, WATCH_MOD_FILE))
         WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE, "IO$_DEACCESS !&S !&S",
                    status, fqptr->IOsb.Status);
      if (fqptr->IOsb.Status && VMSnok (fqptr->IOsb.Status))
         ErrorNoticed (rqptr, fqptr->IOsb.Status, NULL, FI_LI);
      if (VMSnok (status))
         ErrorNoticed (rqptr, status, NULL, FI_LI);

      sys$dassgn (fqptr->QioChannel);
      fqptr->QioChannel = 0;
   }

   /* if the file was being cached at the same time */
   if (rqptr->rqCache.LoadFromFile)
   {
      if (rqptr->FileContentPtr)
      {
         /* copy from the cache buffer */
         fcptr = rqptr->FileContentPtr;
         if (fcptr->ContentLength <= rqptr->rqCache.ContentLength)
         {
            memcpy (fcptr->ContentPtr,
                    rqptr->rqCache.ContentPtr,
                    rqptr->rqCache.ContentLength);
            fcptr->ContentLength = rqptr->rqCache.ContentLength;
            /* null terminate, it's usually text! */
            fcptr->ContentPtr[fcptr->ContentLength] = '\0';
         }
      }

      CacheLoadEnd (rqptr);
   }

   /* release internal RMS parse structures */
   OdsParseRelease (&tkptr->FileOds);

#if WATCH_MOD
   HttpdCheckPriv (FI_LI);
#endif /* WATCH_MOD */

   if (tkptr->NoSuchFileFunction)
   {
      /* file could not have been found, declare the appropriate handler */
      SysDclAst (tkptr->NoSuchFileFunction, rqptr);
   }
   else
   if (rqptr->FileContentPtr)
   {
      /* next task gets control once the file has been content-handled */
      rqptr->FileContentPtr->NextTaskFunction = tkptr->NextTaskFunction;

      /* file contents loaded, now process using the specified handler */
      SysDclAst (rqptr->FileContentPtr->ContentHandlerFunction, rqptr);
      rqptr->FileContentPtr->ContentHandlerFunction = NULL;
   }
   else
   if (tkptr->PreTagEndFileContents)
   {
      /* success, encapsulating a file, add the end tag */
      NetWriteBuffered (rqptr, tkptr->NextTaskFunction, "</pre>\n", 7);
   }
   else
   {
      /* success, declare the next task */
      SysDclAst (tkptr->NextTaskFunction, rqptr);
   }

   rqptr->FileTaskPtr = NULL;

   VmFreeFromHeap (rqptr, tkptr, FI_LI);
} 

/*****************************************************************************/
/*
The file access $QIO (the 'open') has completed.  Check status.
Ensure any output buffer contents are flushed.
*/

FileAccessAst (REQUEST_STRUCT *rqptr)

{
   int  status;
   FILE_QIO  *fqptr;
   FILE_TASK  *tkptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "FileAccessAst() !&F !UL !UL !&S",
                 &FileAccessAst, STR_DSC_LEN(&rqptr->NetWriteBufferDsc),
                 rqptr->FileTaskPtr->FileOds.FileQio.IOsb.Count,
                 rqptr->FileTaskPtr->FileOds.FileQio.IOsb.Status);

   tkptr = rqptr->FileTaskPtr;
   fqptr = &tkptr->FileOds.FileQio;

   if (VMSnok (fqptr->IOsb.Status))
   {
      if (rqptr->rqCache.LoadFromFile)
         rqptr->rqCache.LoadStatus = fqptr->IOsb.Status;
      rqptr->rqResponse.ErrorTextPtr = MapVmsPath (tkptr->FileName, rqptr);
      rqptr->rqResponse.ErrorOtherTextPtr = tkptr->FileName;
      ErrorVmsStatus (rqptr, fqptr->IOsb.Status, FI_LI);
      FileEnd (rqptr);
      return;
   }

   /* the file access is on the channel originally assigned for ACP-QIO */
   fqptr->QioChannel = fqptr->AcpChannel;
   fqptr->AcpChannel = 0;

   if (tkptr->PreTagFileContents)
   {
      tkptr->PreTagEndFileContents = true;
      NetWriteBuffered (rqptr, FileNextBlocks, "<pre>", 5);
      return;
   }

   FileNextBlocks (rqptr);
}

/*****************************************************************************/
/*
QIO a read of blocks from the file.  When the read completes call
FileNextBlocksAst() function to post-process the read and/or send the data to
the client.  Don't bother to test any status here, the AST routine will do
that!
*/ 

FileNextBlocks (REQUEST_STRUCT *rqptr)

{
   int  idx, status;
   ushort  Length;
   uint  BufferSize;
   ulong  EmulBlock;
   char  *bptr;
   FILE_CONTENT  *fcptr;
   FILE_QIO  *fqptr;
   FILE_TASK  *tkptr;
   RANGE_BYTE  *rbptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                "FileNextBlocks() !&F !&S !UL !&B",
                 FileNextBlocks, rqptr->NetIoPtr->WriteStatus,
                 rqptr->FileTaskPtr->FileOds.FileQio.BlockNumber,
                 rqptr->FileTaskPtr->FileOds.FileQio.EndOfFile);

   if (rqptr->RequestState >= REQUEST_STATE_ABORT)
      rqptr->NetIoPtr->WriteStatus = SS$_ABORT;

   tkptr = rqptr->FileTaskPtr;
   fcptr = rqptr->FileContentPtr;
   fqptr = &tkptr->FileOds.FileQio;

   if (!fcptr && VMSnok (rqptr->NetIoPtr->WriteStatus))
   {
      /* network write has failed (delivered via AST), bail out now */
      FileEnd (rqptr);
      return;
   }

   if (fqptr->EndOfFile)
   {
      /* calculated EOF */
      fqptr->EndOfFile = false;
      fqptr->IOsb.Status = SS$_ENDOFFILE;
      SysDclAst (FileNextBlocksAst, rqptr);
      return;
   }

   if (!fqptr->BlockNumber)
   {
      /**************/
      /* initialize */
      /**************/

      if (STR_DSC_LEN(&rqptr->NetWriteBufferDsc))
      {
         /* need exclusive use, flush the current contents */
         NetWriteFullFlush (rqptr, FileNextBlocks);
         return;
      }

      /* ensure we have an initialised buffer (round number of TCP segments) */
      /* any change needs to be replicated in CacheNext() */
      BufferSize = (rqptr->NetIoPtr->TcpMaxQio / 512) * 512;
//BufferSize = (65535 / 512) * 512;
      if (WATCHING (rqptr, WATCH_NETWORK))
         WatchThis (WATCHITM(rqptr), WATCH_NETWORK, "BUFFER qio:!UL size:!UL",
                    rqptr->NetIoPtr->TcpMaxQio, BufferSize);
      StrDscIfNotBegin (rqptr, &rqptr->NetWriteBufferDsc, BufferSize);

      fqptr->BlockNumber = 1;

      if (rqptr->rqHeader.RangeBytePtr &&
          rqptr->rqHeader.RangeBytePtr->Count)
      {
         /* returning a byte range within the file (partial content) */
         rbptr = rqptr->rqHeader.RangeBytePtr;
         idx = rbptr->Index;
         fqptr->BlockNumber += rbptr->First[idx] / 512;
         rbptr->Offset = rbptr->First[idx] % 512;
         rbptr->Length = rbptr->Last[idx] - rbptr->First[idx] + 1;
         rbptr->Remaining = rbptr->Length;

         if (rbptr->Count > 1)
         {
            /* returning 'multipart/byteranges' range content */
            char Buffer [256];
            FaoToBuffer (Buffer, sizeof(Buffer), &Length,
"!AZ--!AZ\r\n\
Content-Type: !AZ\r\n\
Range: bytes !@UQ-!@UQ/!@UQ\r\n\
\r\n",
                         rbptr->Index ? "\r\n" : "",
                         rqptr->rqResponse.MultipartBoundaryPtr,
                         tkptr->ContentTypePtr,
                         &rbptr->First[idx], &rbptr->Last[idx],
                         &fqptr->SizeInBytes64);
            /* synchronous network write (just for the convenience of it!) */
            NetWrite (rqptr, NULL, Buffer, Length);
         }

         if (WATCHMOD (rqptr, WATCH_MOD_FILE))
            WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                       "range !UL-!UL(!UL) vbn:!UL off:!UL rem:!UL",
                       rbptr->First[idx], rbptr->Last[idx],
                       rbptr->Length, fqptr->BlockNumber,
                       rbptr->Offset, rbptr->Remaining);
      }

      if (fcptr)
      {
         /* file content buffer (perhaps concurrently with cache buffer) */
         fcptr->CurrentPtr = fcptr->ContentPtr;
         fcptr->ContentRemaining = fcptr->ContentSize;
         fcptr->ContentLength = 0;
      }
      else
      if (!rqptr->rqCache.LoadFromFile)
      {
         /* initialize output buffer (round number of TCP segments) */
         BufferSize = (rqptr->NetIoPtr->TcpMaxQio / 512) * 512;
         if (WATCHMOD (rqptr, WATCH_MOD_FILE))
            WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE, "2 !@UQ !UL",
                       &fqptr->SizeInBytes64, BufferSize);
         StrDscBegin (rqptr, &rqptr->NetWriteBufferDsc, BufferSize);
      }

      if (!fqptr->SizeInBytes64)
      {
         /* empty file */
         fqptr->IOsb.Status = SS$_ENDOFFILE;
         SysDclAst (FileNextBlocksAst, rqptr);
         return;
      }
   }
   else
   {
      /********************/
      /* subsequent reads */
      /********************/

      fqptr->BlockNumber += fqptr->BufferSize >> 9;
   }

   /**************/
   /* queue read */
   /**************/

   if (rqptr->rqCache.LoadFromFile)
   {
      /* populating a cache buffer, load it progressively */
      fqptr->BufferPtr = rqptr->rqCache.CurrentPtr;
      if (rqptr->rqCache.ContentRemaining > 0xfe00)
         BufferSize = 0xfe00;
      else
      {
         BufferSize = rqptr->rqCache.ContentRemaining;
         fqptr->EndOfFile = true;
      }
   }
   else
   if (fcptr)
   {
      /* populating a file contents buffer, load it progressively */
      fqptr->BufferPtr = fcptr->CurrentPtr;
      if (fcptr->ContentRemaining > 0xfe00)
         BufferSize = 0xfe00;
      else
      {
         BufferSize = fcptr->ContentRemaining;
         fqptr->EndOfFile = true;
      }
   }
   else
   {
      /* standard output buffer, make it a round number of 512 byte blocks */
      BufferSize = STR_DSC_SIZE(&rqptr->NetWriteBufferDsc) & 0xfe00;
      fqptr->BufferPtr = STR_DSC_PTR(&rqptr->NetWriteBufferDsc);
   }

   /* adjust the buffer size for where we have been told the EOF to be */
   if (((fqptr->BlockNumber-1)<<9) + BufferSize > fqptr->SizeInBytes64)
   {
      BufferSize = fqptr->SizeInBytes64 - ((fqptr->BlockNumber-1)<<9);
      fqptr->EndOfFile = true;
   }
   fqptr->BufferSize = BufferSize;

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "BufferSize:!UL", BufferSize);

   /*
      Documented in the VMS I/O Users Guide section entitled "Disk Function
      Codes" ... "P2--The number of bytes that are to be read from the disk,
      or written from memory to the disk. An even number must be specified if
      the controller is an RK611, RL11, RX211, or UDA50".
      Well, make sure we ask for an even number of bytes!
      Thanks to Dave Holland for demonstrating this bug on SIMH, and to
      Mark Pizzolato from the SIMH mailing list for pointing out the above.
   */
   if (BufferSize & 1)
   {
      BufferSize++;
      fqptr->AdjustBuffer = 1;
   }
   else
      fqptr->AdjustBuffer = 0;

   bptr = fqptr->BufferPtr;
   if (fqptr->RecAttr.fat$b_rtype == FAT$C_VARIABLE ||
       fqptr->RecAttr.fat$b_rtype == FAT$C_VFC)
   {
      /*
         Records in VARiable format files need to be word aligned.
         If this is on an odd boundary then push the buffer forward one
         byte in the storage ('elbow-room' in memory allocation means this
         is not an issue).  FileVariableRecord() detects and undoes this.
         This should only be necessary when when using a cache or content
         buffer as the FileVariableRecord() processing can create odd
         numbers of bytes when turning them into stream content.
      */
      if ((int)bptr & 1) bptr++;
   }

   status = sys$qio (EfnNoWait, fqptr->QioChannel,
                     IO$_READVBLK, &fqptr->IOsb,
                     FileNextBlocksAst, rqptr,
                     bptr, BufferSize, fqptr->BlockNumber,
                     0, 0, 0);
   if (VMSok (status)) return;

   /* let the AST routine handle it! */
   fqptr->IOsb.Status = status;
   SysDclAst (FileNextBlocksAst, rqptr);
}

/*****************************************************************************/
/*
The QIO read of blocks from the file has completed.  Post-process and/or queue
a network write to the client.  When the network  write completes it will call
the function FileNextBlocks() to queue a read  of the next series of blocks.
*/ 

FileNextBlocksAst (REQUEST_STRUCT *rqptr)

{
   int  status, bcnt;
   char  *bptr, *cptr, *sptr, *zptr;
   FILE_CONTENT  *fcptr;
   FILE_TASK  *tkptr;
   FILE_QIO  *fqptr;
   RANGE_BYTE  *rbptr;

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

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
   {
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
"FileNextBlocksAst() !&F AcpIOsb.Status:!&S \
QioBlockNumber:!UL QioBufferSize:!UL IOsb.Count:!UL end:!&B %!&M",
                 FileNextBlocksAst,
                 rqptr->FileTaskPtr->FileOds.FileQio.IOsb.Status,
                 rqptr->FileTaskPtr->FileOds.FileQio.BlockNumber,
                 rqptr->FileTaskPtr->FileOds.FileQio.BufferSize,
                 rqptr->FileTaskPtr->FileOds.FileQio.IOsb.Count,
                 rqptr->FileTaskPtr->FileOds.FileQio.EndOfFile,
                 rqptr->FileTaskPtr->FileOds.FileQio.IOsb.Status);
   }

   tkptr = rqptr->FileTaskPtr;
   fcptr = rqptr->FileContentPtr;
   fqptr = &tkptr->FileOds.FileQio;

   if (rqptr->RequestState >= REQUEST_STATE_ABORT)
      tkptr->FileOds.FileQio.IOsb.Status = SS$_ABORT;

   tkptr->NextBlocksTickSecond = HttpdTickSecond;

   if (VMSnok (fqptr->IOsb.Status))
   {
      if (fqptr->IOsb.Status == SS$_ENDOFFILE)
      {
         /***************/
         /* end-of-file */
         /***************/

         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 */
               fqptr->BlockNumber = 0;
               SysDclAst (FileNextBlocks, rqptr);
               return;
            }
            else
            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);
            }
         }
         else
         if (rqptr->rqCache.LoadFromFile)
            rqptr->rqCache.LoadStatus = fqptr->IOsb.Status;

         if (!fcptr)
         {
            /* reset the standard output buffer */
            StrDscNoContent (&rqptr->NetWriteBufferDsc);
         }

         FileEnd (rqptr);
         return;
      }

      /**************/
      /* read error */
      /**************/

      if (rqptr->rqCache.LoadFromFile)
         rqptr->rqCache.LoadStatus = fqptr->IOsb.Status;
      rqptr->rqResponse.ErrorTextPtr = MapVmsPath (tkptr->FileName, rqptr);
      rqptr->rqResponse.ErrorOtherTextPtr = tkptr->FileName;
      ErrorVmsStatus (rqptr, fqptr->IOsb.Status, FI_LI);
      FileEnd (rqptr);
      return;
   }

   /******************/
   /* process blocks */
   /******************/

   /* get the count from the I/O status block (adjusted as necessary) */
   fqptr->IOsb.Count -= fqptr->AdjustBuffer;
   fqptr->BufferCount = fqptr->IOsb.Count;

   /* if NOT transferring the content EXACTLY as it is on-disk */
   if (rqptr->rqPathSet.ResponseVarRecord != FILE_VAR_ASIS)
   {
      /* if not 'binary content' in the blocks then massage */
      if (fqptr->RecAttr.fat$b_rtype == FAT$C_VARIABLE ||
          fqptr->RecAttr.fat$b_rtype == FAT$C_VFC)
         FileVariableRecord (rqptr);
      else
      if (fqptr->RecAttr.fat$b_rtype == FAT$C_FIXED &&
          fqptr->RecAttr.fat$b_rattrib & FAT$M_NOSPAN)
         FileFixedRecordNoSpan (rqptr);
   }

   /* ensure leftover space in a reused buffer is zeroed (just to be tidy) */
   if (fqptr->BlockNumber > 1)
      if (fqptr->BufferSize - fqptr->BufferCount > 0)
         memset (fqptr->BufferPtr + fqptr->BufferCount, 0,
                 fqptr->BufferSize - fqptr->BufferCount);

   if (rqptr->FileTaskPtr->EscapeHtml)
   {
      /* queue a network write to the client, AST to FileNextBlocks() */
      STR_DSC_LEN(&rqptr->NetWriteBufferDsc) = fqptr->BufferCount;

      FileWriteBufferEscapeHtml (rqptr, FileNextBlocks);
      return;
   }

   bptr = fqptr->BufferPtr;
   bcnt = fqptr->BufferCount;

   if (rqptr->rqCache.LoadFromFile)
   {
      /* populating a cache buffer, possibly also a file contents buffer */
      rqptr->rqCache.ContentRemaining -= bcnt;
      rqptr->rqCache.ContentLength += bcnt;
      rqptr->rqCache.CurrentPtr += bcnt;
      if (WATCHMOD (rqptr, WATCH_MOD_FILE))
         WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE, "!UL",
                    rqptr->rqCache.ContentRemaining);
      if (!rqptr->rqCache.ContentRemaining)
         fqptr->EndOfFile = true;
      else
      if (rqptr->rqCache.ContentRemaining < 0)
      {
         /*
            This is not supposed to happen!
            Possibly in between getting the file size and opening and
            reading this far the file has been extended or rewritten.
            Shouldn't happen often enough to be a worry!
         */
         rqptr->rqResponse.ErrorTextPtr = MapVmsPath (tkptr->FileName, rqptr);
         rqptr->rqResponse.ErrorOtherTextPtr = tkptr->FileName;
         ErrorVmsStatus (rqptr, RMS$_RTB, FI_LI);
         FileEnd (rqptr);
         return;
      }
      if (!fcptr)
      {
         /* not populating file content buffer, write cache buffer to client */
         NetWrite (rqptr, FileNextBlocks, bptr, bcnt);
         return;
      }
      /* filling the cache/contents buffer, just get more file data */
      SysDclAst (FileNextBlocks, rqptr);
      return;
   }

   if (fcptr)
   {
      /* populating a file contents buffer, but not a cache buffer */
      fcptr->ContentRemaining -= bcnt;
      fcptr->ContentLength += bcnt;
      fcptr->CurrentPtr += bcnt;
      /* just get more file data */
      SysDclAst (FileNextBlocks, rqptr);
      return;
   }

   if (rqptr->rqHeader.RangeBytePtr &&
       rqptr->rqHeader.RangeBytePtr->Remaining)
   {
      /* returning a byte-range within the file (partial content) */
      rbptr = rqptr->rqHeader.RangeBytePtr;
      if (rbptr->Offset)
      {
         /* first block read, first byte won't necessarily be at the start */
         bptr += rbptr->Offset;
         bcnt -= rbptr->Offset;                   
         rbptr->Offset = 0;
      }
      /* if the range is less than what was read then discard the rest */
      if (bcnt > rbptr->Remaining) bcnt = rbptr->Remaining;
      rbptr->Remaining -= bcnt;
      /* if no range remaining it's an effective EOF for block I/O! */
      if (!rbptr->Remaining) fqptr->EndOfFile = true;

      if (WATCHMOD (rqptr, WATCH_MOD_FILE))
         WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                    "range !UL-!UL(!UL) vbn:!UL off:!UL rem:!UL",
                    rbptr->First[rbptr->Index],
                    rbptr->Last[rbptr->Index],
                    rbptr->Length,
                    fqptr->BlockNumber,
                    rbptr->Offset,
                    rbptr->Remaining);
   }

   /* write raw/massaged data to client */
   if (VMSok (rqptr->NetIoPtr->WriteStatus))
      NetWrite (rqptr, FileNextBlocks, bptr, bcnt);
   else
      SysDclAst (FileNextBlocks, rqptr);
}

/*****************************************************************************/
/*
Create an in situ stream buffer of characters out of disk virtual block
variable-length records where each record is now terminated by

1)  the default - newline character (LF)
    (i.e. turn variable-length into stream-LF on the fly).

2)  with a SET path mapping

    i)  SET /path response=VAR=CRLF    terminate record with <CR><LF>
   ii)  SET /path response=VAR=LF      terminate record with <LF> (as default)
  iii)  SET /path response=VAR=NONE    do not add carriage control
   iv)  SET /path response=var         revert to default

The word (two byte) record size allows sufficient space to support the single
byte <LF> and two byte <CR><LF> carriage-control.

Handles block spanning and non-spanning, LSB and MSB record length word.  In
other words - all variations (hopefully!)
*/ 

FileVariableRecord (REQUEST_STRUCT *rqptr)

{
   BOOL  MsbRcw, NoSpan;
   int  bcnt;
   ushort  VfcSize, MaxRecordSize, rcnt, recnt;
   char  *bptr, *cptr, *sptr, *zptr;
   FILE_QIO  *fqptr;
   FILE_TASK  *tkptr;
                         
   /*********/
   /* begin */
   /*********/

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE, "FileVariableRecord()");

   tkptr = rqptr->FileTaskPtr;
   fqptr = &tkptr->FileOds.FileQio;

   bptr = fqptr->BufferPtr;
   bcnt = fqptr->BufferCount;
   MsbRcw = fqptr->RecAttr.fat$b_rattrib & FAT$M_MSBRCW;
   NoSpan = fqptr->RecAttr.fat$b_rattrib & FAT$M_NOSPAN;
   VfcSize = fqptr->RecAttr.fat$b_vfcsize;
   MaxRecordSize = fqptr->RecAttr.fat$w_maxrec;
   recnt = rcnt = fqptr->RecordCount;

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
"NoSpan:!&B MsbRcw:!&B VfcSize:!UL MaxRecordSize:!UL rcnt:!UL \
RecordSize:!UL aligned:!&B ResponseVarRecord:!UL",
                 NoSpan, MsbRcw, VfcSize, MaxRecordSize,
                 rcnt, fqptr->RecordSize, (int)bptr & 1,
                 rqptr->rqPathSet.ResponseVarRecord);

   if (WATCHING (rqptr, WATCH_RESPONSE))
   {
      if (!tkptr->WatchVarRecordCC)
      {
         tkptr->WatchVarRecordCC = true;
         switch (rqptr->rqPathSet.ResponseVarRecord)
         {
            case FILE_VAR_CRLF : cptr = "<CR><LF>"; break;
            case FILE_VAR_LF   : cptr = "<LF>"; break;
            case FILE_VAR_NONE : cptr = "NONE"; break;
            default : if (rqptr->rqPathSet.ResponseVarRecord)
                         cptr = "(unknown)";
                      else
                         cptr = "DEFAULT (<LF>)";
         }
         WatchThis (WATCHITM(rqptr), WATCH_RESPONSE,
                    "RFM:VAR carriage control !AZ", cptr);
      }
   }

   zptr = (cptr = sptr = bptr) + bcnt;
   /* see note about VAR record alignment in FileNextBlocks() above */
   if ((int)bptr & 1) cptr++;

   while (cptr < zptr)
   {
      if (WATCHMOD (rqptr, WATCH_MOD_FILE))
         if (rcnt || (cptr > sptr && cptr > bptr+1))
            WatchDataFormatted ("!5ZL |!#AZ|\n",
               rcnt, rcnt <= zptr-cptr ? rcnt : zptr-cptr, cptr);

      if (VfcSize && rcnt && rcnt == fqptr->RecordSize)
      {
         /* adjust for any fixed-length control on start of new record */
         cptr += VfcSize;
         rcnt -= VfcSize;
         if (!rcnt)
         {
            /* ensure this last record has carriage control */
            if (rqptr->rqPathSet.ResponseVarRecord == FILE_VAR_CRLF)
            {
               if (!recnt || (recnt >= 2 && *(USHORTPTR)(sptr-2) != '\r\n'))
               {
                  *(USHORTPTR)sptr = '\r\n';
                  sptr += 2;
               }
            }
            else
            if (rqptr->rqPathSet.ResponseVarRecord != FILE_VAR_NONE)
               if (!recnt || (recnt >= 1 && *(sptr-1) != '\n'))
                  *sptr++ = '\n';
         }
      }

      /* copy the contents of this record */
      while (rcnt && cptr < zptr)
      {
         rcnt--;
         *sptr++ = *cptr++;
      }

      /* if no data left in the buffer */
      if (cptr >= zptr)
      {
         /* if still some data to go in this record */
         if (rcnt) break;

         /* ensure this last record has carriage control */
         if (rqptr->rqPathSet.ResponseVarRecord == FILE_VAR_CRLF)
         {
            if (!recnt || (recnt >= 2 && *(USHORTPTR)(sptr-2) != '\r\n'))
            {
               *(USHORTPTR)sptr = '\r\n';
               sptr += 2;
            }
         }
         else
         if (rqptr->rqPathSet.ResponseVarRecord != FILE_VAR_NONE)
            if (!recnt || (recnt >= 1 && *(sptr-1) != '\n'))
               *sptr++ = '\n';

         break;
      }

      /* step to an even byte boundary */
      if ((int)cptr & 1) cptr++;

      /* if no-span and insufficient space for a record left in this block */
      if (NoSpan && 510 - ((int)cptr & 0x1ff) < MaxRecordSize)
      {
         /* step to the start of the next block */
         cptr = ((int)cptr & ~0x1ff) + 512;
      }

      /* if no data left in the buffer */
      if (cptr >= zptr)
      {
         /* ensure this previous record has carriage control */
         if (rqptr->rqPathSet.ResponseVarRecord == FILE_VAR_CRLF)
         {
            if (!recnt || (recnt >= 2 && *(USHORTPTR)(sptr-2) != '\r\n'))
            {
               *(USHORTPTR)sptr = '\r\n';
               sptr += 2;
            }
         }
         else
         if (rqptr->rqPathSet.ResponseVarRecord != FILE_VAR_NONE)
            if (!recnt || (recnt >= 1 && *(sptr-1) != '\n'))
               *sptr++ = '\n';

         break;
      }

      /* get the new record count */
      rcnt = *(USHORTPTR)cptr;

      /* if MSB then swap the bytes */
      if (MsbRcw) rcnt = (rcnt >> 8) | (rcnt << 8);
      cptr += sizeof(ushort);

      /* note the current record count */
      recnt = rcnt;

      /* note the original size of this record */
      fqptr->RecordSize = rcnt;

      /* ensure previous record has carriage control (after count retrieved!) */
      if (sptr > bptr)
      {
         if (rqptr->rqPathSet.ResponseVarRecord == FILE_VAR_CRLF)
         {
            if (!recnt || (recnt >= 2 && *(USHORTPTR)(sptr-2) != '\r\n'))
            {
               *(USHORTPTR)sptr = '\r\n';
               sptr += 2;
            }
         }
         else
         if (rqptr->rqPathSet.ResponseVarRecord != FILE_VAR_NONE)
            if (!recnt || (recnt >= 1 && *(sptr-1) != '\n'))
               *sptr++ = '\n';
      }

      if (!rcnt && !VfcSize)
      {
         if (rqptr->rqPathSet.ResponseVarRecord == FILE_VAR_CRLF)
         {
            if (!recnt || (recnt >= 2 && *(USHORTPTR)(sptr-2) != '\r\n'))
            {
               *(USHORTPTR)sptr = '\r\n';
               sptr += 2;
            }
         }
         else
         if (rqptr->rqPathSet.ResponseVarRecord != FILE_VAR_NONE)
            if (!recnt || (recnt >= 1 && *(sptr-1) != '\n'))
               *sptr++ = '\n';
      }
   }

   /* zero any remainder from the move */
   if (zptr - sptr > 0) memset (sptr, 0, zptr - sptr);

   fqptr->RecordCount = rcnt;
   fqptr->BufferCount = sptr - bptr;

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "rcnt:!UL bcnt:!UL now:!UL", rcnt, bcnt, sptr - bptr);
}

/*****************************************************************************/
/*
Fixed length records where the record is not allowed to span the blocks (who
the hell uses these formats anyway?!)  Fixed length block spanning records are
just handled in the blocks.  No adjustments to carriage control!
*/ 

FileFixedRecordNoSpan (REQUEST_STRUCT *rqptr)

{
   int  bcnt;
   ushort  MaxRecordSize, rcnt;
   char  *bptr, *cptr, *sptr, *zptr;
   FILE_QIO  *fqptr;
   FILE_TASK  *tkptr;
                         
   /*********/
   /* begin */
   /*********/

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE, "FileFixedRecordNoSpan()");

   tkptr = rqptr->FileTaskPtr;
   fqptr = &tkptr->FileOds.FileQio;

   bptr = fqptr->BufferPtr;
   bcnt = fqptr->BufferCount;
   MaxRecordSize = fqptr->RecAttr.fat$w_maxrec;
   rcnt = fqptr->RecordCount;

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "MaxRecordSize:!UL rcnt:!UL", MaxRecordSize, rcnt);

   zptr = (cptr = sptr = bptr) + bcnt;
   while (cptr < zptr)
   {
      if (WATCHMOD (rqptr, WATCH_MOD_FILE))
         if (rcnt || cptr > sptr)
            WatchDataFormatted ("!5ZL |!#AZ|\n",
               rcnt, rcnt <= zptr-cptr ? rcnt : zptr-cptr, cptr);

      while (rcnt && cptr < zptr)
      {
         rcnt--;
         *sptr++ = *cptr++;
      }
      if (cptr >= zptr) break;
      /* if insufficient space for a record left in this block */
      if (510 - ((int)cptr & 0x1ff) < MaxRecordSize)
      {
         /* step to the start of the next block */
         cptr = ((int)cptr & ~0x1ff) + 512;
         if (cptr >= zptr) break;
      }
      rcnt = fqptr->RecAttr.fat$w_rsize;
   }

   fqptr->RecordCount = rcnt;
   fqptr->BufferCount = sptr - bptr;

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "rcnt:!UL bcnt:!UL now:!UL", rcnt, bcnt, sptr - bptr);
}

/*****************************************************************************/
/*
Send the buffer contents escaping any HTML-forbidden characters.
*/ 

FileWriteBufferEscapeHtml
(
REQUEST_STRUCT *rqptr,
REQUEST_AST AstFunction
)
{
   FILE_TASK  *tkptr;
                         
   /*********/
   /* begin */
   /*********/

   if (WATCHMOD (rqptr, WATCH_MOD_FILE))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_FILE,
                 "FileWriteBufferEscapeHtml() !&A !UL",
                 AstFunction, STR_DSC_LEN(&rqptr->NetWriteBufferDsc));

   tkptr = rqptr->FileTaskPtr;

   /* ensure the HTML descriptor is initialised */
   StrDscIfNotBegin (rqptr, &tkptr->BufferDsc,
                     STR_DSC_SIZE(&rqptr->NetWriteBufferDsc));

   /* swap the file read descriptor and the HTML descriptor */
   StrDscSwap (&rqptr->NetWriteBufferDsc, &tkptr->BufferDsc);

   /* reset the (now) output descriptor */
   StrDscNoContent (&rqptr->NetWriteBufferDsc);

   /* copy as HTML into the output descriptor */
   StrDscBuildHtmlEscape (&rqptr->NetWriteBufferDsc, &tkptr->BufferDsc, NULL);

   /* write it out */
   NetWriteStrDsc (rqptr, AstFunction);
}

/*****************************************************************************/
/*
The opaque entity identifier is generated from the six byte file-id plus the
four byte revision date-time.  In this way if the file is moved or revised the
entity should reflect that.  Assumes the supplied string is large enough to
store the generated entity tag (29 bytes).
*/ 

FileGenerateEntityTag
(
char *EntityString,
FILE_QIO *fqptr
)
{
   /* hex digits: 12 for FID, 16 for RDT */
   static $DESCRIPTOR (EntityTagFaoDsc, "!4XL!4XL!4XL!8XL!8XL\0");
   static $DESCRIPTOR (EntityTagDsc, "");

   ulong  *qptr;
   char  *cptr;
                         
   /*********/
   /* begin */
   /*********/

   if (WATCH_MODULE(WATCH_MOD_FILE))
      WatchThis (WATCHALL, WATCH_MOD_FILE, "FileGenerateEntityTag()");

   EntityTagDsc.dsc$w_length = 29;
   EntityTagDsc.dsc$a_pointer = EntityString;

   qptr = &fqptr->RdtTime64;
   sys$fao (&EntityTagFaoDsc, 0, &EntityTagDsc,
            fqptr->Fib.fib$w_fid[0], fqptr->Fib.fib$w_fid[1],
            fqptr->Fib.fib$w_fid[2], qptr[0], qptr[1]);
   for (cptr = EntityString; *cptr; cptr++) *cptr = to_lower(*cptr);

   if (WATCH_MODULE(WATCH_MOD_FILE))
      WatchThis (WATCHALL, WATCH_MOD_FILE, "!AZ", EntityString);
}

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

