//+---------------------------------------------------------------------------
//
//  Microsoft Windows
//  Copyright (C) Microsoft Corporation, 1996 - 2000.
//
//  File:       htx.cxx
//
//  Contents:   Parser for a HTX file
//
//  History:    96/Jan/3    DwightKr    Created
//
//----------------------------------------------------------------------------


#include <pch.cxx>
#pragma hdrstop

//+---------------------------------------------------------------------------
//
//  Member:     CHTXScanner::CHTXScanner - public constructor
//
//  Synopsis:   Builds a scanner for a section within a HTX file
//
//  Arguments:  [variableSet] - list of replaceable parameters
//              [wcsPrefix]   - prefix delimiter for replacable parameters
//              [wcsSuffix]   - suffix delimiter for replacable parameters
//
//  Notes:      The wcsPrefix and wcsSuffix are expected to be the same
//              length and either one or two characters.
//
//  History:    96/Jan/03   DwightKr    created
//
//----------------------------------------------------------------------------
CHTXScanner::CHTXScanner( CVariableSet & variableSet,
                          WCHAR const * wcsPrefix,
                          WCHAR const * wcsSuffix ) :
                                    _wcsPrefix(wcsPrefix),
                                    _wcsSuffix(wcsSuffix),
                                    _variableSet(variableSet),
                                    _type(eNone),
                                    _nextType(eNone),
                                    _wcsString(0),
                                    _wcsPrefixToken(0),
                                    _wcsSuffixToken(0)
{
    Win4Assert( wcslen( _wcsPrefix ) == wcslen( _wcsSuffix ) &&
                wcslen( _wcsPrefix ) <= 2 );

    if ( _wcsPrefix[1] == L'\0' )
        _cchPrefix = _cchSuffix = 1;
    else
        _cchPrefix = _cchSuffix = 2;
}


//+---------------------------------------------------------------------------
//
//  Member:     CHTXScanner::Init - public
//
//  Synopsis:   Saves a pointer to the string to be parsed.
//
//  Arguments:  [wcsString] - the string to be parsed
//
//  History:    96/Jan/03   DwightKr    created
//
//  NOTES:      THIS STRING WILL BE MODIFIED BY SUBSEQUENT CALLS TO MEMBER
//              FUNCTIONS OF THIS CLASS.
//
//----------------------------------------------------------------------------
void CHTXScanner::Init( WCHAR * wcsString )
{
    _wcsString = wcsString;
}


//+---------------------------------------------------------------------------
//
//  Member:     CHTXScanner::IsToken - private
//
//  Synopsis:   Determines if a string is a special token.
//
//  Arguments:  [wcs] - start of string to be tested.
//
//  Notes:      If the string is a token, the members _type, _wcsPrefixToken
//              and _wcsSuffixToken are set appropriately.
//
//  History:    96/Apr/02   AlanW       Created
//              96/May/17   DwightKr    Treat all <%..%> as variables
//
//----------------------------------------------------------------------------

BOOL CHTXScanner::IsToken(WCHAR * wcs)
{
    if ( wcsncmp( _wcsPrefix, wcs, _cchPrefix ) != 0 )
    {
        ciGibDebugOut(( DEB_USER1, "CHTXScanner::IsToken  end of string\n" ));
        return FALSE;
    }

    wcs += _cchPrefix;
    WCHAR * wcsSuffixTok = wcs2chr( wcs, _wcsSuffix );
    if ( 0 == wcsSuffixTok )
    {
        ciGibDebugOut(( DEB_USER1, "CHTXScanner::IsToken  no suffix token\n" ));
        return FALSE;
    }

    *wcsSuffixTok = L'\0';
    _wcsPrefixToken = wcs - _cchPrefix;
    _wcsupr( wcs );

    //
    //  Strip leading spaces before token
    //
    while ( iswspace(*wcs) && (wcs < wcsSuffixTok) )
    {
        wcs++;
    }

    //
    //  Strip trailing spaces after token
    //
    WCHAR * wcsSuffix = wcsSuffixTok - 1;
    while ( iswspace(*wcsSuffix) && (wcsSuffix > wcs) )
    {
        *wcsSuffix = 0;
        wcsSuffix--;
    }

    ciGibDebugOut(( DEB_USER1, "CHTXScanner::IsToken  wcs=%ws\n", wcs ));

    if ( wcsncmp( wcs, L"IF ", 3 ) == 0 )
    {
        _type = eIf;
    }
    else if ( wcscmp( wcs, L"ELSE" ) == 0 )
    {
        _type = eElse;
    }
    else if ( wcscmp( wcs, L"ENDIF" ) == 0 )
    {
        _type = eEndIf;
    }
    else if ( wcsncmp( wcs, L"ESCAPEHTML ", 11 ) == 0 )
    {
        _type = eEscapeHTML;
    }
    else if ( wcsncmp( wcs, L"ESCAPEURL ", 10 ) == 0 )
    {
        _type = eEscapeURL;
    }
    else if ( wcsncmp( wcs, L"ESCAPERAW ", 10 ) == 0 )
    {
        _type = eEscapeRAW;
    }
    else
    {
        //
        //  Find this name in the list of replaceable parameters.  Note that
        //  if we can't find this variable in the list of replaceable
        //  parameters, we've converted some output text to uppercase.  This
        //  is probably OK since the user used <% ... %> to delimit their
        //  output; <% & %> are reserved tokens hence this would be an error.
        //
        CVariable *pVariable = _variableSet.Find( wcs );

        if ( 0 != pVariable )
        {
            //
            //  We have a match, this is a replaceable parameter.  Compiler
            //  bug.  _type needs to be assigned in both places.
            //
            _type = eParameter | pVariable->GetFlags();
        }
        else
        {
            ciGibDebugOut(( DEB_IWARN,
                            "Warning: CHTXScanner::IsToken found a unknown variable: '%ws'\n",
                            wcs ));

            _type = eParameter;
        }
    }

    *_wcsPrefixToken = L'\0';
    _wcsSuffixToken = wcsSuffixTok;
    _wcsNextToken = wcsSuffixTok + _cchSuffix;

    return TRUE;
}

