///////////////////////////////////////////////////////////////////////////////
/*  File: userbat.cpp

    Description: Contains member function definitions for class 
        DiskQuotaUserBatch.
        The DiskQuotaUserBatch object represents a batch update mechanism for
        rapid update of multiple-user-object quota information.  This class
        takes advantage of the batching capabilities built into the NTIOAPI.
        A user batch object is obtained through 
        IDiskQuotaControl::CreateUserBatch().

    Revision History:

    Date        Description                                          Programmer
    --------    ---------------------------------------------------  ----------
    06/06/96    Initial creation.                                    BrianAu
*/
///////////////////////////////////////////////////////////////////////////////
#include "pch.h" // PCH
#pragma hdrstop

#include "userbat.h"

//
// Verify that build is UNICODE.
//
#if !defined(UNICODE)
#   error This module must be compiled UNICODE.
#endif

//
// The NTFS quota write function can only handle a max of 64K of data
// in any one write operation.  IssacHe recommended 60K as a comfortable
// limit.
//
const INT MAX_BATCH_BUFFER_BYTES = (1 << 10) * 60;

///////////////////////////////////////////////////////////////////////////////
/*  Function: DiskQuotaUserBatch::DiskQuotaUserBatch

    Description: Constructor.

    Arguments: 
        pFSObject - Address of File System object to be utilized by the
            batching operations.

    Returns: Nothing.

    Revision History:

    Date        Description                                          Programmer
    --------    ---------------------------------------------------  ----------
    09/03/96    Initial creation.                                    BrianAu
*/
///////////////////////////////////////////////////////////////////////////////
DiskQuotaUserBatch::DiskQuotaUserBatch(
    FSObject *pFSObject
    ) : m_cRef(0),
        m_pFSObject(pFSObject)
{      
    DBGASSERT((NULL != m_pFSObject));

    m_pFSObject->AddRef();
}


///////////////////////////////////////////////////////////////////////////////
/*  Function: DiskQuotaUserBatch::~DiskQuotaUserBatch

    Description: Destructor.

    Arguments: Destroys the batch object.

    Returns: Nothing.

    Revision History:

    Date        Description                                          Programmer
    --------    ---------------------------------------------------  ----------
    07/26/96    Initial creation.                                    BrianAu
*/
///////////////////////////////////////////////////////////////////////////////
DiskQuotaUserBatch::~DiskQuotaUserBatch(
    VOID
    )
{
    Destroy();
}

///////////////////////////////////////////////////////////////////////////////
/*  Function: DiskQuotaUserBatch::QueryInterface

    Description: Returns an interface pointer to the object's IUnknown or 
        IDiskQuotaUserBatch interface.  Only IID_IUnknown and 
        IID_IDiskQuotaUserBatch are recognized.  The object referenced by the 
        returned interface pointer is uninitialized.  The recipient of the 
        pointer must call Initialize() before the object is usable.

    Arguments:
        riid - Reference to requested interface ID.

        ppvOut - Address of interface pointer variable to accept interface ptr.

    Returns:
        NOERROR         - Success.
        E_NOINTERFACE   - Requested interface not supported.
        E_INVALIDARG    - ppvOut argument was NULL.

    Revision History:

    Date        Description                                          Programmer
    --------    ---------------------------------------------------  ----------
    06/06/96    Initial creation.                                    BrianAu
*/
///////////////////////////////////////////////////////////////////////////////
STDMETHODIMP 
DiskQuotaUserBatch::QueryInterface(
    REFIID riid, 
    LPVOID *ppvOut
    )
{
    HRESULT hResult = E_NOINTERFACE;

    if (NULL == ppvOut)
        return E_INVALIDARG;

    *ppvOut = NULL;

    if (IID_IUnknown == riid || IID_IDiskQuotaUserBatch == riid)
    {
        *ppvOut = this;
        ((LPUNKNOWN)*ppvOut)->AddRef();
        hResult = NOERROR;
    }

    return hResult;
}


