/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "nsICache.h"
#include "nsCache.h"
#include "nsCacheService.h"
#include "nsCacheEntryDescriptor.h"
#include "nsCacheEntry.h"
#include "nsReadableUtils.h"
#include "nsIOutputStream.h"
#include "nsCRT.h"
#include "nsThreadUtils.h"
#include <algorithm>

#define kMinDecompressReadBufLen 1024
#define kMinCompressWriteBufLen  1024


/******************************************************************************
 * nsAsyncDoomEvent
 *****************************************************************************/

class nsAsyncDoomEvent : public mozilla::Runnable {
public:
    nsAsyncDoomEvent(nsCacheEntryDescriptor *descriptor,
                     nsICacheListener *listener)
    {
        mDescriptor = descriptor;
        mListener = listener;
        mThread = do_GetCurrentThread();
        // We addref the listener here and release it in nsNotifyDoomListener
        // on the callers thread. If posting of nsNotifyDoomListener event fails
        // we leak the listener which is better than releasing it on a wrong
        // thread.
        NS_IF_ADDREF(mListener);
    }

    NS_IMETHOD Run() override
    {
        nsresult status = NS_OK;

        {
            nsCacheServiceAutoLock lock;

            if (mDescriptor->mCacheEntry) {
                status = nsCacheService::gService->DoomEntry_Internal(
                             mDescriptor->mCacheEntry, true);
            } else if (!mDescriptor->mDoomedOnClose) {
                status = NS_ERROR_NOT_AVAILABLE;
            }
        }

        if (mListener) {
            mThread->Dispatch(new nsNotifyDoomListener(mListener, status),
                              NS_DISPATCH_NORMAL);
            // posted event will release the reference on the correct thread
            mListener = nullptr;
        }

        return NS_OK;
    }

private:
    RefPtr<nsCacheEntryDescriptor> mDescriptor;
    nsICacheListener                *mListener;
    nsCOMPtr<nsIThread>              mThread;
};


NS_IMPL_ISUPPORTS(nsCacheEntryDescriptor,
                  nsICacheEntryDescriptor,
                  nsICacheEntryInfo)

nsCacheEntryDescriptor::nsCacheEntryDescriptor(nsCacheEntry * entry,
                                               nsCacheAccessMode accessGranted)
    : mCacheEntry(entry),
      mAccessGranted(accessGranted),
      mOutputWrapper(nullptr),
      mLock("nsCacheEntryDescriptor.mLock"),
      mAsyncDoomPending(false),
      mDoomedOnClose(false),
      mClosingDescriptor(false)
{
    PR_INIT_CLIST(this);
    NS_ADDREF(nsCacheService::GlobalInstance());  // ensure it lives for the lifetime of the descriptor
}


nsCacheEntryDescriptor::~nsCacheEntryDescriptor()
{
    // No need to close if the cache entry has already been severed.  This
    // helps avoid a shutdown assertion (bug 285519) that is caused when
    // consumers end up holding onto these objects past xpcom-shutdown.  It's
    // okay for them to do that because the cache service calls our Close
    // method during xpcom-shutdown, so we don't need to complain about it.
    if (mCacheEntry)
        Close();

    NS_ASSERTION(mInputWrappers.IsEmpty(),
                 "We have still some input wrapper!");
    NS_ASSERTION(!mOutputWrapper, "We have still an output wrapper!");

    nsCacheService * service = nsCacheService::GlobalInstance();
    NS_RELEASE(service);
}


NS_IMETHODIMP
nsCacheEntryDescriptor::GetClientID(char ** result)
{
    NS_ENSURE_ARG_POINTER(result);

    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

    return ClientIDFromCacheKey(*(mCacheEntry->Key()), result);
}


NS_IMETHODIMP
nsCacheEntryDescriptor::GetDeviceID(char ** aDeviceID)
{
    NS_ENSURE_ARG_POINTER(aDeviceID);
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

    const char* deviceID = mCacheEntry->GetDeviceID();
    if (!deviceID) {
        *aDeviceID = nullptr;
        return NS_OK;
    }

    *aDeviceID = NS_strdup(deviceID);
    return *aDeviceID ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
}


NS_IMETHODIMP
nsCacheEntryDescriptor::GetKey(nsACString &result)
{
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

    return ClientKeyFromCacheKey(*(mCacheEntry->Key()), result);
}


NS_IMETHODIMP
nsCacheEntryDescriptor::GetFetchCount(int32_t *result)
{
    NS_ENSURE_ARG_POINTER(result);
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

    *result = mCacheEntry->FetchCount();
    return NS_OK;
}


NS_IMETHODIMP
nsCacheEntryDescriptor::GetLastFetched(uint32_t *result)
{
    NS_ENSURE_ARG_POINTER(result);
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

    *result = mCacheEntry->LastFetched();
    return NS_OK;
}


NS_IMETHODIMP
nsCacheEntryDescriptor::GetLastModified(uint32_t *result)
{
    NS_ENSURE_ARG_POINTER(result);
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

    *result = mCacheEntry->LastModified();
    return NS_OK;
}


NS_IMETHODIMP
nsCacheEntryDescriptor::GetExpirationTime(uint32_t *result)
{
    NS_ENSURE_ARG_POINTER(result);
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

    *result = mCacheEntry->ExpirationTime();
    return NS_OK;
}


NS_IMETHODIMP
nsCacheEntryDescriptor::SetExpirationTime(uint32_t expirationTime)
{
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

    mCacheEntry->SetExpirationTime(expirationTime);
    mCacheEntry->MarkEntryDirty();
    return NS_OK;
}


NS_IMETHODIMP nsCacheEntryDescriptor::IsStreamBased(bool *result)
{
    NS_ENSURE_ARG_POINTER(result);
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

    *result = mCacheEntry->IsStreamData();
    return NS_OK;
}