//+---------------------------------------------------------------------------
//
//  Member:     CHTXScanner::FindNextToken - public
//
//  Synopsis:   Locates the next token in the string.
//
//  History:    96/Jan/03   DwightKr    created
//
//----------------------------------------------------------------------------

BOOL CHTXScanner::FindNextToken()
{
    if (_nextType != eNone)
    {
        //
        // Found a token on the previous call.  Just return it.
        //
        Win4Assert ( _wcsPrefixToken && _wcsSuffixToken > _wcsPrefixToken );
        _type = _nextType;
        _nextType = eNone;
        _wcsString = _wcsNextToken = _wcsSuffixToken + _cchSuffix;
        return TRUE;
    }

    if ( (0 == _wcsString) || (0 == *_wcsString) )
    {
        _type = eNone;
        _wcsNextToken = 0;

        return FALSE;
    }

    if ( *_wcsString == *_wcsPrefix &&
         IsToken( _wcsString ) )
    {
        _nextType = eNone;
        return TRUE;
    }

    //
    // The string doesn't start with one of our special keywords.
    // Treat it as an ordinary string, and look ahead to the next
    // valid token.
    //

    _wcsPrefixToken = wcs2chr( _wcsString+1, _wcsPrefix );
    while ( _wcsPrefixToken )
    {
        if ( IsToken( _wcsPrefixToken ) )
        {
            _nextType = _type;
            _wcsNextToken = _wcsPrefixToken;
            _type = eString;
            return TRUE;
        }
        _wcsPrefixToken = wcs2chr( _wcsPrefixToken+_cchPrefix, _wcsPrefix );
    }

    _nextType = eNone;
    _type = eString;
    _wcsNextToken = _wcsString + wcslen( _wcsString );
    return TRUE;
}


//+---------------------------------------------------------------------------
//
//  Member:     CHTXScanner::GetToken - public
//
//  Synopsis:   Returns a pointer to the replaceable parameter token found.
//              Prepares the scanner to return the next token.
//
//  History:    96/Jan/03   DwightKr    created
//              96/Mar/13   DwightKr    add support for eEscapeURL &
//                                      eEscapeHTML
//
//----------------------------------------------------------------------------

WCHAR * CHTXScanner::GetToken()
{
    if ( eString == _type )
    {
        if ( 0 != _wcsString )
        {
            WCHAR * wcsString = _wcsString;
            _wcsString = _wcsNextToken;

            return wcsString;
        }
    }
    else if ( eEscapeHTML == _type )
    {
        WCHAR * wcsString = _wcsPrefixToken + _cchPrefix;
        wcsString += 10;                        // Skip 'EscapeHTML'
        *_wcsSuffixToken = 0;                   // Null terminate

        while ( (0 != *wcsString) && iswspace(*wcsString) )
        {
            wcsString++;
        }

        _wcsString = _wcsNextToken;

        return wcsString;
    }
    else if ( eEscapeURL == _type ||
              eEscapeRAW == _type )
    {
        WCHAR * wcsString = _wcsPrefixToken + _cchPrefix;
        wcsString += 9;                         // Skip 'EscapeURL'
        *_wcsSuffixToken = 0;                   // Null terminate

        while ( (0 != *wcsString) && iswspace(*wcsString) )
        {
            wcsString++;
        }

        _wcsString = _wcsNextToken;

        return wcsString;
    }
    else
    {
        if ( 0 != _wcsPrefixToken )
        {
            Win4Assert( 0 != _wcsSuffixToken &&
                        _wcsPrefixToken < _wcsSuffixToken &&
                        _wcsSuffixToken < _wcsNextToken );

            *_wcsPrefixToken = 0;
            *_wcsSuffixToken = 0;

            _wcsString = _wcsNextToken;

            WCHAR * wcsString = _wcsPrefixToken + _cchPrefix;
            while ( (0 != *wcsString) && iswspace(*wcsString) )
            {
                wcsString++;
            }


            return wcsString;
        }
    }

    return 0;
}