///////////////////////////////////////////////////////////////////////////////
/*  Function: DiskQuotaUserBatch::AddRef

    Description: Increments object reference count.

    Arguments: None.

    Returns: New reference count value.

    Revision History:

    Date        Description                                          Programmer
    --------    ---------------------------------------------------  ----------
    06/06/96    Initial creation.                                    BrianAu
*/
///////////////////////////////////////////////////////////////////////////////
STDMETHODIMP_(ULONG) 
DiskQuotaUserBatch::AddRef(
    VOID
    )
{
    ULONG ulReturn = m_cRef + 1;

    DBGPRINT((DM_COM, DL_HIGH, TEXT("DiskQuotaUserBatch::AddRef, 0x%08X  %d -> %d\n"),
             this, ulReturn - 1, ulReturn));

    InterlockedIncrement(&m_cRef);

    return ulReturn;
}


///////////////////////////////////////////////////////////////////////////////
/*  Function: DiskQuotaUserBatch::Release

    Description: Decrements object reference count.  If count drops to 0,
        object is deleted.

    Arguments: None.

    Returns: New reference count value.

    Revision History:

    Date        Description                                          Programmer
    --------    ---------------------------------------------------  ----------
    06/06/96    Initial creation.                                    BrianAu
*/
///////////////////////////////////////////////////////////////////////////////
STDMETHODIMP_(ULONG) 
DiskQuotaUserBatch::Release(
    VOID
    )
{
    ULONG ulReturn = m_cRef - 1;

    DBGPRINT((DM_COM, DL_HIGH, TEXT("DiskQuotaUserBatch::Release, 0x%08X  %d -> %d\n"),
             this, ulReturn + 1, ulReturn));

    if (InterlockedDecrement(&m_cRef) == 0)
    {   
        delete this;
        ulReturn = 0;
    }
    return ulReturn;
}



///////////////////////////////////////////////////////////////////////////////
/*  Function: DiskQuotaUserBatch::Destroy

    Description: Destroys the contents of a user batch object and releases
        its FSObject pointer.

    Arguments: None.

    Returns: Nothing.

    Revision History:

    Date        Description                                          Programmer
    --------    ---------------------------------------------------  ----------
    06/06/96    Initial creation.                                    BrianAu
*/
///////////////////////////////////////////////////////////////////////////////
VOID
DiskQuotaUserBatch::Destroy(VOID)
{
    //
    // Remove and release all user object pointers from the batch list.
    //
    RemoveAll();

    if (NULL != m_pFSObject)
    {
        //
        // Release hold on File System object.
        //
        m_pFSObject->Release();
        m_pFSObject = NULL;
    }
}


///////////////////////////////////////////////////////////////////////////////
/*  Function: DiskQuotaUserBatch::Add

    Description: Adds an IDiskQuotaUser interface pointer to the batch list.

    Arguments:
        pUser - Address of IDiskQuotaUser interface.

    Returns:
        NOERROR         - Success.
        E_INVALIDARG    - pUser arg is NULL. 
        E_OUTOFMEMORY   - Couldn't create new node in batch list.
        E_UNEXPECTED    - Unexpected exception.

    Revision History:

    Date        Description                                          Programmer
    --------    ---------------------------------------------------  ----------
    06/06/96    Initial creation.                                    BrianAu
    09/03/96    Add exception handling.                              BrianAu
*/
///////////////////////////////////////////////////////////////////////////////
STDMETHODIMP
DiskQuotaUserBatch::Add(
    PDISKQUOTA_USER pUser
    )
{
    HRESULT hResult = NOERROR;
        
    if (NULL == pUser)
        return E_INVALIDARG;

    try
    {
        m_UserList.Append(pUser);
        //
        // Success.  Increment ref count on object.
        //
        pUser->AddRef();
    }
    catch(CAllocException& e)
    {
        hResult = E_OUTOFMEMORY;
    }
    return hResult;
}