NS_IMETHODIMP nsCacheEntryDescriptor::GetPredictedDataSize(int64_t *result)
{
    NS_ENSURE_ARG_POINTER(result);
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry) return NS_ERROR_NOT_AVAILABLE;

    *result = mCacheEntry->PredictedDataSize();
    return NS_OK;
}

NS_IMETHODIMP nsCacheEntryDescriptor::SetPredictedDataSize(int64_t
                                                           predictedSize)
{
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

    mCacheEntry->SetPredictedDataSize(predictedSize);
    return NS_OK;
}

NS_IMETHODIMP nsCacheEntryDescriptor::GetDataSize(uint32_t *result)
{
    NS_ENSURE_ARG_POINTER(result);
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

    const char* val = mCacheEntry->GetMetaDataElement("uncompressed-len");
    if (!val) {
        *result = mCacheEntry->DataSize();
    } else {
        *result = atol(val);
    }

    return NS_OK;
}


NS_IMETHODIMP nsCacheEntryDescriptor::GetStorageDataSize(uint32_t *result)
{
    NS_ENSURE_ARG_POINTER(result);
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

    *result = mCacheEntry->DataSize();

    return NS_OK;
}


nsresult
nsCacheEntryDescriptor::RequestDataSizeChange(int32_t deltaSize)
{
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

    nsresult  rv;
    rv = nsCacheService::OnDataSizeChange(mCacheEntry, deltaSize);
    if (NS_SUCCEEDED(rv)) {
        // XXX review for signed/unsigned math errors
        uint32_t  newDataSize = mCacheEntry->DataSize() + deltaSize;
        mCacheEntry->SetDataSize(newDataSize);
        mCacheEntry->TouchData();
    }
    return rv;
}


NS_IMETHODIMP
nsCacheEntryDescriptor::SetDataSize(uint32_t dataSize)
{
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

    // XXX review for signed/unsigned math errors
    int32_t  deltaSize = dataSize - mCacheEntry->DataSize();

    nsresult  rv;
    rv = nsCacheService::OnDataSizeChange(mCacheEntry, deltaSize);
    // this had better be NS_OK, this call instance is advisory for memory cache objects
    if (NS_SUCCEEDED(rv)) {
        // XXX review for signed/unsigned math errors
        uint32_t  newDataSize = mCacheEntry->DataSize() + deltaSize;
        mCacheEntry->SetDataSize(newDataSize);
        mCacheEntry->TouchData();
    } else {
        NS_WARNING("failed SetDataSize() on memory cache object!");
    }
    
    return rv;
}


NS_IMETHODIMP
nsCacheEntryDescriptor::OpenInputStream(uint32_t offset, nsIInputStream ** result)
{
    NS_ENSURE_ARG_POINTER(result);

    nsInputStreamWrapper* cacheInput = nullptr;
    {
        nsCacheServiceAutoLock lock;
        if (!mCacheEntry)                  return NS_ERROR_NOT_AVAILABLE;
        if (!mCacheEntry->IsStreamData())  return NS_ERROR_CACHE_DATA_IS_NOT_STREAM;

        // Don't open any new stream when closing descriptor or clearing entries
        if (mClosingDescriptor || nsCacheService::GetClearingEntries())
            return NS_ERROR_NOT_AVAILABLE;

        // ensure valid permissions
        if (!(mAccessGranted & nsICache::ACCESS_READ))
            return NS_ERROR_CACHE_READ_ACCESS_DENIED;

        const char *val;
        val = mCacheEntry->GetMetaDataElement("uncompressed-len");
        if (val) {
            cacheInput = new nsDecompressInputStreamWrapper(this, offset);
        } else {
            cacheInput = new nsInputStreamWrapper(this, offset);
        }
        if (!cacheInput) return NS_ERROR_OUT_OF_MEMORY;

        mInputWrappers.AppendElement(cacheInput);
    }

    NS_ADDREF(*result = cacheInput);
    return NS_OK;
}

NS_IMETHODIMP
nsCacheEntryDescriptor::OpenOutputStream(uint32_t offset, nsIOutputStream ** result)
{
    NS_ENSURE_ARG_POINTER(result);

    nsOutputStreamWrapper* cacheOutput = nullptr;
    {
        nsCacheServiceAutoLock lock;
        if (!mCacheEntry)                  return NS_ERROR_NOT_AVAILABLE;
        if (!mCacheEntry->IsStreamData())  return NS_ERROR_CACHE_DATA_IS_NOT_STREAM;

        // Don't open any new stream when closing descriptor or clearing entries
        if (mClosingDescriptor || nsCacheService::GetClearingEntries())
            return NS_ERROR_NOT_AVAILABLE;

        // ensure valid permissions
        if (!(mAccessGranted & nsICache::ACCESS_WRITE))
            return NS_ERROR_CACHE_WRITE_ACCESS_DENIED;

        int32_t compressionLevel = nsCacheService::CacheCompressionLevel();
        const char *val;
        val = mCacheEntry->GetMetaDataElement("uncompressed-len");
        if ((compressionLevel > 0) && val) {
            cacheOutput = new nsCompressOutputStreamWrapper(this, offset);
        } else {
            // clear compression flag when compression disabled - see bug 715198
            if (val) {
                mCacheEntry->SetMetaDataElement("uncompressed-len", nullptr);
            }
            cacheOutput = new nsOutputStreamWrapper(this, offset);
        }
        if (!cacheOutput) return NS_ERROR_OUT_OF_MEMORY;

        mOutputWrapper = cacheOutput;
    }

    NS_ADDREF(*result = cacheOutput);
    return NS_OK;
}