//+---------------------------------------------------------------------------
//
//  Member:     CHTXFile::CHTXFile - public constructor
//
//  Synopsis:   Builds a CHTXFile object and initializes values.
//
//  History:    96/Jan/03   DwightKr    created
//
//----------------------------------------------------------------------------
CHTXFile::CHTXFile( XPtrST<WCHAR> & wcsTemplate,
                    UINT codePage,
                    CSecurityIdentity const & securityIdentity,
                    ULONG ulServerInstance )
        : _wcsVirtualName( wcsTemplate.Acquire() ),
          _pVarHeader(0),
          _pVarRowDetails(0),
          _pVarFooter(0),
          _wcsFileBuffer(0),
          _fSequential(TRUE),
          _cIncludeFiles(0),
          _refCount(0),
          _codePage(codePage),
          _securityIdentity( securityIdentity ),
          _ulServerInstance( ulServerInstance )
{
    _wcsPhysicalName[0] = 0;
}

//+---------------------------------------------------------------------------
//
//  Member:     CHTXFile::~CHTXFile - public destructor
//
//  History:    96/Jan/03   DwightKr    created
//
//----------------------------------------------------------------------------
CHTXFile::~CHTXFile()
{
    Win4Assert( _refCount == 0 );

    delete _wcsVirtualName;
    delete _pVarHeader;
    delete _pVarRowDetails;
    delete _pVarFooter;
    delete _wcsFileBuffer;

    for (unsigned i=0; i<_cIncludeFiles; i++)
    {
        delete _awcsIncludeFileName[i];
    }
}


//+---------------------------------------------------------------------------
//
//  Member:     CHTXFile::ParseFile - public
//
//  Synopsis:   Parses the HTX file and breaks it up into its sections.
//
//  History:    96/Jan/03   DwightKr    created
//
//----------------------------------------------------------------------------
void CHTXFile::ParseFile( WCHAR const * wcsFileName,
                          CVariableSet & variableSet,
                          CWebServer & webServer )
{
    Win4Assert( wcsFileName != 0 );

    wcscpy( _wcsPhysicalName, wcsFileName );

    //
    //  Read the entire file into a buffer
    //
    CVirtualString wcsBuffer;
    ExpandFile( _wcsPhysicalName, webServer, wcsBuffer, _ftHTXLastWriteTime );

    Win4Assert( wcsBuffer.GetPointer() != 0 );

    //
    //  Break the buffer into the sections; the header, the detail section,
    //  and the footer.  Verify that if there is a <%BeginDetail%>
    //  section, then there MUST be a <%EndDetail%> section AFTER it, not
    //  before.  Neither <%EndDetail%> nor <%BeginDetail%> can appear on
    //  their own.
    //

    //
    //  Find the <%BeginDetail%> and <%EndDetail%> sections
    //
    _wcsFileBuffer        = wcsBuffer.StrDup();         // Save buffer
    WCHAR * wcsHeader     = _wcsFileBuffer;             // Assume a header
    WCHAR * wcsRowDetails = wcsipattern(wcsHeader, L"<%BEGINDETAIL%>" );
    WCHAR * wcsFooter     = wcsipattern(wcsHeader, L"<%ENDDETAIL%>" );

    if ( wcsHeader == wcsRowDetails )
    {
        //
        //  No header found in this file; it begins with the detail section.
        //
        wcsHeader = 0;
    }

    const int cwcBeginDetail = 15;
    const int cwcEndDetail   = 13;

    Win4Assert( cwcBeginDetail == wcslen( L"<%BEGINDETAIL%>" ) );
    Win4Assert( cwcEndDetail == wcslen( L"<%ENDDETAIL%>" ) );

    if ( 0 != wcsRowDetails )
    {
        //
        //  A <%BeginDetail%> section was found.  We better also have an
        //  <%EndDetail%> section AFTER the <%BeginDetail%> section.
        //

        *wcsRowDetails = 0;         // Null terminate the header string
        wcsRowDetails += cwcBeginDetail;

        if ( 0 != wcsFooter )
        {
            if ( wcsFooter < wcsRowDetails )
            {
                //
                //  The <%EndDetail%> was found before the <%BeginDetail%>
                //
                WCHAR * wcsHTXFileName;
                LONG    lLineNumber;

                GetFileNameAndLineNumber( (int)(wcsFooter - _wcsFileBuffer),
                                          wcsHTXFileName,
                                          lLineNumber );

                THROW( CHTXException(MSG_CI_HTX_ENDDETAIL_BEFORE_BEGINDETAIL,
                                     wcsHTXFileName,
                                     lLineNumber) );
            }

            *wcsFooter = 0;         // Null terminate the BeginDetail section
            wcsFooter += cwcEndDetail;
        }
        else
        {
            //
            //  Report an error:  <%BeginDetail%> without an <%EndDetail%>
            //

            WCHAR * wcsHTXFileName;
            LONG    lLineNumber;

            GetFileNameAndLineNumber( (int)(wcsRowDetails - _wcsFileBuffer),
                                      wcsHTXFileName,
                                      lLineNumber );

            THROW( CHTXException(MSG_CI_HTX_NO_ENDDETAIL_SECTION,
                                 wcsHTXFileName,
                                 lLineNumber) );
        }
    }
    else if ( 0 != wcsFooter )
    {
        //
        //  A <%BeginDetail%> section could be found.  There should
        //  be no <%EndDetail%> section either.
        //

        WCHAR * wcsHTXFileName;
        LONG    lLineNumber;

        GetFileNameAndLineNumber( (int)(wcsFooter - _wcsFileBuffer),
                                  wcsHTXFileName,
                                  lLineNumber );

        THROW( CHTXException(MSG_CI_HTX_NO_BEGINDETAIL_SECTION,
                             wcsHTXFileName,
                             lLineNumber) );
    }


    if ( 0 != wcsHeader )
    {
        _pVarHeader = new CParameterReplacer ( wcsHeader,
                                               L"<%",
                                               L"%>" );
        _pVarHeader->ParseString( variableSet );
    }

    if ( 0 != wcsRowDetails )
    {
        _pVarRowDetails = new CParameterReplacer ( wcsRowDetails,
                                                   L"<%",
                                                   L"%>" );
        _pVarRowDetails->ParseString( variableSet );
    }

    if ( 0 != wcsFooter )
    {
        _pVarFooter = new CParameterReplacer ( wcsFooter,
                                               L"<%",
                                               L"%>" );
        _pVarFooter->ParseString( variableSet );
    }

    _fSequential = CheckForSequentialAccess();
}