///////////////////////////////////////////////////////////////////////////////
/*  Function: DiskQuotaUserBatch::Remove

    Description: Removes a user pointer from the batch queue.  

    Arguments:
        pUser - Address of IDiskQuotaUser interface for the user object to
            be removed.

    Returns:
        S_OK         - Success.
        S_FALSE      - User not found in batch object.
        E_INVALIDARG - pUser argument is NULL.
        E_UNEXPECTED - Unexpected exception.

    Revision History:

    Date        Description                                          Programmer
    --------    ---------------------------------------------------  ----------
    06/06/96    Initial creation.                                    BrianAu
    09/03/96    Add exception handling.                              BrianAu
*/
///////////////////////////////////////////////////////////////////////////////
HRESULT
DiskQuotaUserBatch::Remove(
    PDISKQUOTA_USER pUser
    )
{
    HRESULT hResult = S_FALSE;  // Assume user not present.
    PDISKQUOTA_USER pRemoved = NULL;

    if (NULL == pUser)
        return E_INVALIDARG;

    m_UserList.Lock();
    INT iUser = m_UserList.Find(pUser);
    if (-1 != iUser)
    {
        try
        {
            DBGASSERT((NULL != m_UserList[iUser]));
            m_UserList[iUser]->Release();
            m_UserList.Delete(iUser);
            hResult = S_OK;
        }
        catch(CAllocException& e)
        {
            hResult = E_OUTOFMEMORY;
        }
    }
    m_UserList.ReleaseLock();

    return hResult;
}


///////////////////////////////////////////////////////////////////////////////
/*  Function: DiskQuotaUserBatch::RemoveAll

    Description: Removes all user pointers from the batch
        list and calling Release() through the removed pointer.  

    Arguments: None.

    Returns:
        NOERROR      - Success.
        E_UNEXPECTED - Unexpected exception.

    Revision History:

    Date        Description                                          Programmer
    --------    ---------------------------------------------------  ----------
    06/06/96    Initial creation.                                    BrianAu
    09/03/96    Add exception handling.                              BrianAu
*/
///////////////////////////////////////////////////////////////////////////////
STDMETHODIMP
DiskQuotaUserBatch::RemoveAll(
    VOID
    )
{
    HRESULT hResult = NOERROR;

    m_UserList.Lock();
    INT cUsers = m_UserList.Count();
    for (INT i = 0; i < cUsers; i++)
    {
        try
        {
            DBGASSERT((NULL != m_UserList[i]));
            m_UserList[i]->Release();
        }
        catch(CAllocException& e)
        {
            hResult = E_OUTOFMEMORY;
        }
    }
    m_UserList.Clear();
    m_UserList.ReleaseLock();

    return hResult;
}