NS_IMETHODIMP
nsCacheEntryDescriptor::GetCacheElement(nsISupports ** result)
{
    NS_ENSURE_ARG_POINTER(result);
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)                 return NS_ERROR_NOT_AVAILABLE;
    if (mCacheEntry->IsStreamData())  return NS_ERROR_CACHE_DATA_IS_STREAM;

    NS_IF_ADDREF(*result = mCacheEntry->Data());
    return NS_OK;
}


NS_IMETHODIMP
nsCacheEntryDescriptor::SetCacheElement(nsISupports * cacheElement)
{
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)                 return NS_ERROR_NOT_AVAILABLE;
    if (mCacheEntry->IsStreamData())  return NS_ERROR_CACHE_DATA_IS_STREAM;

    return nsCacheService::SetCacheElement(mCacheEntry, cacheElement);
}


NS_IMETHODIMP
nsCacheEntryDescriptor::GetAccessGranted(nsCacheAccessMode *result)
{
    NS_ENSURE_ARG_POINTER(result);
    *result = mAccessGranted;
    return NS_OK;
}


NS_IMETHODIMP
nsCacheEntryDescriptor::GetStoragePolicy(nsCacheStoragePolicy *result)
{
    NS_ENSURE_ARG_POINTER(result);
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;
    
    *result = mCacheEntry->StoragePolicy();
    return NS_OK;
}


NS_IMETHODIMP
nsCacheEntryDescriptor::SetStoragePolicy(nsCacheStoragePolicy policy)
{
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;
    // XXX validate policy against session?
    
    bool        storageEnabled = false;
    storageEnabled = nsCacheService::IsStorageEnabledForPolicy_Locked(policy);
    if (!storageEnabled)    return NS_ERROR_FAILURE;

    // Don't change the storage policy of entries we can't write
    if (!(mAccessGranted & nsICache::ACCESS_WRITE))
        return NS_ERROR_NOT_AVAILABLE;
    
    // Don't allow a cache entry to move from memory-only to anything else
    if (mCacheEntry->StoragePolicy() == nsICache::STORE_IN_MEMORY &&
        policy != nsICache::STORE_IN_MEMORY)
        return NS_ERROR_NOT_AVAILABLE;
        
    mCacheEntry->SetStoragePolicy(policy);
    mCacheEntry->MarkEntryDirty();
    return NS_OK;
}


NS_IMETHODIMP
nsCacheEntryDescriptor::GetFile(nsIFile ** result)
{
    NS_ENSURE_ARG_POINTER(result);
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

    return nsCacheService::GetFileForEntry(mCacheEntry, result);
}


NS_IMETHODIMP
nsCacheEntryDescriptor::GetSecurityInfo(nsISupports ** result)
{
    NS_ENSURE_ARG_POINTER(result);
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

    *result = mCacheEntry->SecurityInfo();
    NS_IF_ADDREF(*result);
    return NS_OK;
}


NS_IMETHODIMP
nsCacheEntryDescriptor::SetSecurityInfo(nsISupports * securityInfo)
{
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

    mCacheEntry->SetSecurityInfo(securityInfo);
    mCacheEntry->MarkEntryDirty();
    return NS_OK;
}


NS_IMETHODIMP
nsCacheEntryDescriptor::Doom()
{
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

    return nsCacheService::DoomEntry(mCacheEntry);
}


NS_IMETHODIMP
nsCacheEntryDescriptor::DoomAndFailPendingRequests(nsresult status)
{
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

    return NS_ERROR_NOT_IMPLEMENTED;
}


NS_IMETHODIMP
nsCacheEntryDescriptor::AsyncDoom(nsICacheListener *listener)
{
    bool asyncDoomPending;
    {
        mozilla::MutexAutoLock lock(mLock);
        asyncDoomPending = mAsyncDoomPending;
        mAsyncDoomPending = true;
    }

    if (asyncDoomPending) {
        // AsyncDoom was already called. Notify listener if it is non-null,
        // otherwise just return success.
        if (listener) {
            nsresult rv = NS_DispatchToCurrentThread(
                new nsNotifyDoomListener(listener, NS_ERROR_NOT_AVAILABLE));
            if (NS_SUCCEEDED(rv))
                NS_IF_ADDREF(listener);
            return rv;
        }
        return NS_OK;
    }

    nsCOMPtr<nsIRunnable> event = new nsAsyncDoomEvent(this, listener);
    return nsCacheService::DispatchToCacheIOThread(event);
}


NS_IMETHODIMP
nsCacheEntryDescriptor::MarkValid()
{
    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

    nsresult  rv = nsCacheService::ValidateEntry(mCacheEntry);
    return rv;
}


NS_IMETHODIMP
nsCacheEntryDescriptor::Close()
{
    RefPtr<nsOutputStreamWrapper> outputWrapper;
    nsTArray<RefPtr<nsInputStreamWrapper> > inputWrappers;

    {
        nsCacheServiceAutoLock lock;
        if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

        // Make sure no other stream can be opened
        mClosingDescriptor = true;
        outputWrapper = mOutputWrapper;
        for (size_t i = 0; i < mInputWrappers.Length(); i++)
            inputWrappers.AppendElement(mInputWrappers[i]);
    }

    // Call Close() on the streams outside the lock since it might need to call
    // methods that grab the cache service lock, e.g. compressed output stream
    // when it finalizes the entry
    if (outputWrapper) {
        if (NS_FAILED(outputWrapper->Close())) {
            NS_WARNING("Dooming entry because Close() failed!!!");
            Doom();
        }
        outputWrapper = nullptr;
    }

    for (uint32_t i = 0 ; i < inputWrappers.Length() ; i++)
        inputWrappers[i]->Close();

    inputWrappers.Clear();

    nsCacheServiceAutoLock lock;
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

    // XXX perhaps closing descriptors should clear/sever transports

    // tell nsCacheService we're going away
    nsCacheService::CloseDescriptor(this);
    NS_ASSERTION(mCacheEntry == nullptr, "mCacheEntry not null");

    return NS_OK;
}