//+---------------------------------------------------------------------------
//
//  Member:     CHTXFile::ReadFile - public
//
//  Synopsis:   Read the HTX file into a buffer
//
//  Arguments:  [wcsFileName]  - full physical path name of file
//              [ftLastWrite]  - File's last write time is stored here
//
//  History:    96/Jan/03   DwightKr    created
//              96/Apr/06   DwightKr    add support for unicode files
//
//----------------------------------------------------------------------------
WCHAR * CHTXFile::ReadFile( WCHAR const * wcsFileName,
                            FILETIME & ftLastWrite )
{
    Win4Assert ( 0 != wcsFileName );

    // We don't support impersonation for scripts.
    // It involves getting the server ip address, vpath and then using
    // that for impersonation

    if ( IsNetPath(wcsFileName) )
    {
        ciGibDebugOut(( DEB_ERROR, "The htx file (%ws) is on remote UNC\n",
                        wcsFileName ));
        THROW( CHTXException( MSG_CI_SCRIPTS_ON_REMOTE_UNC, wcsFileName, 0 ));
    }

    //
    //  Verify the HTX file exists, and is a file, not a directory.
    //
    WIN32_FIND_DATA ffData;
    if ( !GetFileAttributesEx( wcsFileName, GetFileExInfoStandard, &ffData ) )
    {
        ULONG error = GetLastError();

        ciGibDebugOut(( DEB_IERROR,
                        "Unable to GetFileAttributesEx(%ws) GetLastError=0x%x\n",
                        wcsFileName,
                        error ));

        THROW( CIDQException(MSG_CI_IDQ_NO_SUCH_TEMPLATE, 0) );
    }


    if ( (ffData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 )
    {
        THROW( CIDQException(MSG_CI_IDQ_NO_SUCH_TEMPLATE, 0) );
    }

    //
    //  Save the last write time of this file.
    //
    ftLastWrite = ffData.ftLastWriteTime;


    //
    //  Open the file and map its contents
    //
    CFileMapView mapView( wcsFileName );
    mapView.Init();

    int cbBuffer = mapView.GetBufferSize() + 1;
    XArray<WCHAR> pwBuffer(cbBuffer);

    //
    //  If the first two BYTES of the file are 0xFF 0xFE, then this is a
    //  unicode file, and we don't need to convert it.
    //
    if ( mapView.IsUnicode() )
    {
        RtlCopyMemory( pwBuffer.Get(), mapView.GetBuffer()+2, cbBuffer-2 );
        pwBuffer[ ( cbBuffer - 2 ) / sizeof WCHAR ] = 0;

        return pwBuffer.Acquire();
    }

    //
    //  Copy & convert the ASCII buffer to a WCHAR buffer.
    //
    int cwBuffer = mapView.GetBufferSize() + 1;
    int cwConvert;

    do
    {
        cwConvert = MultiByteToWideChar(_codePage,
                                        0,
                         (const char *) mapView.GetBuffer(),    // Ptr to input buf
                                        mapView.GetBufferSize(),// Size of input buf
                                        pwBuffer.Get(), // Ptr to output buf
                                        cwBuffer - 1 ); // Size of output buf

        if ( 0 == cwConvert )
        {
            if ( GetLastError() == ERROR_INSUFFICIENT_BUFFER )
            {
                cwBuffer += (cwBuffer/2);
                delete pwBuffer.Acquire();
                pwBuffer.Init(cwBuffer);
            }
            else
            {
                THROW( CException() );
            }
        }
        else
        {
            pwBuffer[cwConvert] = 0;        // Null terminate the buffer
        }

        Win4Assert( cwConvert < cwBuffer );

    } while ( 0 == cwConvert );

    return pwBuffer.Acquire();
}


//+---------------------------------------------------------------------------
//
//  Member:     CHTXFile::ExpandFile, public
//
//  Synopsis:   Expands the contents of an HTX file into memory, processing
//              included files.
//
//  Arguments:  [wcsFileName]  - file path name of the file to be expanded
//              [webServer]    - CWebServer for virtual path translation
//              [vString]      - String to which file contents are appended
//              [ftLastWrite]  - File's last write time is stored here
//
//----------------------------------------------------------------------------
void CHTXFile::ExpandFile( WCHAR const * wcsFileName,
                           CWebServer & webServer,
                           CVirtualString & vString,
                           FILETIME & ftLastWrite )
{
    Win4Assert ( 0 != wcsFileName );

    //
    //  Read the existing file into a WCHAR buffer.
    //
    XPtrST<WCHAR> wcsBuffer( ReadFile(wcsFileName, ftLastWrite) );

    WCHAR * wcsString = wcsBuffer.GetPointer();
    ULONG   cwcString = wcslen( wcsString );
    WCHAR * wcsEnd    = wcsString + cwcString;

    //
    //  Search the WCHAR buffer for <%include ... %>
    //
    WCHAR * wcsPattern = L"<%INCLUDE ";
    ULONG   cwcPattern = 10;

    Win4Assert( cwcPattern == wcslen(wcsPattern) );

    WCHAR * wcsToken = wcsipattern( wcsString, wcsPattern );

    while ( 0 != wcsToken )
    {
        if ( _cIncludeFiles >= MAX_HTX_INCLUDE_FILES )
        {
            LONG cLines = CountLines( wcsBuffer.GetPointer(), wcsToken );

            THROW( CHTXException(MSG_CI_HTX_TOO_MANY_INCLUDES,
                                 wcsFileName,
                                 cLines) );
        }

        //
        //  Concatentate everything before the <%include .. %> into the
        //  virtual string.
        //
        ULONG cwcCat = (ULONG)(wcsToken - wcsString);
        Win4Assert( cwcCat <= cwcString );
        *wcsToken = 0;                          // Null terminate the string
        vString.StrCat( wcsString, cwcCat );

        //
        //  Find the end of the <%include ... %>
        //

        wcsToken += cwcPattern;                 //  Skip the <%include
        WCHAR * wcsIncludeFileName = wcsToken;  //  Point to the include filename
        wcsString = wcs2chr( wcsToken, L"%>" );  //  Point to the end of the filename

        //
        //  wcsString should be pointing to the %> at the end of the include
        //
        if ( 0 == wcsString )
        {
            //
            //  Missing %>
            //

            LONG cLines = CountLines( vString.GetPointer(), wcsToken );

            THROW( CHTXException(MSG_CI_HTX_ILL_FORMED_INCLUDE,
                                 wcsFileName,
                                 cLines) );
        }

        //
        //  Process the <%include ... %>
        //
        *wcsString = 0;
        if ( (wcsString - wcsIncludeFileName) >= MAX_PATH )
        {
            LONG cLines = CountLines( wcsBuffer.GetPointer(), wcsToken );

            THROW( CHTXException(MSG_CI_HTX_INVALID_INCLUDE_FILENAME,
                                 wcsFileName,
                                 cLines ) );
        }

        WCHAR   awcPhysicalPath[MAX_PATH];
        webServer.GetPhysicalPath( wcsIncludeFileName,
                                   awcPhysicalPath,
                                   MAX_PATH );

        //
        //  Save the include filename away
        //
        ULONG cwcPhysicalPath = wcslen(awcPhysicalPath) + 1;
        _awcsIncludeFileName[_cIncludeFiles]  = new WCHAR[ cwcPhysicalPath ];
        _aulIncludeFileOffset[_cIncludeFiles] = vString.StrLen();

        RtlCopyMemory( _awcsIncludeFileName[_cIncludeFiles],
                       awcPhysicalPath,
                       cwcPhysicalPath * sizeof(WCHAR) );

        FILETIME & ftLastWrite = _aftIncludeLastWriteTime[ _cIncludeFiles ];
        _cIncludeFiles++;

        ExpandFile( awcPhysicalPath, webServer, vString, ftLastWrite );

        wcsString += 2;                     // Skip the %>
        cwcString = (ULONG)(wcsEnd - wcsString);
        wcsToken  = wcsipattern( wcsString, wcsPattern );
    }

    vString.StrCat( wcsString, cwcString );
}


//+---------------------------------------------------------------------------
//
//  Member:     CHTXFile::GetFileNameAndLineNumber
//
//  Synopsis:   Determines the filename & line number amoung a group of
//              nested includes for a particular offset into the buffer.
//
//  Arguments:  [offset] - offset of the error in the overall buffer
//              [wcsFileName] - resulting name of file containing error
//              [lineNumber]  - line # containing the error
//
//  History:    96/Jun/25   DwightKr    created
//
//----------------------------------------------------------------------------
void CHTXFile::GetFileNameAndLineNumber( int offset,
                                         WCHAR *& wcsFileName,
                                         LONG & lineNumber )
{
    //
    //  Search the array of offsets for the one containing our offset
    //
    for (ULONG i = 0;
         (i < _cIncludeFiles) && ((ULONG) offset > _aulIncludeFileOffset[i]);
         i++ )
    {
    }

    //
    //  Save a pointer to the name of the file containing the error
    //
    WCHAR const * pCurrent = _wcsFileBuffer;
    if ( 0 == i )
    {
        //
        //  We have a problem in the outer-most container file; not
        //  an include file.
        //
        wcsFileName = _wcsVirtualName;
    }
    else
    {
        wcsFileName = _awcsIncludeFileName[i-1];
        pCurrent += _aulIncludeFileOffset[i-1];
    }


    //
    //  Count the number of lines in this sub-file
    //
    Win4Assert( 0 != _wcsFileBuffer );
    WCHAR const * pEnd = _wcsFileBuffer + offset;

    lineNumber = CountLines( pCurrent, pEnd );
}


//+---------------------------------------------------------------------------
//
//  Member:     CHTXFile::CountLines - private
//
//  Synopsis:   Deterines the number of lines (CR's) between the start
//              of the buffer and the end.
//
//  Arguments:  [wcsStart] - start location of search
//              [wcsEnd]   - end of search
//
//  History:    96/Jun/25   DwightKr    created
//
//----------------------------------------------------------------------------
LONG CHTXFile::CountLines( WCHAR const * wcsStart,
                           WCHAR const * wcsEnd ) const
{
    Win4Assert( 0 != wcsStart );
    Win4Assert( 0 != wcsEnd );

    LONG cLines = 1;

    while ( wcsStart <= wcsEnd )
    {
        if ( L'\n' == *wcsStart )
        {
            cLines++;
        }

        wcsStart++;
    }

    return cLines;
}


//+---------------------------------------------------------------------------
//
//  Member:     CHTXFile::IsCachedDataValid - public
//
//  Synopsis:   Determines if the cached & parsed data from the HTX file
//              is still valid.  The HTX file itself may have changed
//              since we read and parsed it.
//
//  History:    96/Jan/03   DwightKr    created
//              96/Mar/14   DwightKr    check <%include%> file times
//
//----------------------------------------------------------------------------
BOOL CHTXFile::IsCachedDataValid()
{
    FILETIME ft;

    SCODE sc = GetLastWriteTime( _wcsPhysicalName, ft );

    if ( FAILED( sc ) )
        return FALSE;

    if ( (_ftHTXLastWriteTime.dwLowDateTime != ft.dwLowDateTime) ||
         (_ftHTXLastWriteTime.dwHighDateTime != ft.dwHighDateTime) )
    {
        return FALSE;
    }

    for ( unsigned i=0; i<_cIncludeFiles; i++ )
    {
        sc = GetLastWriteTime(_awcsIncludeFileName[i], ft );

        if ( FAILED( sc ) )
            return FALSE;

        if ( (_aftIncludeLastWriteTime[i].dwLowDateTime != ft.dwLowDateTime) ||
             (_aftIncludeLastWriteTime[i].dwHighDateTime != ft.dwHighDateTime) )
        {
            return FALSE;
        }
    }

    return TRUE;
}

//+---------------------------------------------------------------------------
//
//  Member:     CHTXFile::GetHeader - public
//
//  Synopsis:   Appends to a CVirtualString the data in the HTX file BEFORE
//              the <%begindetail%> section.  This may require replacing
//              parameters.
//
//  Arguments:  [string]       - the CVirtualString to append data to
//              [variableSet]  - a list of replaceable parameters
//              [outputFormat] - format for numbers & dates
//
//  History:    96/Jan/03   DwightKr    created
//
//----------------------------------------------------------------------------
void CHTXFile::GetHeader( CVirtualString & string,
                          CVariableSet & variableSet,
                          COutputFormat & outputFormat )
{
    if ( 0 != _pVarHeader )
    {
        _pVarHeader->ReplaceParams( string, variableSet, outputFormat );
    }
}



//+---------------------------------------------------------------------------
//
//  Member:     CHTXFile::GetFooter - public
//
//  Synopsis:   Appends to a CVirtualString the data in the HTX file AFTER
//              the <%enddetail%> section.  This may require replacing
//              parameters.
//
//  Arguments:  [string]      - the CVirtualString to append data to
//              [variableSet] - a list of replaceable parameters
//              [outputFormat] - format for numbers & dates
//
//  History:    96/Jan/03   DwightKr    created
//
//----------------------------------------------------------------------------
void CHTXFile::GetFooter( CVirtualString & string,
                          CVariableSet & variableSet,
                          COutputFormat & outputFormat )
{
    if ( 0 != _pVarFooter )
    {
        _pVarFooter->ReplaceParams( string, variableSet, outputFormat );
    }
}


//+---------------------------------------------------------------------------
//
//  Member:     CHTXFile::CheckForSequentialAccess - public
//
//  Synopsis:   Determines if a sequential query cursor can be used to
//              extract query results.  This is possible if the HTX file
//              does not use any replaceable parameters which require
//              data from an IRowsetScroll.
//
//  History:    96/Jan/03   DwightKr    created
//
//----------------------------------------------------------------------------
BOOL CHTXFile::CheckForSequentialAccess()
{
    //
    //  If an HTX file contains any of the following variables, it must
    //  use a non-sequential access, since we need one or more interfaces
    //  from IRowsetScroll.
    //
    //      CiMatchedRecordCount
    //      CiRecordsNextPage
    //      CiTotalNumberPages
    //

    if ( (0 != _pVarHeader) && (_pVarHeader->GetFlags() & eParamRequiresNonSequentialCursor) )
    {
        return FALSE;
    }

    if ( (0 != _pVarRowDetails) && (_pVarRowDetails->GetFlags() & eParamRequiresNonSequentialCursor) )
    {
        return FALSE;
    }

    if ( (0 != _pVarFooter) && (_pVarFooter->GetFlags() & eParamRequiresNonSequentialCursor) )
    {
        return FALSE;
    }

    return TRUE;
}

//+---------------------------------------------------------------------------
//
//  Member:     CHTXFileList::Find - public
//
//  Synopsis:   Finds a matching parsed HTX file in list, or builds a new
//              one if a match can not be found.
//
//  Arguments:  [wcsFileName]      -- full path to HTX file
//              [variableSet]      -- list of replaceable parameters
//              [outputFormat]     -- format for numbers and dates
//              [securityIdentity] -- Logon for this query
//              [ulServerInstance] -- Virtual Server Instance Number
//
//  History:    96/Mar/27   DwightKr    Created.
//
//----------------------------------------------------------------------------

CHTXFile & CHTXFileList::Find( WCHAR const * wcsFileName,
                               CVariableSet & variableSet,
                               COutputFormat & outputFormat,
                               CSecurityIdentity const & securityIdentity,
                               ULONG ulServerInstance )
{
    Win4Assert( 0 != wcsFileName );

    //
    //  Determine the name of the HTX/template file.  It may have a
    //  replaceable value from the client.
    //

    ULONG cwcOut;
    XPtrST<WCHAR> wcsVirtualName( ReplaceParameters( wcsFileName,
                                                     variableSet,
                                                     outputFormat,
                                                     cwcOut ) );

    if ( 0 == *(wcsVirtualName.GetPointer()) )
    {
        THROW( CIDQException( MSG_CI_IDQ_MISSING_TEMPLATEFILE, 0 ) );
    }


    //
    //  Refcount everything in the list so that we can examine the list
    //  outside of the lock.
    //

    ULONG      cItems;
    XArray<CHTXFile *> aHTXFile;

    // ==========================================
    {
        CLock lock( _mutex );

        cItems = _aHTXFile.Count();         // Save count of items to examine
        aHTXFile.Init( cItems );

        for (unsigned i=0; i<cItems; i++)
        {
            aHTXFile[i] = _aHTXFile[i];
            aHTXFile[i]->LokAddRef();
        }
    }
    // ==========================================


    //
    //  Now walk though the list looking for a match; outside of the lock.
    //
    XInterface<CHTXFile> xHTXFile;
    SCODE sc = S_OK;

    TRY
    {
        for (unsigned i=0; i<cItems; i++)
        {
            if ( (_wcsicmp(aHTXFile[i]->GetVirtualName(), wcsVirtualName.GetPointer() ) == 0) &&
                 (aHTXFile[i]->GetCodePage() == outputFormat.CodePage()) &&
                 (aHTXFile[i]->GetServerInstance() == ulServerInstance) &&
                 (aHTXFile[i]->IsCachedDataValid() )
               )
            {
                xHTXFile.Set( aHTXFile[i] );

                ciGibDebugOut(( DEB_ITRACE,
                                "A cached version of HTX file %ws was found\n",
                                wcsVirtualName.GetPointer() ));

                break;
            }
        }
    }
    CATCH( CException, e )
    {
        sc = e.GetErrorCode();
    }
    END_CATCH

    //
    //  If xHTXFile is non-0, we've found a match.  Decrement the ref-count
    //  for all items which did not match.
    //

    for (unsigned i=0; i<cItems; i++)
    {
        if ( aHTXFile[i] != xHTXFile.GetPointer() )
        {
            aHTXFile[i]->Release();
        }
    }

    if ( S_OK != sc )
    {
        Win4Assert( xHTXFile.IsNull() );
        THROW( CException( sc ) );
    }

    //
    // We may have matched, but still not have access to this file.  First, make
    // a quick check for an exact match on security token, and then try harder
    // by opening the file.
    //

    if ( !xHTXFile.IsNull() )
    {
        if ( !xHTXFile->CheckSecurity( securityIdentity ) )
        {
            HANDLE h = CreateFile( xHTXFile->GetPhysicalName(),
                                   GENERIC_READ,
                                   FILE_SHARE_READ,
                                   0,
                                   OPEN_EXISTING,
                                   0,
                                   0 );

            //
            // Don't try to determine here if security caused the problem.
            // Just let the standard exception handling below in file parsing
            // deal with the error.
            //

            if ( INVALID_HANDLE_VALUE == h )
            {
                xHTXFile.Free();
            }
            else
            {
                CloseHandle( h );

                //
                // Update the security token of the cached Htx file,
                // to optimize away the CreateFile check in two cases:
                //   1.  When the file is first parsed with admin
                //       privileges, and all subsequent queries are with
                //       anonymous privileges.
                //   2.  When the security token changes over time
                //
                xHTXFile->SetSecurityToken( securityIdentity );
            }
        }
    }

    //
    //  If we didn't find a match, then open and parse a new HTX file, and
    //  add it to the list of parsed HTX files
    //

    if ( xHTXFile.IsNull() )
    {
        ciGibDebugOut(( DEB_ITRACE,
                        "Adding HTX file %ws to cache\n",
                        wcsVirtualName.GetPointer() ));

        WCHAR wcsPhysicalName[MAX_PATH];
        if ( outputFormat.IsValid() )
        {
            outputFormat.GetPhysicalPath( wcsVirtualName.GetPointer(),
                                          wcsPhysicalName,
                                          MAX_PATH );
        }
        else
        {
            if ( !GetFullPathName( wcsVirtualName.GetPointer(),
                                   MAX_PATH,
                                   wcsPhysicalName,
                                   0 ) )
            {
                THROW( CException() );
            }
        }

        XPtr<CHTXFile> xHTXFilePtr( new CHTXFile( wcsVirtualName,
                                                  outputFormat.CodePage(),
                                                  securityIdentity,
                                                  ulServerInstance ) );
        xHTXFilePtr->ParseFile( wcsPhysicalName,
                                variableSet,
                                outputFormat );

        {
            // ==========================================
            CLock lock( _mutex );
            _aHTXFile.Add( xHTXFilePtr.GetPointer(), _aHTXFile.Count() );
            xHTXFilePtr->LokAddRef();
            // ==========================================
        }

        xHTXFile.Set( xHTXFilePtr.Acquire() );
    }

    // CopyStringValue can fail.

    variableSet.CopyStringValue( ISAPI_CI_TEMPLATE,
                                 xHTXFile->GetVirtualName(),
                                 0 );

    return *xHTXFile.Acquire();
}

//+---------------------------------------------------------------------------
//
//  Member:     CHTXFileList::FindCanonicHTX
//
//  Synopsis:   Finds the standard HTX file for canonical output
//
//  Arguments:  [variableSet]      - list of replaceable parameters
//              [codePage]         - code page to open HTX with
//              [securityIdentity] - Identity of the htx file needed
//              [ulServerInstance] - Virtual Server metabase instance #
//
//  History:    7-03-96   srikants   Created
//
//  Notes:      There is nothing to be displayed for the canonical output.
//
//----------------------------------------------------------------------------

CHTXFile & CHTXFileList::FindCanonicHTX( CVariableSet & variableSet,
                                         UINT codePage,
                                         CSecurityIdentity const & securityIdentity,
                                         ULONG ulServerInstance )
{
    CHTXFile * pHTXFile = 0;

    CLock   lock(_mutex);

    if ( 0 == _pCanonicHTX )
    {
        ciGibDebugOut(( DEB_ITRACE,
                        "Adding Canonic HTX file cache\n" ));

        unsigned len = wcslen( CANONIC_HTX_FILE );
        XPtrST<WCHAR> wcsVirtualName( new WCHAR [len+1] );
        RtlCopyMemory( wcsVirtualName.GetPointer(), CANONIC_HTX_FILE,
                       (len+1)*sizeof(WCHAR) );

        pHTXFile = new CHTXFile( wcsVirtualName,
                                 codePage,
                                 securityIdentity,
                                 ulServerInstance );
        _pCanonicHTX = pHTXFile;

        pHTXFile->LokAddRef();
    }
    else
    {
        pHTXFile = _pCanonicHTX;
        pHTXFile->LokAddRef();
    }

    Win4Assert( 0 != variableSet.Find( ISAPI_CI_BOOKMARK ) );
    variableSet.CopyStringValue( ISAPI_CI_TEMPLATE,
                                 pHTXFile->GetVirtualName(),
                                 0 );

    return *pHTXFile;
}


//+---------------------------------------------------------------------------
//
//  Member:     CHTXFileList::~CHTXFileList - public destructor
//
//  History:    96/Mar/27   DwightKr    Created.
//
//----------------------------------------------------------------------------
CHTXFileList::~CHTXFileList()
{
    for (unsigned i=0; i<_aHTXFile.Count(); i++)
    {
        ciGibDebugOut(( DEB_ITRACE,
                        "Deleting HTX cache entry %ws\n",
                        _aHTXFile[i]->GetVirtualName() ));

        delete _aHTXFile[i];
    }

    delete _pCanonicHTX;
}


//+---------------------------------------------------------------------------
//
//  Member:     CHTXFileList::Release - public
//
//  Synopsis:   Releases the HTX file by decrementing its refcount.
//
//  Arguments:  [htxFile] -- pointer to the HTX file object
//
//  History:    96/Mar/27   DwightKr    Created.
//
//----------------------------------------------------------------------------
void CHTXFileList::Release( CHTXFile & htxFile )
{
    htxFile.Release();
}


//+---------------------------------------------------------------------------
//
//  Member:     CHTXFileList::DeleteZombies - public
//
//  Synopsis:   Removes HTX files that are zombies; i.e. out of date
//
//  History:    96/Mar/28   DwightKr    Created.
//
//----------------------------------------------------------------------------
void CHTXFileList::DeleteZombies()
{
    // ==========================================
    CLock lock( _mutex );

    unsigned i=0;
    while ( i<_aHTXFile.Count() )
    {
        if ( _aHTXFile[i]->LokGetRefCount() == 0 &&
             !_aHTXFile[i]->IsCachedDataValid() )
        {
            CHTXFile * pHTXFile = _aHTXFile[i];
            _aHTXFile.Remove(i);

            ciGibDebugOut(( DEB_ITRACE,
                            "Deleting zombie HTX cache entry %ws, %d entries cached\n",
                            pHTXFile->GetVirtualName(),
                            _aHTXFile.Count() ));

            delete pHTXFile;
        }
        else
        {
            ciGibDebugOut(( DEB_ITRACE,
                            "HTX cache entry %ws was not deleted, refCount=%d\n",
                            _aHTXFile[i]->GetVirtualName(),
                            _aHTXFile[i]->LokGetRefCount() ));
            i++;
        }
    }
    // ==========================================
}