2001-04-17 03:54:48 +00:00
|
|
|
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
|
|
|
|
*
|
|
|
|
* The contents of this file are subject to the Mozilla Public
|
|
|
|
* License Version 1.1 (the "License"); you may not use this file
|
|
|
|
* except in compliance with the License. You may obtain a copy of
|
|
|
|
* the License at http://www.mozilla.org/MPL/
|
|
|
|
*
|
|
|
|
* Software distributed under the License is distributed on an "AS
|
|
|
|
* IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
|
|
|
|
* implied. See the License for the specific language governing
|
|
|
|
* rights and limitations under the License.
|
|
|
|
*
|
|
|
|
* The Original Code is nsDiskCacheBlockFile.cpp, released April 12, 2001.
|
|
|
|
*
|
|
|
|
* The Initial Developer of the Original Code is Netscape Communications
|
|
|
|
* Corporation. Portions created by Netscape are
|
|
|
|
* Copyright (C) 2001 Netscape Communications Corporation. All
|
|
|
|
* Rights Reserved.
|
|
|
|
*
|
|
|
|
* Contributor(s):
|
|
|
|
* Gordon Sheridan <gordon@netscape.com>
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "nsCRT.h"
|
|
|
|
#include "nsDiskCacheBlockFile.h"
|
|
|
|
|
|
|
|
/******************************************************************************
|
|
|
|
* nsDiskCacheBlockFile -
|
|
|
|
*****************************************************************************/
|
|
|
|
|
|
|
|
|
|
|
|
/******************************************************************************
|
|
|
|
* Open
|
|
|
|
*****************************************************************************/
|
|
|
|
nsresult
|
|
|
|
nsDiskCacheBlockFile::Open( nsILocalFile * blockFile, PRUint32 blockSize)
|
|
|
|
{
|
2001-05-18 23:24:09 +00:00
|
|
|
PRStatus err = PR_SUCCESS;
|
|
|
|
PRInt32 fileSize;
|
|
|
|
|
2001-04-17 03:54:48 +00:00
|
|
|
mBlockSize = blockSize;
|
|
|
|
|
2001-05-18 23:24:09 +00:00
|
|
|
// open the file
|
|
|
|
nsresult rv = blockFile->OpenNSPRFileDesc(PR_RDWR | PR_CREATE_FILE, 00666, &mFD);
|
|
|
|
if (NS_FAILED(rv)) return rv; // unable to open or create file
|
2001-04-17 03:54:48 +00:00
|
|
|
|
|
|
|
// allocate bit map buffer
|
|
|
|
mBitMap = new PRUint8[kBitMapBytes];
|
|
|
|
if (!mBitMap) {
|
|
|
|
rv = NS_ERROR_OUT_OF_MEMORY;
|
|
|
|
goto error_exit;
|
|
|
|
}
|
|
|
|
|
|
|
|
// check if we just creating the file
|
2001-05-18 23:24:09 +00:00
|
|
|
fileSize = PR_Available(mFD);
|
|
|
|
if (fileSize < 0) {
|
|
|
|
// XXX an error occurred. We could call PR_GetError(), but how would that help?
|
|
|
|
rv = NS_ERROR_UNEXPECTED;
|
|
|
|
goto error_exit;
|
|
|
|
}
|
|
|
|
mEndOfFile = fileSize;
|
2001-04-17 03:54:48 +00:00
|
|
|
if (mEndOfFile == 0) {
|
|
|
|
// initialize bit map and write it
|
|
|
|
nsCRT::zero(mBitMap, kBitMapBytes);
|
2001-05-18 23:24:09 +00:00
|
|
|
PRInt32 bytesWritten = PR_Write(mFD, mBitMap, kBitMapBytes);
|
|
|
|
if (bytesWritten < kBitMapBytes) goto error_exit;
|
2001-04-17 03:54:48 +00:00
|
|
|
mEndOfFile = kBitMapBytes;
|
|
|
|
|
|
|
|
} else if (mEndOfFile < kBitMapBytes) {
|
|
|
|
rv = NS_ERROR_UNEXPECTED; // XXX NS_ERROR_CACHE_INVALID;
|
|
|
|
goto error_exit;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
// read the bit map
|
2001-05-18 23:24:09 +00:00
|
|
|
PRInt32 bytesRead = PR_Read(mFD, mBitMap, kBitMapBytes);
|
|
|
|
if (bytesRead < kBitMapBytes) {
|
2001-04-17 03:54:48 +00:00
|
|
|
rv = NS_ERROR_UNEXPECTED;
|
|
|
|
goto error_exit;
|
|
|
|
}
|
|
|
|
|
|
|
|
// validate block file
|
|
|
|
rv = ValidateFile();
|
|
|
|
if (NS_FAILED(rv)) goto error_exit;
|
|
|
|
}
|
|
|
|
|
|
|
|
return NS_OK;
|
|
|
|
|
|
|
|
error_exit:
|
2001-05-18 23:24:09 +00:00
|
|
|
if (mFD) {
|
|
|
|
(void) PR_Close(mFD);
|
|
|
|
mFD = nsnull;
|
2001-04-17 03:54:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (mBitMap) {
|
|
|
|
delete [] mBitMap;
|
|
|
|
mBitMap = nsnull;
|
|
|
|
}
|
|
|
|
return rv;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/******************************************************************************
|
|
|
|
* Close
|
|
|
|
*****************************************************************************/
|
|
|
|
nsresult
|
|
|
|
nsDiskCacheBlockFile::Close()
|
|
|
|
{
|
2001-05-18 23:24:09 +00:00
|
|
|
if (!mFD) return NS_OK;
|
2001-04-17 03:54:48 +00:00
|
|
|
|
2001-05-18 23:24:09 +00:00
|
|
|
nsresult rv = FlushBitMap();
|
|
|
|
PRStatus err = PR_Close(mFD);
|
|
|
|
mFD = nsnull;
|
2001-04-17 03:54:48 +00:00
|
|
|
|
|
|
|
if (mBitMap) {
|
|
|
|
delete [] mBitMap;
|
|
|
|
mBitMap = nsnull;
|
|
|
|
}
|
2001-05-18 23:24:09 +00:00
|
|
|
|
|
|
|
if (NS_SUCCEEDED(rv) && (err != PR_SUCCESS))
|
|
|
|
rv = NS_ERROR_UNEXPECTED;
|
|
|
|
|
|
|
|
return rv;
|
2001-04-17 03:54:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/******************************************************************************
|
|
|
|
* Trim
|
|
|
|
*
|
|
|
|
* Truncate the block file to the end of the last allocated block.
|
|
|
|
*
|
|
|
|
*****************************************************************************/
|
|
|
|
nsresult
|
|
|
|
nsDiskCacheBlockFile::Trim()
|
|
|
|
{
|
|
|
|
return NS_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/******************************************************************************
|
|
|
|
* AllocateBlocks
|
|
|
|
*
|
|
|
|
* Allocates 1-4 blocks, using a first fit strategy,
|
|
|
|
* so that no group of blocks spans a quad block boundary.
|
|
|
|
*
|
|
|
|
* Returns block number of first block allocated or -1 on failure.
|
|
|
|
*
|
|
|
|
*****************************************************************************/
|
|
|
|
PRInt32
|
|
|
|
nsDiskCacheBlockFile::AllocateBlocks(PRInt32 numBlocks)
|
|
|
|
{
|
2001-05-18 23:24:09 +00:00
|
|
|
if (!mFD) return -1; // NS_ERROR_NOT_AVAILABLE;
|
2001-04-17 03:54:48 +00:00
|
|
|
// return -1 if unable to allocate blocks
|
|
|
|
// PRUint8 mask = (0x01 << numBlocks) - 1;
|
|
|
|
int i = 0;
|
|
|
|
PRUint8 mapByte;
|
|
|
|
PRUint8 mask;
|
|
|
|
|
|
|
|
// presume allocation will succeed
|
|
|
|
PRBool oldValue = mBitMapDirty;
|
|
|
|
mBitMapDirty = PR_TRUE;
|
|
|
|
|
|
|
|
while ((mBitMap[i] == 0xFF) && (i < kBitMapBytes)) ++i; // find first block with a free bit
|
|
|
|
|
|
|
|
if (numBlocks == 1) {
|
|
|
|
if (i < kBitMapBytes) {
|
|
|
|
// don't need a while loop, because we know there's at least 1 free bit in this byte
|
|
|
|
mapByte = ~mBitMap[i]; // flip bits so free bits are 1
|
|
|
|
/*
|
|
|
|
* // Linear search for first free bit in byte
|
|
|
|
* mask = 0x01;
|
|
|
|
* for (int j=0; j<8; ++j, mask <<= 1)
|
|
|
|
* if (mask & mapByte) {mBitMap[i] |= mask; return (i * 8 + j); }
|
|
|
|
*/
|
|
|
|
// Binary search for first free bit in byte
|
|
|
|
PRUint8 bit = 0;
|
|
|
|
if ((mapByte & 0x0F) == 0) { bit |= 4; mapByte >>= 4; }
|
|
|
|
if ((mapByte & 0x03) == 0) { bit |= 2; mapByte >>= 2; }
|
|
|
|
if ((mapByte & 0x01) == 0) { bit |= 1; mapByte >>= 1; }
|
|
|
|
mBitMap[i] |= (PRUint8)1 << bit;
|
|
|
|
return i * 8 + bit;
|
|
|
|
}
|
|
|
|
} else if (numBlocks == 2) {
|
|
|
|
while (i < kBitMapBytes) {
|
|
|
|
mapByte = ~mBitMap[i]; // flip bits so free bits are 1
|
|
|
|
mask = 0x03;
|
|
|
|
// check for fit in lower quad bits
|
|
|
|
if ((mask & mapByte) == mask) { mBitMap[i] |= mask; return (i * 8); } mask <<= 1;
|
|
|
|
if ((mask & mapByte) == mask) { mBitMap[i] |= mask; return (i * 8 + 1); } mask <<= 1;
|
|
|
|
if ((mask & mapByte) == mask) { mBitMap[i] |= mask; return (i * 8 + 2); } mask <<= 2;
|
|
|
|
// check for fit in upper quad bits
|
|
|
|
if ((mask & mapByte) == mask) { mBitMap[i] |= mask; return (i * 8 + 4); } mask <<= 1;
|
|
|
|
if ((mask & mapByte) == mask) { mBitMap[i] |= mask; return (i * 8 + 5); } mask <<= 1;
|
|
|
|
if ((mask & mapByte) == mask) { mBitMap[i] |= mask; return (i * 8 + 6); }
|
|
|
|
++i;
|
|
|
|
}
|
|
|
|
} else if (numBlocks == 3) {
|
|
|
|
while (i < kBitMapBytes) {
|
|
|
|
mapByte = ~mBitMap[i]; // flip bits so free bits are 1
|
|
|
|
mask = 0x07;
|
|
|
|
// check for fit in lower quad bits
|
|
|
|
if ((mask & mapByte) == mask) { mBitMap[i] |= mask; return (i * 8); } mask <<= 1;
|
|
|
|
if ((mask & mapByte) == mask) { mBitMap[i] |= mask; return (i * 8 + 1); } mask <<= 3;
|
|
|
|
// check for fit in upper quad bits
|
|
|
|
if ((mask & mapByte) == mask) { mBitMap[i] |= mask; return (i * 8 + 4); } mask <<= 1;
|
|
|
|
if ((mask & mapByte) == mask) { mBitMap[i] |= mask; return (i * 8 + 5); }
|
|
|
|
++i;
|
|
|
|
}
|
|
|
|
} else if (numBlocks == 4) {
|
|
|
|
while (i < kBitMapBytes) {
|
|
|
|
mapByte = ~mBitMap[i]; // flip bits so free bits are 1
|
|
|
|
mask = 0x0F;
|
|
|
|
// check for fit in lower quad bits
|
|
|
|
if ((mask & mapByte) == mask) { mBitMap[i] |= mask; return (i * 8); } mask <<= 4;
|
|
|
|
// check for fit in upper quad bits
|
|
|
|
if ((mask & mapByte) == mask) { mBitMap[i] |= mask; return (i * 8 + 4); }
|
|
|
|
++i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
mBitMapDirty = oldValue;
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/******************************************************************************
|
|
|
|
* DeallocateBlocks
|
|
|
|
*****************************************************************************/
|
|
|
|
nsresult
|
|
|
|
nsDiskCacheBlockFile::DeallocateBlocks( PRInt32 startBlock, PRInt32 numBlocks)
|
|
|
|
{
|
2001-05-18 23:24:09 +00:00
|
|
|
if (!mFD) return NS_ERROR_NOT_AVAILABLE;
|
2001-04-17 03:54:48 +00:00
|
|
|
if ((startBlock < 0) || (startBlock > kBitMapBytes * 8 - 1) ||
|
|
|
|
(numBlocks < 1) || (numBlocks > 4))
|
|
|
|
return NS_ERROR_ILLEGAL_VALUE;
|
|
|
|
|
|
|
|
PRInt32 startByte = startBlock / 8;
|
|
|
|
PRUint8 startBit = startBlock % 8;
|
|
|
|
|
|
|
|
// make sure requested deallocation doesn't span a byte boundary
|
|
|
|
if ((startBlock + numBlocks - 1) / 8 != startByte) return NS_ERROR_UNEXPECTED;
|
|
|
|
PRUint8 mask = ((0x01 << numBlocks) - 1) << startBit;
|
|
|
|
|
|
|
|
PRUint8 mapByte = ~mBitMap[startByte]; // flip so allocated bits are zero
|
|
|
|
|
|
|
|
// make sure requested deallocation is currently allocated
|
|
|
|
if (mapByte & mask) return NS_ERROR_ABORT;
|
|
|
|
|
|
|
|
mBitMap[startByte] ^= mask; // flips the bits off;
|
|
|
|
mBitMapDirty = PR_TRUE;
|
|
|
|
// XXX rv = FlushBitMap(); // coherency vs. performance
|
|
|
|
return NS_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/******************************************************************************
|
|
|
|
* WriteBlocks
|
|
|
|
*****************************************************************************/
|
|
|
|
nsresult
|
2001-05-18 23:24:09 +00:00
|
|
|
nsDiskCacheBlockFile::WriteBlocks( void * buffer,
|
2001-04-17 03:54:48 +00:00
|
|
|
PRInt32 startBlock,
|
|
|
|
PRInt32 numBlocks)
|
|
|
|
{
|
|
|
|
// presume buffer != nsnull
|
2001-05-18 23:24:09 +00:00
|
|
|
if (!mFD) return NS_ERROR_NOT_AVAILABLE;
|
2001-04-17 03:54:48 +00:00
|
|
|
nsresult rv = VerifyAllocation(startBlock, numBlocks);
|
|
|
|
if (NS_FAILED(rv)) return rv;
|
|
|
|
|
|
|
|
// seek to block position
|
|
|
|
PRInt32 blockPos = kBitMapBytes + startBlock * mBlockSize;
|
2001-05-18 23:24:09 +00:00
|
|
|
PRInt32 filePos = PR_Seek(mFD, blockPos, PR_SEEK_SET);
|
|
|
|
if (filePos != blockPos) return NS_ERROR_UNEXPECTED;
|
2001-04-17 03:54:48 +00:00
|
|
|
|
|
|
|
if (mEndOfFile < (blockPos + numBlocks * mBlockSize))
|
|
|
|
mEndOfFile = (blockPos + numBlocks * mBlockSize);
|
|
|
|
|
|
|
|
// write the blocks
|
2001-05-18 23:24:09 +00:00
|
|
|
PRInt32 bytesToWrite = numBlocks * mBlockSize;
|
|
|
|
PRInt32 bytesWritten = PR_Write(mFD, buffer, bytesToWrite);
|
|
|
|
if (bytesWritten < bytesToWrite) return NS_ERROR_UNEXPECTED;
|
2001-04-17 03:54:48 +00:00
|
|
|
|
|
|
|
// write the bit map and flush the file
|
2001-05-18 23:24:09 +00:00
|
|
|
// XXX rv = FlushBitMap();
|
2001-04-17 03:54:48 +00:00
|
|
|
return rv;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/******************************************************************************
|
|
|
|
* ReadBlocks
|
|
|
|
*****************************************************************************/
|
|
|
|
nsresult
|
2001-05-18 23:24:09 +00:00
|
|
|
nsDiskCacheBlockFile::ReadBlocks( void * buffer,
|
2001-04-17 03:54:48 +00:00
|
|
|
PRInt32 startBlock,
|
|
|
|
PRInt32 numBlocks)
|
|
|
|
{
|
|
|
|
// presume buffer != nsnull
|
2001-05-18 23:24:09 +00:00
|
|
|
if (!mFD) return NS_ERROR_NOT_AVAILABLE;
|
2001-04-17 03:54:48 +00:00
|
|
|
nsresult rv = VerifyAllocation(startBlock, numBlocks);
|
|
|
|
if (NS_FAILED(rv)) return rv;
|
|
|
|
|
|
|
|
// seek to block position
|
|
|
|
PRInt32 blockPos = kBitMapBytes + startBlock * mBlockSize;
|
2001-05-18 23:24:09 +00:00
|
|
|
PRInt32 filePos = PR_Seek(mFD, blockPos, PR_SEEK_SET);
|
|
|
|
if (filePos != blockPos) return NS_ERROR_UNEXPECTED;
|
2001-04-17 03:54:48 +00:00
|
|
|
|
|
|
|
// read the blocks
|
2001-05-18 23:24:09 +00:00
|
|
|
PRInt32 bytesToRead = numBlocks * mBlockSize;
|
|
|
|
PRInt32 bytesRead = PR_Read(mFD, buffer, bytesToRead);
|
|
|
|
if (bytesRead < bytesToRead) return NS_ERROR_UNEXPECTED;
|
2001-04-17 03:54:48 +00:00
|
|
|
|
|
|
|
return rv;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/******************************************************************************
|
|
|
|
* FlushBitMap
|
|
|
|
*****************************************************************************/
|
|
|
|
nsresult
|
|
|
|
nsDiskCacheBlockFile::FlushBitMap()
|
|
|
|
{
|
|
|
|
if (!mBitMapDirty) return NS_OK;
|
|
|
|
|
|
|
|
// seek to bitmap
|
2001-05-18 23:24:09 +00:00
|
|
|
PRInt32 filePos = PR_Seek(mFD, 0, PR_SEEK_SET);
|
|
|
|
if (filePos != 0) return NS_ERROR_UNEXPECTED;
|
2001-04-17 03:54:48 +00:00
|
|
|
|
|
|
|
// write bitmap
|
2001-05-18 23:24:09 +00:00
|
|
|
PRInt32 bytesWritten = PR_Write(mFD, mBitMap, kBitMapBytes);
|
|
|
|
if (bytesWritten < kBitMapBytes) return NS_ERROR_UNEXPECTED;
|
|
|
|
|
|
|
|
PRStatus err = PR_Sync(mFD);
|
|
|
|
if (err != PR_SUCCESS) return NS_ERROR_UNEXPECTED;
|
2001-04-17 03:54:48 +00:00
|
|
|
|
|
|
|
mBitMapDirty = PR_FALSE;
|
|
|
|
return NS_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/******************************************************************************
|
|
|
|
* ValidateFile
|
|
|
|
*
|
|
|
|
* Check size of file against last bit allocated for mBlockSize.
|
|
|
|
*
|
|
|
|
*****************************************************************************/
|
|
|
|
nsresult
|
|
|
|
nsDiskCacheBlockFile::ValidateFile()
|
|
|
|
{
|
2001-05-18 23:24:09 +00:00
|
|
|
PRInt32 estimatedSize = kBitMapBytes;
|
2001-04-17 03:54:48 +00:00
|
|
|
PRInt32 lastBlock = LastBlock();
|
|
|
|
if (lastBlock >= 0)
|
|
|
|
estimatedSize += (lastBlock + 1) * mBlockSize;
|
2001-05-18 23:24:09 +00:00
|
|
|
|
2001-04-17 03:54:48 +00:00
|
|
|
// seek to beginning
|
2001-05-18 23:24:09 +00:00
|
|
|
PRInt32 filePos = PR_Seek(mFD, 0, PR_SEEK_SET);
|
|
|
|
if (filePos != 0) return NS_ERROR_UNEXPECTED;
|
2001-04-17 03:54:48 +00:00
|
|
|
|
2001-05-18 23:24:09 +00:00
|
|
|
PRInt32 fileSize = PR_Available(mFD);
|
|
|
|
|
2001-04-17 03:54:48 +00:00
|
|
|
if (estimatedSize > fileSize)
|
|
|
|
return NS_ERROR_UNEXPECTED;
|
|
|
|
|
|
|
|
return NS_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/******************************************************************************
|
|
|
|
* VerfiyAllocation
|
|
|
|
*
|
|
|
|
* Return values:
|
|
|
|
* NS_OK if all bits are marked allocated
|
|
|
|
* NS_ERROR_ILLEGAL_VALUE if parameters don't obey constraints
|
|
|
|
* NS_ERROR_FAILURE if some or all the bits are marked unallocated
|
|
|
|
*
|
|
|
|
*****************************************************************************/
|
|
|
|
nsresult
|
|
|
|
nsDiskCacheBlockFile::VerifyAllocation( PRInt32 startBlock, PRInt32 numBlocks)
|
|
|
|
{
|
|
|
|
if ((startBlock < 0) || (startBlock > kBitMapBytes * 8 - 1) ||
|
|
|
|
(numBlocks < 1) || (numBlocks > 4))
|
|
|
|
return NS_ERROR_ILLEGAL_VALUE;
|
|
|
|
|
|
|
|
PRInt32 startByte = startBlock / 8;
|
|
|
|
PRUint8 startBit = startBlock % 8;
|
|
|
|
|
|
|
|
// make sure requested deallocation doesn't span a byte boundary
|
|
|
|
if ((startBlock + numBlocks - 1) / 8 != startByte) return NS_ERROR_ILLEGAL_VALUE;
|
|
|
|
PRUint8 mask = ((0x01 << numBlocks) - 1) << startBit;
|
|
|
|
|
|
|
|
// check if all specified blocks are currently allocated
|
|
|
|
if ((mBitMap[startByte] & mask) != mask) return NS_ERROR_FAILURE;
|
|
|
|
|
|
|
|
return NS_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/******************************************************************************
|
|
|
|
* LastBlock
|
|
|
|
*
|
|
|
|
* Return last block allocated or -1 if no blocks are allocated.
|
|
|
|
*
|
|
|
|
*****************************************************************************/
|
|
|
|
PRInt32
|
|
|
|
nsDiskCacheBlockFile::LastBlock()
|
|
|
|
{
|
|
|
|
// search for last byte in mBitMap with allocated bits
|
|
|
|
PRInt32 i = kBitMapBytes;
|
|
|
|
while (--i >= 0) {
|
|
|
|
if (mBitMap[i]) break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (i >= 0) {
|
|
|
|
// binary search to find last allocated bit in byte
|
|
|
|
PRUint8 mapByte = mBitMap[i];
|
|
|
|
PRUint8 lastBit = 7;
|
|
|
|
if ((mapByte & 0xF0) == 0) { lastBit ^= 4; mapByte <<= 4; }
|
|
|
|
if ((mapByte & 0xC0) == 0) { lastBit ^= 2; mapByte <<= 2; }
|
|
|
|
if ((mapByte & 0x80) == 0) { lastBit ^= 1; mapByte <<= 1; }
|
|
|
|
return i * 8 + lastBit;
|
|
|
|
}
|
|
|
|
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
}
|