NS_IMETHODIMP
nsCacheEntryDescriptor::GetMetaDataElement(const char *key, char **result)
{
    NS_ENSURE_ARG_POINTER(key);
    *result = nullptr;

    nsCacheServiceAutoLock lock;
    NS_ENSURE_TRUE(mCacheEntry, NS_ERROR_NOT_AVAILABLE);

    const char *value;

    value = mCacheEntry->GetMetaDataElement(key);
    if (!value) return NS_ERROR_NOT_AVAILABLE;

    *result = NS_strdup(value);
    if (!*result) return NS_ERROR_OUT_OF_MEMORY;

    return NS_OK;
}


NS_IMETHODIMP
nsCacheEntryDescriptor::SetMetaDataElement(const char *key, const char *value)
{
    NS_ENSURE_ARG_POINTER(key);

    nsCacheServiceAutoLock lock;
    NS_ENSURE_TRUE(mCacheEntry, NS_ERROR_NOT_AVAILABLE);

    // XXX allow null value, for clearing key?

    nsresult rv = mCacheEntry->SetMetaDataElement(key, value);
    if (NS_SUCCEEDED(rv))
        mCacheEntry->TouchMetaData();
    return rv;
}


NS_IMETHODIMP
nsCacheEntryDescriptor::VisitMetaData(nsICacheMetaDataVisitor * visitor)
{
    nsCacheServiceAutoLock lock;
    // XXX check callers, we're calling out of module
    NS_ENSURE_ARG_POINTER(visitor);
    if (!mCacheEntry)  return NS_ERROR_NOT_AVAILABLE;

    return mCacheEntry->VisitMetaDataElements(visitor);
}


/******************************************************************************
 * nsCacheInputStream - a wrapper for nsIInputStream keeps the cache entry
 *                      open while referenced.
 ******************************************************************************/

NS_IMPL_ADDREF(nsCacheEntryDescriptor::nsInputStreamWrapper)
NS_IMETHODIMP_(MozExternalRefCountType)
nsCacheEntryDescriptor::nsInputStreamWrapper::Release()
{
    // Holding a reference to descriptor ensures that cache service won't go
    // away. Do not grab cache service lock if there is no descriptor.
    RefPtr<nsCacheEntryDescriptor> desc;

    {
        mozilla::MutexAutoLock lock(mLock);
        desc = mDescriptor;
    }

    if (desc)
        nsCacheService::Lock();

    nsrefcnt count;
    NS_PRECONDITION(0 != mRefCnt, "dup release");
    count = --mRefCnt;
    NS_LOG_RELEASE(this, count, "nsCacheEntryDescriptor::nsInputStreamWrapper");

    if (0 == count) {
        // don't use desc here since mDescriptor might be already nulled out
        if (mDescriptor) {
            NS_ASSERTION(mDescriptor->mInputWrappers.Contains(this),
                         "Wrapper not found in array!");
            mDescriptor->mInputWrappers.RemoveElement(this);
        }

        if (desc)
            nsCacheService::Unlock();

        mRefCnt = 1;
        delete (this);
        return 0;
    }

    if (desc)
        nsCacheService::Unlock();

    return count;
}

NS_INTERFACE_MAP_BEGIN(nsCacheEntryDescriptor::nsInputStreamWrapper)
  NS_INTERFACE_MAP_ENTRY(nsIInputStream)
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END_THREADSAFE

nsresult nsCacheEntryDescriptor::
nsInputStreamWrapper::LazyInit()
{
    // Check if we have the descriptor. If not we can't even grab the cache
    // lock since it is not ensured that the cache service still exists.
    if (!mDescriptor)
        return NS_ERROR_NOT_AVAILABLE;

    nsCacheServiceAutoLock lock;

    nsCacheAccessMode mode;
    nsresult rv = mDescriptor->GetAccessGranted(&mode);
    if (NS_FAILED(rv)) return rv;

    NS_ENSURE_TRUE(mode & nsICache::ACCESS_READ, NS_ERROR_UNEXPECTED);

    nsCacheEntry* cacheEntry = mDescriptor->CacheEntry();
    if (!cacheEntry) return NS_ERROR_NOT_AVAILABLE;

    rv = nsCacheService::OpenInputStreamForEntry(cacheEntry, mode,
                                                 mStartOffset,
                                                 getter_AddRefs(mInput));

    CACHE_LOG_DEBUG(("nsInputStreamWrapper::LazyInit "
                      "[entry=%p, wrapper=%p, mInput=%p, rv=%d]",
                      mDescriptor, this, mInput.get(), int(rv)));

    if (NS_FAILED(rv)) return rv;

    mInitialized = true;
    return NS_OK;
}

nsresult nsCacheEntryDescriptor::
nsInputStreamWrapper::EnsureInit()
{
    if (mInitialized) {
        NS_ASSERTION(mDescriptor, "Bad state");
        return NS_OK;
    }

    return LazyInit();
}

void nsCacheEntryDescriptor::
nsInputStreamWrapper::CloseInternal()
{
    mLock.AssertCurrentThreadOwns();
    if (!mDescriptor) {
        NS_ASSERTION(!mInitialized, "Bad state");
        NS_ASSERTION(!mInput, "Bad state");
        return;
    }

    nsCacheServiceAutoLock lock;

    if (mDescriptor) {
        mDescriptor->mInputWrappers.RemoveElement(this);
        nsCacheService::ReleaseObject_Locked(mDescriptor);
        mDescriptor = nullptr;
    }
    mInitialized = false;
    mInput = nullptr;
}

nsresult nsCacheEntryDescriptor::
nsInputStreamWrapper::Close()
{
    mozilla::MutexAutoLock lock(mLock);

    return Close_Locked();
}