///////////////////////////////////////////////////////////////////////////////
/*  Function: DiskQuotaUserBatch::FlushToDisk

    Description: Writes data for all batched user objects to disk in a single
        NTIOAPI call.  This is the real worker function for the batch object.

    Arguments: None.

    Returns:
        NOERROR       - Success.
        E_OUTOFMEMORY - Insufficient memory.
        E_UNEXPECTED  - Unexpected exception.

    Revision History:

    Date        Description                                          Programmer
    --------    ---------------------------------------------------  ----------
    06/06/96    Initial creation.                                    BrianAu
    09/03/96    Add exception handling.                              BrianAu
    02/27/97    Divided NTFS writes into max 60KB pieces.            BrianAu
                The quota code in NTFS couldn't handle larger
                buffers.  It got into an infinite loop condition
                due to filling of the log.
    07/01/97    Replaced use of PointerList with CArray<>.           BrianAu
                Now use indexes instead of iterators.
*/
///////////////////////////////////////////////////////////////////////////////
STDMETHODIMP
DiskQuotaUserBatch::FlushToDisk(
    VOID
    )
{
    HRESULT hResult                   = NOERROR;
    PFILE_QUOTA_INFORMATION pUserInfo = NULL;
    PDISKQUOTA_USER pUser             = NULL;
    PBYTE pbBatchBuffer               = NULL;
    DWORD cbMinimumSid = FIELD_OFFSET(SID, SubAuthority) + sizeof(LONG);
    INT iOuter = 0;

    //
    // Do nothing if the batch object is empty.
    //
    if (0 == m_UserList.Count())
        return NOERROR;

    m_UserList.Lock();

    try
    {
        //
        // Process the data in 60K chunks using a nested loop.
        //
        while(iOuter < m_UserList.UpperBound())
        {
            //
            // Clone the outer iterator so we can process the next 60K of data.
            // Need two new iterators.  One for counting the bytes and
            // one for transferring data to the write buffer.  They're very small
            // objects and cheap to create.
            //
            INT iCount    = iOuter;
            INT iTransfer = iOuter;

            DWORD cbBatchBuffer = 0;
            DWORD cItemsThisBatch = 0;

            while(cbBatchBuffer < MAX_BATCH_BUFFER_BYTES &&
                  iCount <= m_UserList.UpperBound())

            {
                DWORD cbSid = 0;
                pUser = m_UserList[iCount++];
                pUser->GetSidLength(&cbSid);

                //
                // Total size required for user records.
                //
                cbBatchBuffer += FIELD_OFFSET(FILE_QUOTA_INFORMATION, Sid) + cbSid;

                //
                // Ensure it's quad-word aligned.
                //
                if (cbBatchBuffer & 0x00000007)
                    cbBatchBuffer = (cbBatchBuffer & 0xFFFFFFF8) + 8;

                cItemsThisBatch++;
            }

            //
            // Allocate the buffer.
            //
            pbBatchBuffer = new BYTE[cbBatchBuffer];

            PBYTE pbBatchBufferItem = pbBatchBuffer;
            DWORD cbNextEntryOffset = 0;
            //
            // Now fill in the batch transaction buffer with data from 
            // all of the users in the batch list.
            //
            while(0 != cItemsThisBatch-- &&
                  iTransfer <= m_UserList.UpperBound())
            {
                pUser = m_UserList[iTransfer++];
                pUserInfo = (PFILE_QUOTA_INFORMATION)pbBatchBufferItem;

                pUser->GetSidLength(&pUserInfo->SidLength);

                cbNextEntryOffset = FIELD_OFFSET(FILE_QUOTA_INFORMATION, Sid) + pUserInfo->SidLength;
                //
                // Ensure quad-word alignment.
                //
                if (cbNextEntryOffset & 0x00000007)
                    cbNextEntryOffset = (cbNextEntryOffset & 0xFFFFFFF8) + 8;

                pUserInfo->NextEntryOffset = cbNextEntryOffset;

                pUser->GetQuotaThreshold(&pUserInfo->QuotaThreshold.QuadPart);
                pUser->GetQuotaLimit(&pUserInfo->QuotaLimit.QuadPart);
                pUser->GetSid((PBYTE)&pUserInfo->Sid, pUserInfo->SidLength);

                //
                // These two don't get set but let's provide a known value anyway.
                //
                pUserInfo->ChangeTime.QuadPart = 0;
                pUserInfo->QuotaUsed.QuadPart  = 0;

                pbBatchBufferItem += cbNextEntryOffset;
            }
            pUserInfo->NextEntryOffset = 0;  // Last entry needs a 0 here.
            //
            // Submit the batch to the NTIOAPI for update.
            //
            hResult = m_pFSObject->SetUserQuotaInformation(pbBatchBuffer, cbBatchBuffer);

            //
            // Delete the data buffer.
            //
            delete[] pbBatchBuffer;
            pbBatchBuffer = NULL;

            //
            // Advance the outer iterator to where the transfer iterator left off.
            //
            iOuter = iTransfer;
        }
    }
    catch(CAllocException& e)
    {
        hResult = E_OUTOFMEMORY;
    }

    if (FAILED(hResult))
    {
        //
        // Something failed.  Invalid data cached in user objects.
        // Next request for user data will have to read from disk.
        //
        iOuter = 0;

        while(iOuter <= m_UserList.UpperBound())
        {
            pUser = m_UserList[iOuter++];
            pUser->Invalidate();
        }
    }

    m_UserList.ReleaseLock();

    delete[] pbBatchBuffer;

    return hResult;
}