nsresult nsCacheEntryDescriptor::
nsInputStreamWrapper::Close_Locked()
{
    nsresult rv = EnsureInit();
    if (NS_SUCCEEDED(rv)) {
        rv = mInput->Close();
    } else {
        NS_ASSERTION(!mInput,
                     "Shouldn't have mInput when EnsureInit() failed");
    }

    // Call CloseInternal() even when EnsureInit() failed, e.g. in case we are
    // closing streams with nsCacheService::CloseAllStream()
    CloseInternal();
    return rv;
}

nsresult nsCacheEntryDescriptor::
nsInputStreamWrapper::Available(uint64_t *avail)
{
    mozilla::MutexAutoLock lock(mLock);

    nsresult rv = EnsureInit();
    if (NS_FAILED(rv)) return rv;

    return mInput->Available(avail);
}

nsresult nsCacheEntryDescriptor::
nsInputStreamWrapper::Read(char *buf, uint32_t count, uint32_t *countRead)
{
    mozilla::MutexAutoLock lock(mLock);

    return Read_Locked(buf, count, countRead);
}

nsresult nsCacheEntryDescriptor::
nsInputStreamWrapper::Read_Locked(char *buf, uint32_t count, uint32_t *countRead)
{
    nsresult rv = EnsureInit();
    if (NS_SUCCEEDED(rv))
        rv = mInput->Read(buf, count, countRead);

    CACHE_LOG_DEBUG(("nsInputStreamWrapper::Read "
                      "[entry=%p, wrapper=%p, mInput=%p, rv=%d]",
                      mDescriptor, this, mInput.get(), rv));

    return rv;
}

nsresult nsCacheEntryDescriptor::
nsInputStreamWrapper::ReadSegments(nsWriteSegmentFun writer, void *closure,
                                   uint32_t count, uint32_t *countRead)
{
    // cache stream not buffered
    return NS_ERROR_NOT_IMPLEMENTED;
}

nsresult nsCacheEntryDescriptor::
nsInputStreamWrapper::IsNonBlocking(bool *result)
{
    // cache streams will never return NS_BASE_STREAM_WOULD_BLOCK
    *result = false;
    return NS_OK;
}


/******************************************************************************
 * nsDecompressInputStreamWrapper - an input stream wrapper that decompresses
 ******************************************************************************/

NS_IMPL_ADDREF(nsCacheEntryDescriptor::nsDecompressInputStreamWrapper)
NS_IMETHODIMP_(MozExternalRefCountType)
nsCacheEntryDescriptor::nsDecompressInputStreamWrapper::Release()
{
    // Holding a reference to descriptor ensures that cache service won't go
    // away. Do not grab cache service lock if there is no descriptor.
    RefPtr<nsCacheEntryDescriptor> desc;

    {
        mozilla::MutexAutoLock lock(mLock);
        desc = mDescriptor;
    }

    if (desc)
        nsCacheService::Lock();

    nsrefcnt count;
    NS_PRECONDITION(0 != mRefCnt, "dup release");
    count = --mRefCnt;
    NS_LOG_RELEASE(this, count,
                   "nsCacheEntryDescriptor::nsDecompressInputStreamWrapper");

    if (0 == count) {
        // don't use desc here since mDescriptor might be already nulled out
        if (mDescriptor) {
            NS_ASSERTION(mDescriptor->mInputWrappers.Contains(this),
                         "Wrapper not found in array!");
            mDescriptor->mInputWrappers.RemoveElement(this);
        }

        if (desc)
            nsCacheService::Unlock();

        mRefCnt = 1;
        delete (this);
        return 0;
    }

    if (desc)
        nsCacheService::Unlock();

    return count;
}

NS_INTERFACE_MAP_BEGIN(nsCacheEntryDescriptor::nsDecompressInputStreamWrapper)
  NS_INTERFACE_MAP_ENTRY(nsIInputStream)
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END_THREADSAFE

NS_IMETHODIMP nsCacheEntryDescriptor::
nsDecompressInputStreamWrapper::Read(char *    buf, 
                                     uint32_t  count, 
                                     uint32_t *countRead)
{
    mozilla::MutexAutoLock lock(mLock);

    int zerr = Z_OK;
    nsresult rv = NS_OK;

    if (!mStreamInitialized) {
        rv = InitZstream();
        if (NS_FAILED(rv)) {
            return rv;
        }
    }

    mZstream.next_out = (Bytef*)buf;
    mZstream.avail_out = count;

    if (mReadBufferLen < count) {
        // Allocate a buffer for reading from the input stream. This will
        // determine the max number of compressed bytes read from the
        // input stream at one time. Making the buffer size proportional
        // to the request size is not necessary, but helps minimize the
        // number of read requests to the input stream.
        uint32_t newBufLen = std::max(count, (uint32_t)kMinDecompressReadBufLen);
        unsigned char* newBuf;
        newBuf = (unsigned char*)moz_xrealloc(mReadBuffer,
            newBufLen);
        if (newBuf) {
            mReadBuffer = newBuf;
            mReadBufferLen = newBufLen;
        }
        if (!mReadBuffer) {
            mReadBufferLen = 0;
            return NS_ERROR_OUT_OF_MEMORY;
        }
    }

    // read and inflate data until the output buffer is full, or
    // there is no more data to read
    while (NS_SUCCEEDED(rv) &&
           zerr == Z_OK && 
           mZstream.avail_out > 0 &&
           count > 0) {
        if (mZstream.avail_in == 0) {
            rv = nsInputStreamWrapper::Read_Locked((char*)mReadBuffer,
                                                   mReadBufferLen,
                                                   &mZstream.avail_in);
            if (NS_FAILED(rv) || !mZstream.avail_in) {
                break;
            }
            mZstream.next_in = mReadBuffer;
        }
        zerr = inflate(&mZstream, Z_NO_FLUSH);
        if (zerr == Z_STREAM_END) {
            // The compressed data may have been stored in multiple
            // chunks/streams. To allow for this case, re-initialize 
            // the inflate stream and continue decompressing from 
            // the next byte.
            Bytef * saveNextIn = mZstream.next_in;
            unsigned int saveAvailIn = mZstream.avail_in;
            Bytef * saveNextOut = mZstream.next_out;
            unsigned int saveAvailOut = mZstream.avail_out;
            inflateReset(&mZstream);
            mZstream.next_in = saveNextIn;
            mZstream.avail_in = saveAvailIn;
            mZstream.next_out = saveNextOut;
            mZstream.avail_out = saveAvailOut;
            zerr = Z_OK;
        } else if (zerr != Z_OK) {
            rv = NS_ERROR_INVALID_CONTENT_ENCODING;
        }
    }
    if (NS_SUCCEEDED(rv)) {
        *countRead = count - mZstream.avail_out;
    }
    return rv;
}

nsresult nsCacheEntryDescriptor::
nsDecompressInputStreamWrapper::Close()
{
    mozilla::MutexAutoLock lock(mLock);

    if (!mDescriptor)
        return NS_ERROR_NOT_AVAILABLE;

    EndZstream();
    if (mReadBuffer) {
        free(mReadBuffer);
        mReadBuffer = 0;
        mReadBufferLen = 0;
    }
    return nsInputStreamWrapper::Close_Locked();
}

nsresult nsCacheEntryDescriptor::
nsDecompressInputStreamWrapper::InitZstream()
{
    if (!mDescriptor)
        return NS_ERROR_NOT_AVAILABLE;

    if (mStreamEnded)
        return NS_ERROR_FAILURE;

    // Initialize zlib inflate stream
    mZstream.zalloc = Z_NULL;
    mZstream.zfree = Z_NULL;
    mZstream.opaque = Z_NULL;
    mZstream.next_out = Z_NULL;
    mZstream.avail_out = 0;
    mZstream.next_in = Z_NULL;
    mZstream.avail_in = 0;
    if (inflateInit(&mZstream) != Z_OK) {
        return NS_ERROR_FAILURE;
    }
    mStreamInitialized = true;
    return NS_OK;
}

nsresult nsCacheEntryDescriptor::
nsDecompressInputStreamWrapper::EndZstream()
{
    if (mStreamInitialized && !mStreamEnded) {
        inflateEnd(&mZstream);
        mStreamInitialized = false;
        mStreamEnded = true;
    }
    return NS_OK;
}


/******************************************************************************
 * nsOutputStreamWrapper - a wrapper for nsIOutputstream to track the amount of
 *                         data written to a cache entry.
 *                       - also keeps the cache entry open while referenced.
 ******************************************************************************/

NS_IMPL_ADDREF(nsCacheEntryDescriptor::nsOutputStreamWrapper)
NS_IMETHODIMP_(MozExternalRefCountType)
nsCacheEntryDescriptor::nsOutputStreamWrapper::Release()
{
    // Holding a reference to descriptor ensures that cache service won't go
    // away. Do not grab cache service lock if there is no descriptor.
    RefPtr<nsCacheEntryDescriptor> desc;

    {
        mozilla::MutexAutoLock lock(mLock);
        desc = mDescriptor;
    }

    if (desc)
        nsCacheService::Lock();

    nsrefcnt count;
    NS_PRECONDITION(0 != mRefCnt, "dup release");
    count = --mRefCnt;
    NS_LOG_RELEASE(this, count,
                   "nsCacheEntryDescriptor::nsOutputStreamWrapper");

    if (0 == count) {
        // don't use desc here since mDescriptor might be already nulled out
        if (mDescriptor)
            mDescriptor->mOutputWrapper = nullptr;

        if (desc)
            nsCacheService::Unlock();

        mRefCnt = 1;
        delete (this);
        return 0;
    }

    if (desc)
        nsCacheService::Unlock();

    return count;
}

NS_INTERFACE_MAP_BEGIN(nsCacheEntryDescriptor::nsOutputStreamWrapper)
  NS_INTERFACE_MAP_ENTRY(nsIOutputStream)
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END_THREADSAFE

nsresult nsCacheEntryDescriptor::
nsOutputStreamWrapper::LazyInit()
{
    // Check if we have the descriptor. If not we can't even grab the cache
    // lock since it is not ensured that the cache service still exists.
    if (!mDescriptor)
        return NS_ERROR_NOT_AVAILABLE;

    nsCacheServiceAutoLock lock;

    nsCacheAccessMode mode;
    nsresult rv = mDescriptor->GetAccessGranted(&mode);
    if (NS_FAILED(rv)) return rv;

    NS_ENSURE_TRUE(mode & nsICache::ACCESS_WRITE, NS_ERROR_UNEXPECTED);

    nsCacheEntry* cacheEntry = mDescriptor->CacheEntry();
    if (!cacheEntry) return NS_ERROR_NOT_AVAILABLE;

    NS_ASSERTION(mOutput == nullptr, "mOutput set in LazyInit");

    nsCOMPtr<nsIOutputStream> stream;
    rv = nsCacheService::OpenOutputStreamForEntry(cacheEntry, mode, mStartOffset,
                                                  getter_AddRefs(stream));
    if (NS_FAILED(rv))
        return rv;

    nsCacheDevice* device = cacheEntry->CacheDevice();
    if (device) {
        // the entry has been truncated to mStartOffset bytes, inform device
        int32_t size = cacheEntry->DataSize();
        rv = device->OnDataSizeChange(cacheEntry, mStartOffset - size);
        if (NS_SUCCEEDED(rv))
            cacheEntry->SetDataSize(mStartOffset);
    } else {
        rv = NS_ERROR_NOT_AVAILABLE;
    }

    // If anything above failed, clean up internal state and get out of here
    // (see bug #654926)...
    if (NS_FAILED(rv)) {
        nsCacheService::ReleaseObject_Locked(stream.forget().take());
        mDescriptor->mOutputWrapper = nullptr;
        nsCacheService::ReleaseObject_Locked(mDescriptor);
        mDescriptor = nullptr;
        mInitialized = false;
        return rv;
    }

    mOutput = stream;
    mInitialized = true;
    return NS_OK;
}

nsresult nsCacheEntryDescriptor::
nsOutputStreamWrapper::EnsureInit()
{
    if (mInitialized) {
        NS_ASSERTION(mDescriptor, "Bad state");
        return NS_OK;
    }

    return LazyInit();
}

nsresult nsCacheEntryDescriptor::
nsOutputStreamWrapper::OnWrite(uint32_t count)
{
    if (count > INT32_MAX)  return NS_ERROR_UNEXPECTED;
    return mDescriptor->RequestDataSizeChange((int32_t)count);
}

void nsCacheEntryDescriptor::
nsOutputStreamWrapper::CloseInternal()
{
    mLock.AssertCurrentThreadOwns();
    if (!mDescriptor) {
        NS_ASSERTION(!mInitialized, "Bad state");
        NS_ASSERTION(!mOutput, "Bad state");
        return;
    }

    nsCacheServiceAutoLock lock;

    if (mDescriptor) {
        mDescriptor->mOutputWrapper = nullptr;
        nsCacheService::ReleaseObject_Locked(mDescriptor);
        mDescriptor = nullptr;
    }
    mInitialized = false;
    mOutput = nullptr;
}


NS_IMETHODIMP nsCacheEntryDescriptor::
nsOutputStreamWrapper::Close()
{
    mozilla::MutexAutoLock lock(mLock);

    return Close_Locked();
}

nsresult nsCacheEntryDescriptor::
nsOutputStreamWrapper::Close_Locked()
{
    nsresult rv = EnsureInit();
    if (NS_SUCCEEDED(rv)) {
        rv = mOutput->Close();
    } else {
        NS_ASSERTION(!mOutput,
                     "Shouldn't have mOutput when EnsureInit() failed");
    }

    // Call CloseInternal() even when EnsureInit() failed, e.g. in case we are
    // closing streams with nsCacheService::CloseAllStream()
    CloseInternal();
    return rv;
}

NS_IMETHODIMP nsCacheEntryDescriptor::
nsOutputStreamWrapper::Flush()
{
    mozilla::MutexAutoLock lock(mLock);

    nsresult rv = EnsureInit();
    if (NS_FAILED(rv)) return rv;

    return mOutput->Flush();
}

NS_IMETHODIMP nsCacheEntryDescriptor::
nsOutputStreamWrapper::Write(const char * buf,
                             uint32_t     count,
                             uint32_t *   result)
{
    mozilla::MutexAutoLock lock(mLock);
    return Write_Locked(buf, count, result);
}

nsresult nsCacheEntryDescriptor::
nsOutputStreamWrapper::Write_Locked(const char * buf,
                                    uint32_t count,
                                    uint32_t * result)
{
    nsresult rv = EnsureInit();
    if (NS_FAILED(rv)) return rv;

    rv = OnWrite(count);
    if (NS_FAILED(rv)) return rv;

    return mOutput->Write(buf, count, result);
}

NS_IMETHODIMP nsCacheEntryDescriptor::
nsOutputStreamWrapper::WriteFrom(nsIInputStream * inStr,
                                 uint32_t         count,
                                 uint32_t *       result)
{
    return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP nsCacheEntryDescriptor::
nsOutputStreamWrapper::WriteSegments(nsReadSegmentFun  reader,
                                     void *            closure,
                                     uint32_t          count,
                                     uint32_t *        result)
{
    return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP nsCacheEntryDescriptor::
nsOutputStreamWrapper::IsNonBlocking(bool *result)
{
    // cache streams will never return NS_BASE_STREAM_WOULD_BLOCK
    *result = false;
    return NS_OK;
}


/******************************************************************************
 * nsCompressOutputStreamWrapper - an output stream wrapper that compresses
 *   data before it is written
 ******************************************************************************/

NS_IMPL_ADDREF(nsCacheEntryDescriptor::nsCompressOutputStreamWrapper)
NS_IMETHODIMP_(MozExternalRefCountType)
nsCacheEntryDescriptor::nsCompressOutputStreamWrapper::Release()
{
    // Holding a reference to descriptor ensures that cache service won't go
    // away. Do not grab cache service lock if there is no descriptor.
    RefPtr<nsCacheEntryDescriptor> desc;

    {
        mozilla::MutexAutoLock lock(mLock);
        desc = mDescriptor;
    }

    if (desc)
        nsCacheService::Lock();

    nsrefcnt count;
    NS_PRECONDITION(0 != mRefCnt, "dup release");
    count = --mRefCnt;
    NS_LOG_RELEASE(this, count,
                   "nsCacheEntryDescriptor::nsCompressOutputStreamWrapper");

    if (0 == count) {
        // don't use desc here since mDescriptor might be already nulled out
        if (mDescriptor)
            mDescriptor->mOutputWrapper = nullptr;

        if (desc)
            nsCacheService::Unlock();

        mRefCnt = 1;
        delete (this);
        return 0;
    }

    if (desc)
        nsCacheService::Unlock();

    return count;
}

NS_INTERFACE_MAP_BEGIN(nsCacheEntryDescriptor::nsCompressOutputStreamWrapper)
  NS_INTERFACE_MAP_ENTRY(nsIOutputStream)
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END_THREADSAFE

NS_IMETHODIMP nsCacheEntryDescriptor::
nsCompressOutputStreamWrapper::Write(const char * buf,
                                     uint32_t     count,
                                     uint32_t *   result)
{
    mozilla::MutexAutoLock lock(mLock);

    int zerr = Z_OK;
    nsresult rv = NS_OK;

    if (!mStreamInitialized) {
        rv = InitZstream();
        if (NS_FAILED(rv)) {
            return rv;
        }
    }

    if (!mWriteBuffer) {
        // Once allocated, this buffer is referenced by the zlib stream and
        // cannot be grown. We use 2x(initial write request) to approximate
        // a stream buffer size proportional to request buffers.
        mWriteBufferLen = std::max(count*2, (uint32_t)kMinCompressWriteBufLen);
        mWriteBuffer = (unsigned char*)moz_xmalloc(mWriteBufferLen);
        if (!mWriteBuffer) {
            mWriteBufferLen = 0;
            return NS_ERROR_OUT_OF_MEMORY;
        }
        mZstream.next_out = mWriteBuffer;
        mZstream.avail_out = mWriteBufferLen;
    }

    // Compress (deflate) the requested buffer. Keep going
    // until the entire buffer has been deflated.
    mZstream.avail_in = count;
    mZstream.next_in = (Bytef*)buf;
    while (mZstream.avail_in > 0) {
        zerr = deflate(&mZstream, Z_NO_FLUSH);
        if (zerr == Z_STREAM_ERROR) {
            deflateEnd(&mZstream);
            mStreamEnded = true;
            mStreamInitialized = false;
            return NS_ERROR_FAILURE;
        }
        // Note: Z_BUF_ERROR is non-fatal and sometimes expected here.

        // If the compression stream output buffer is filled, write
        // it out to the underlying stream wrapper.
        if (mZstream.avail_out == 0) {
            rv = WriteBuffer();
            if (NS_FAILED(rv)) {
                deflateEnd(&mZstream);
                mStreamEnded = true;
                mStreamInitialized = false;
                return rv;
            }
        }
    }
    *result = count;
    mUncompressedCount += *result;
    return NS_OK;
}

NS_IMETHODIMP nsCacheEntryDescriptor::
nsCompressOutputStreamWrapper::Close()
{
    mozilla::MutexAutoLock lock(mLock);

    if (!mDescriptor)
        return NS_ERROR_NOT_AVAILABLE;

    nsresult retval = NS_OK;
    nsresult rv;
    int zerr = 0;

    if (mStreamInitialized) {
        // complete compression of any data remaining in the zlib stream
        do {
            zerr = deflate(&mZstream, Z_FINISH);
            rv = WriteBuffer();
            if (NS_FAILED(rv))
                retval = rv;
        } while (zerr == Z_OK && rv == NS_OK);
        deflateEnd(&mZstream);
        mStreamInitialized = false;
    }
    // Do not allow to initialize stream after calling Close().
    mStreamEnded = true;

    // In some rare cases, flushing the zlib stream can take too long
    // and we lose our cache entry in the meantime. Do another check
    // and bail if so.
    if (!mDescriptor)
        return NS_ERROR_NOT_AVAILABLE;

    if (mDescriptor->CacheEntry()) {
        nsAutoCString uncompressedLenStr;
        rv = mDescriptor->GetMetaDataElement("uncompressed-len",
                                             getter_Copies(uncompressedLenStr));
        if (NS_SUCCEEDED(rv)) {
            int32_t oldCount = uncompressedLenStr.ToInteger(&rv);
            if (NS_SUCCEEDED(rv)) {
                mUncompressedCount += oldCount;
            }
        }
        uncompressedLenStr.Adopt(0);
        uncompressedLenStr.AppendInt(mUncompressedCount);
        rv = mDescriptor->SetMetaDataElement("uncompressed-len",
            uncompressedLenStr.get());
        if (NS_FAILED(rv))
            retval = rv;
    }

    if (mWriteBuffer) {
        free(mWriteBuffer);
        mWriteBuffer = 0;
        mWriteBufferLen = 0;
    }

    rv = nsOutputStreamWrapper::Close_Locked();
    if (NS_FAILED(rv))
        retval = rv;

    return retval;
}

nsresult nsCacheEntryDescriptor::
nsCompressOutputStreamWrapper::InitZstream()
{
    if (!mDescriptor)
        return NS_ERROR_NOT_AVAILABLE;

    if (mStreamEnded)
        return NS_ERROR_FAILURE;

    // Determine compression level: Aggressive compression
    // may impact performance on mobile devices, while a
    // lower compression level still provides substantial
    // space savings for many text streams.
    int32_t compressionLevel = nsCacheService::CacheCompressionLevel();

    // Initialize zlib deflate stream
    mZstream.zalloc = Z_NULL;
    mZstream.zfree = Z_NULL;
    mZstream.opaque = Z_NULL;
    if (deflateInit2(&mZstream, compressionLevel, Z_DEFLATED,
                     MAX_WBITS, 8, Z_DEFAULT_STRATEGY) != Z_OK) {
        return NS_ERROR_FAILURE;
    }
    mZstream.next_in = Z_NULL;
    mZstream.avail_in = 0;

    mStreamInitialized = true;

    return NS_OK;
}

nsresult nsCacheEntryDescriptor::
nsCompressOutputStreamWrapper::WriteBuffer()
{
    uint32_t bytesToWrite = mWriteBufferLen - mZstream.avail_out;
    uint32_t result = 0;
    nsresult rv = nsCacheEntryDescriptor::nsOutputStreamWrapper::Write_Locked(
        (const char *)mWriteBuffer, bytesToWrite, &result);
    mZstream.next_out = mWriteBuffer;
    mZstream.avail_out = mWriteBufferLen;
    return rv;
}

