userlibandfileserver/fileserver/sfat32/sl_vfat.cpp
branchRCL_3
changeset 62 4a8fed1c0ef6
parent 15 4122176ea935
child 80 597aaf25e343
--- a/userlibandfileserver/fileserver/sfat32/sl_vfat.cpp	Tue Feb 02 01:24:03 2010 +0200
+++ b/userlibandfileserver/fileserver/sfat32/sl_vfat.cpp	Sat Feb 20 00:10:51 2010 +0200
@@ -29,11 +29,14 @@
 // use second half of ISO Latin 1 character set for extended chars
 const TUint KExtendedCharStart=0x80;
 const TUint KExtendedCharEnd=0xff;
+const TUint KMaxVFatEntries = 21; ///< Max possible number of entries in the VFAT entryset
 
-LOCAL_C TBool IsLegalChar(TChar aCharacter,TBool aAllowWildChars,TBool aUseExtendedChars=EFalse,TBool aInScanDrive=EFalse)
-//
-// Returns ETrue if aCharacter is legal inside a dos filename
-//
+
+//-----------------------------------------------------------------------------
+/**
+    Returns ETrue if aCharacter is legal inside a dos filename
+*/
+static TBool IsLegalChar(TChar aCharacter,TBool aAllowWildChars,TBool aUseExtendedChars=EFalse,TBool aInScanDrive=EFalse)
 	{
 	if ((aCharacter==KMatchOne) || (aCharacter==KMatchAny))
 		return(aAllowWildChars);
@@ -46,7 +49,8 @@
 	return LocaleUtils::IsLegalShortNameCharacter(aCharacter,aUseExtendedChars);
 	}
 
-LOCAL_C void ReplaceFirstCharacterIfClashesWithE5L(TDes8& aShortName)
+//-----------------------------------------------------------------------------
+static void ReplaceFirstCharacterIfClashesWithE5L(TDes8& aShortName)
 	{
 	if (0 < aShortName.Length() && aShortName[0] == KEntryErasedMarker)
 		{
@@ -54,7 +58,8 @@
 		}
 	}
 
-LOCAL_C void ReplaceIllegalCharactersL(TDes& aLongName, TUint aCharacterToReplaceWith)
+//-----------------------------------------------------------------------------
+static void ReplaceIllegalCharactersL(TDes& aLongName, TUint aCharacterToReplaceWith)
 	{
 	TBool alreadyFoundExtensionDelimiter=EFalse;
 
@@ -130,10 +135,11 @@
 		}
 	}
 
+//-----------------------------------------------------------------------------
+/**
+    Create a legal shortname from aLongName
+*/
 TShortName DoGenerateShortNameL(const TDesC& aLongName,TInt& aNum,TBool aUseTildeSelectively)
-//
-// Create a legal shortname from aLongName
-//
 	{
 
 	TFileName longName(aLongName);
@@ -259,7 +265,7 @@
 	return shortName;
 	}
 
-
+//-----------------------------------------------------------------------------
 /**
 Check whether a Dos name is legal or not.
 
@@ -271,7 +277,6 @@
 
 @return ETrue if the name is a legal DOS one.
 */
-
 static TBool DoCheckLegalDosName(const TDesC& aName, TBool anAllowWildCards, TBool aUseExtendedChars, TBool aInScanDrive, TBool aAllowLowerCase, TBool aIsForFileCreation)
 	{
     const TInt count=aName.Length();
@@ -388,6 +393,7 @@
 	return ETrue;
 	}
 
+//-----------------------------------------------------------------------------
 /**
     Check whether a Dos name is legal or not. Unicode version
     parameters and return value absolutely the same as in DoCheckLegalDosName()
@@ -395,18 +401,19 @@
 TBool IsLegalDosName(const TDesC16& aName, TBool anAllowWildCards, TBool aUseExtendedChars, TBool aInScanDrive, TBool aAllowLowerCase, TBool aIsForFileCreation)
 	{
 
-	__PRINT(_L("IsLegalDosName 16"));
+	//__PRINT(_L("IsLegalDosName 16"));
 
     return DoCheckLegalDosName(aName, anAllowWildCards, aUseExtendedChars, aInScanDrive, aAllowLowerCase, aIsForFileCreation);	
 	}
 
+//-----------------------------------------------------------------------------
+/**
+    Returns ETrue and the entryPos of aName if found or EFalse
+*/
 TBool CFatMountCB::FindShortNameL(const TShortName& aName,TEntryPos& anEntryPos)
-//
-// Returns ETrue and the entryPos of aName if found or EFalse
-//
 	{
 	
-	__PRINT(_L("VFAT::CFatMountCB::FindShortNameL"));	
+	__PRINT(_L("CFatMountCB::FindShortNameL"));	
 	TFatDirEntry fatEntry;
 	TInt count=0;
 	FOREVER
@@ -428,27 +435,29 @@
 	return EFalse;
 	}
 	
+//-----------------------------------------------------------------------------
+/**
+    Returns ETrue if aName is unique, EFalse if a matching name is found.
+*/
 TBool CFatMountCB::IsUniqueNameL(const TShortName& aName,TInt aDirCluster)
-//
-// Returns ETrue if aName is unique, EFalse if a matching name is found.
-//
 	{
 
-	__PRINT(_L("VFAT::CFatMountCB::IsUniqueNameL"));	
+	__PRINT(_L("CFatMountCB::IsUniqueNameL"));	
 	TEntryPos entryPos(aDirCluster,0);
 	if (FindShortNameL(aName,entryPos))
 		return(EFalse);
 	return(ETrue);
 	}
 
+//-----------------------------------------------------------------------------
+/**
+    A legal dos name has been typed that clashes with a computer generated shortname
+    Change the shortname to something else.
+*/
 void CFatMountCB::ReplaceClashingNameL(const TShortName& aNewName,const TEntryPos& anEntryPos)
-//
-// A legal dos name has been typed that clashes with a computer generated shortname
-// Change the shortname to something else.
-//
 	{
 
-	__PRINT(_L("VFAT::CFatMountCB::ReplaceClashingNameL"));	
+	__PRINT(_L("CFatMountCB::ReplaceClashingNameL"));	
 	TFatDirEntry entry;
 	ReadDirEntryL(anEntryPos,entry);
 	__ASSERT_ALWAYS(entry.IsEndOfDirectory()==EFalse,User::Leave(KErrCorrupt));
@@ -469,14 +478,22 @@
 		}
 	}
 
+//-----------------------------------------------------------------------------
+/**
+    Generate a legal dos filename as an alias for aName.
+    @return ETrue if aName is a legal dos name.
+*/
 TBool CFatMountCB::GenerateShortNameL(TInt aDirCluster,const TDesC& aName,TShortName& aGeneratedName, TBool aForceRandomize)
-//
-// Generate a legal dos filename as an alias for aName.
-// Returns ETrue if aName is a legal dos name.
-//
 	{
 
-	__PRINT(_L("VFAT::CFatMountCB::GenerateShortNameL"));
+	__PRINT1(_L("CFatMountCB::GenerateShortNameL() cl:%d"), aDirCluster);
+
+    if(!ClusterNumberValid(aDirCluster))
+        {
+        ASSERT(0);
+        User::Leave(KErrCorrupt);
+        }
+
 	// Given the long file-name "ABCDEFGHI.TXT", EPOC used to generate short 
 	// file-names in the following pecking order:
 	//     "ABCDEFGH.TXT",
@@ -546,142 +563,307 @@
 
 	}
 
-void TFatDirEntry::InitializeAsVFat(TUint8 aCheckSum)
-//
-// Initialize a FAT entry as a VFAT filename
-//
+
+//-----------------------------------------------------------------------------
+/**
+    Write up to KMaxVFatEntryName unicode chars from aName to the entry
+    @param  aName       long file name part that will be converted into the VFAT entryset
+    @param  aLen        length of the remaining name
+    @param  aCheckSum   DOS entry name checksum.
+*/
+void TFatDirEntry::SetVFatEntry(const TDesC& aName, TUint aLen, TUint8 aCheckSum)
 	{
-
-	Mem::Fill(this,sizeof(SFatDirEntry),0xFF);
+    //-- initialise some VFAT entry specific fields
 	iData[0x0B]=0x0F;
 	iData[0x0C]=0x00; iData[0x0D]=aCheckSum;
 	iData[0x1A]=0x00; iData[0x1B]=0x00;
-	}
-
-void TFatDirEntry::SetVFatEntry(const TDesC& aName,TInt aLen)
-//
-// Write up to KMaxVFatEntryName unicode chars from aName to the entry
-//
-	{
 
 	TInt rem=aName.Length()-aLen;
 	TPtrC section(aName.Ptr()+aLen,Min(rem,KMaxVFatEntryName));
 	TBuf16<KMaxVFatEntryName> buf16;
 	buf16.Copy(section);
+	
 	if (rem<KMaxVFatEntryName)
 		{
 		rem++;
 		buf16.ZeroTerminate();
 		buf16.SetLength(rem); // Zero termination doesn't increase the buf length
 		}
+
 	TUint8 orderNo=(TUint8)(aLen/KMaxVFatEntryName+1);
 	TInt s=Min(rem,5);
 	Mem::Copy(&iData[0x01],buf16.Ptr(),s*2);//Copy up to 10 bytes of buf16 into iData
+	
 	TInt offset=s;
 	rem-=s;
 	s=Min(rem,6);
 	Mem::Copy(&iData[0x0E],buf16.Ptr()+offset,s*2);
+	
 	offset+=s;
 	rem-=s;
+	
 	s=Min(rem,2);
 	Mem::Copy(&iData[0x1C],buf16.Ptr()+offset,s*2);
 	rem-=s;
+
 	if (rem==0)
 		orderNo|=0x40;
+
 	iData[0]=orderNo;
 	}
 
+
+//-----------------------------------------------------------------------------
+/**
+    Read KMaxVFatEntryName unicode chars from the entry
+*/
 void TFatDirEntry::ReadVFatEntry(TDes16& aBuf) const
-//
-// Read KMaxVFatEntryName unicode chars from the entry
-//
 	{
-
 	aBuf.SetLength(KMaxVFatEntryName);
 	Mem::Copy(&aBuf[0],&iData[0x01],5*2);
 	Mem::Copy(&aBuf[5],&iData[0x0E],6*2);
 	Mem::Copy(&aBuf[11],&iData[0x1C],2*2);
 	}
 
-void CFatMountCB::WriteDirEntryL(TEntryPos& aPos,const TFatDirEntry& aFatDirEntry,const TDesC& aLongName)
-//
-// Write a VFAT directory entry to disk at position aPos - leave aPos refering to the dos entry
-// Assumes sufficient space has been created for it by AddDirEntry.
-//
-	{
+//-----------------------------------------------------------------------------
+/**
+    Write a VFAT directory entry set to disk at position aPos - leave aPos refering to the dos entry
+    Assumes sufficient space has been created for it by AddDirEntry.
+    For Rugged FAT mode bulk writing of the whole entryset is OK. If the entryset fits into media atomic write unit, the 
+    write is transactional anyway. if the entryset is split between media atomic write units, the part of it with the DOS
+    entry is written last; if this write operation fails, the artifact would be just several orphaned VFAT entries; 
 
-	__PRINT(_L("VFAT::CFatMountCB::WriteDirEntryL"));	
+    @param  aPos            in: specifies the entryste start position. out: points to the last (DOS) entry in the created entryset
+    @param  aFatDirEntry    aDosEntry DOS entry
+    @param  aLongName       VFAT entry long name
+*/
+void CFatMountCB::WriteDirEntryL(TEntryPos& aPos, const TFatDirEntry& aDosEntry, const TDesC& aLongName)
+    {
+    __PRINT2(_L("CFatMountCB::WriteDirEntryL() cl:%d, pos:%d"), aPos.Cluster(), aPos.Pos());   
 	__ASSERT_DEBUG(aLongName.Length(),Fault(EVFatNoLongName));
-	TEntryPos startPos(aPos.iCluster,aPos.iPos);
-	TUint8  localBuf[KDefaultSectorSize];
-	TUint8 cksum=CalculateShortNameCheckSum(aFatDirEntry.Name());
-	TInt numEntries=NumberOfVFatEntries(aLongName.Length())-1; // Excluding dos entry
-	// see if all entries written to one sector
-	// single sector writes not supported if sector size>default size 
-	TInt dosOffset=numEntries<<KSizeOfFatDirEntryLog2;
-	TInt absolutePos=(aPos.iCluster<<ClusterSizeLog2())+ClusterRelativePos(aPos.iPos);
-	TBool isSameSector=(((absolutePos^(absolutePos+dosOffset))>>SectorSizeLog2())==0 && ((TUint)(1<<SectorSizeLog2())<=KDefaultSectorSize));
-	TFatDirEntry vFatEntry;
-	vFatEntry.InitializeAsVFat(cksum);
-	TInt offset=0;
+
+    //-- scratch buffer for whole VFAT entryset. Max number of entries in it is 21 entry or 672 bytes. 
+    //-- in the worst case the entryset can span across 3 clusters (512 bytes per cluster)
+    //-- Using the scratch buffer is not ideal, but write-back directory cache isn't in place yet
+    const TUint KBufSize = 680;
+    TUint8  scratchBuf[KBufSize];
+
+    const TUint8    cksum=CalculateShortNameCheckSum(aDosEntry.Name());
+    TUint           numEntries=NumberOfVFatEntries(aLongName.Length())-1; // Excluding dos entry
+
+    ASSERT(KBufSize >= ((numEntries+1)<<KSizeOfFatDirEntryLog2));
+    TEntryPos startPos;
+
+    for(;;)
+        {
+        TInt posInBuf = 0;
+        startPos = aPos;
+        TBool movedCluster = EFalse;
+
+        while(numEntries)
+            {
+            TFatDirEntry* pEntry = (TFatDirEntry*)(&scratchBuf[posInBuf]);
+            pEntry->SetVFatEntry(aLongName, KMaxVFatEntryName*(numEntries-1), cksum); //KMaxVFatEntryName=13  
+
+            posInBuf += KSizeOfFatDirEntry;
+            MoveToNextEntryL(aPos);
+
+            numEntries--;
+            movedCluster = (startPos.Cluster() != aPos.Cluster()); //-- if moved to another cluser, need to flush buffer
+            
+            if(!numEntries || movedCluster)
+                break; //-- VFAT entryset is completed
+            }
+    
+        if(movedCluster)
+            {
+            DirWriteL(startPos, TPtrC8(&scratchBuf[0], posInBuf));
+            continue;
+            }    
+
+        if(!numEntries)
+            {//-- need to append DOS entry
+            Mem::Copy(&scratchBuf[posInBuf], &aDosEntry, KSizeOfFatDirEntry);    
+            posInBuf+= KSizeOfFatDirEntry;
+            DirWriteL(startPos, TPtrC8(&scratchBuf[0], posInBuf));
+            break;
+            }
+    
+        }//for(;;)
+    }
+
+
+
+//---------------------------------------------------------------------------------
+
+void CFatMountCB::DoEraseEntrySetChunkL(const TEntrySetChunkInfo& aEntrySetChunk)
+    {
+
+    //-- scratch buffer for whole VFAT entryset. Max number of entries in it is 21 entry or 672 bytes. 
+    //-- in the worst case the entryset can span across 3 clusters (512 bytes per cluster)
+    //-- Using the scratch buffer is not ideal, but write-back directory cache isn't in place yet
+
+    const TUint KBufSize = 680;
+    TBuf8<KBufSize> scratchBuf;
+
+    TUint numEntries = aEntrySetChunk.iNumEntries;
+
+    ASSERT(numEntries >0 && numEntries <= KMaxVFatEntries);
+    const TUint32 KChunkLen = numEntries << KSizeOfFatDirEntryLog2;
+
+    DirReadL(aEntrySetChunk.iEntryPos, KChunkLen, scratchBuf);
+    
+    TInt posInBuf = 0;
 	while (numEntries--)
 		{
-		vFatEntry.SetVFatEntry(aLongName,KMaxVFatEntryName*numEntries);//	KMaxVFatEntryName=13
-		if(isSameSector)
-			{
-			Mem::Copy(&localBuf[offset],&vFatEntry,KSizeOfFatDirEntry);
-			offset+=KSizeOfFatDirEntry;
-			MoveToNextEntryL(aPos);
-			}
-		else
-			{
-			WriteDirEntryL(aPos,vFatEntry);
-			MoveToNextEntryL(aPos);
-			}
-		}
-	if(isSameSector)
-		{
-		Mem::Copy(&localBuf[offset],&aFatDirEntry,KSizeOfFatDirEntry);
-		
-        //-- use special interface to access FAT directory file
-        DirWriteL(startPos,TPtrC8(&localBuf[0],dosOffset+KSizeOfFatDirEntry));
+        TFatDirEntry* pEntry = (TFatDirEntry*)(scratchBuf.Ptr()+posInBuf);
+        pEntry->SetErased();
+        posInBuf += KSizeOfFatDirEntry;
         }
-	else
-		WriteDirEntryL(aPos,aFatDirEntry);
-	}
+            
+    DirWriteL(aEntrySetChunk.iEntryPos, scratchBuf);
+    }
 
+//---------------------------------------------------------------------------------
+/**
+    Erase whole VFAT entryset. 
+    For Rugged FAT the situation is more complicated: we need firstly delete the DOS entry _atomically_ i.e. if this operation fails,
+    the whole VFAT entryset won't be broken.  Deleting VFAT entries doesn't require the atomic media writes; DOS entry contains necessary
+    information about data stream.
+    
+    @param aPos         position of the entryset start in the directory.
+    @param aFirstEntry  first entry in the entryset, it can be DOS entry
+    
+*/
 void CFatMountCB::EraseDirEntryL(TEntryPos aPos,const TFatDirEntry& aFirstEntry)
-//
-// Mark all entries in a VFat directory entry as erased
-//
 	{
-	__PRINT(_L("VFAT::CFatMountCB::EraseDirEntryL"));
-	TInt numEntries=0;
+    __PRINT2(_L("CFatMountCB::EraseDirEntryL() cl:%d, offset:%d"), aPos.Cluster(), aPos.Pos());
+
+    TUint numEntries=0;
 	if (aFirstEntry.IsVFatEntry())
+        {
 		numEntries=aFirstEntry.NumFollowing();
-	if(IsRuggedFSys()&&numEntries)
-		{
-		TInt count=numEntries;
-		TEntryPos pos=aPos;
-		while(count--)
-			MoveToNextEntryL(pos);
-		EraseDirEntryL(pos);
-		numEntries--;
+        numEntries++; //-- take into account the last DOS entry
 		}
-	FOREVER
-		{
+    else
+        {//-- we are deleting a single DOS entry. This is an atomic operation.
 		EraseDirEntryL(aPos);
-		if (!numEntries--)
-			break;
-		MoveToNextEntryL(aPos);
-		}
-	}
+        return;        
+        }
+
+    ASSERT(numEntries > 1 && numEntries <= KMaxVFatEntries);
+
+    TEntrySetChunkInfo chunksInfo[TEntrySetChunkInfo::KMaxChunks];
+
+    //-- 1. check if the entryset fits into a unit of write ganularity. This will be 1 sector for rugged FAT or 1 cluster otherwise
+
+    TUint32 MaxWriteGranularityLog2;
+    
+    if(IsRuggedFSys())
+        {
+        MaxWriteGranularityLog2 = AtomicWriteGranularityLog2();
+        }
+    else if(IsRootDir(aPos))
+        {//-- root dir. for FAT12/16 is a special case, it is not made of clusters. it's unit is 1 sector.
+        MaxWriteGranularityLog2 = KDefSectorSzLog2;
+        }
+    else
+        {//-- minimal unit size will be a cluster
+        MaxWriteGranularityLog2 = ClusterSizeLog2();
+        }
+    
+
+        {
+        const TUint64 KEntrySetStartPos = MakeLinAddrL(aPos);
+        const TUint64 KEntrySetLogicalEndPos = KEntrySetStartPos + (numEntries << KSizeOfFatDirEntryLog2);
+ 
+        const TUint64 KBlockEndPos = ((KEntrySetLogicalEndPos-1) >> MaxWriteGranularityLog2) << MaxWriteGranularityLog2;
+        const TUint64 KBlockStartPos = (KEntrySetStartPos >> MaxWriteGranularityLog2) << MaxWriteGranularityLog2;
+        
+        if(KBlockEndPos == KBlockStartPos)
+            {//-- whole entryet is in the same block; the whole entryset erase operation will be atomic for Rugged/non-rugged FAT
+            chunksInfo[0].iEntryPos = aPos;
+            chunksInfo[0].iNumEntries = numEntries;
+            DoEraseEntrySetChunkL(chunksInfo[0]);
+            return;
+            }
+
+        }
+
+    //-- the entryset is split on max. 3 parts between units of write granularity (see MaxWriteGranularityLog2).
+    ASSERT(numEntries > 1 && numEntries <= KMaxVFatEntries);
+
+    TInt cntChunk = 1; //-- there is at least 1 entries chunk
+    TEntrySetChunkInfo* pChunkInfo = chunksInfo;
+
+    //-- collect information about dir. entry chunks that reside in different units of write granularity
+    for(;;)
+        {
+        TBool movedUnit = EFalse;
 
+        pChunkInfo->iEntryPos   = aPos;
+        pChunkInfo->iNumEntries = 0;
+        
+        const TUint64 KChunkStartPos = MakeLinAddrL(aPos);
+        const TUint64 KChunkBlockStartPos = (KChunkStartPos >> MaxWriteGranularityLog2) << MaxWriteGranularityLog2;
+        const TUint64 KChunkBlockEndPos   = (KChunkBlockStartPos-1) + (1<<MaxWriteGranularityLog2);
 
+        while(numEntries)
+            {
+            pChunkInfo->iNumEntries++;
+		MoveToNextEntryL(aPos);
+            
+            numEntries--;
+            const TUint64 currPos = MakeLinAddrL(aPos);
+            movedUnit = !(currPos >= KChunkBlockStartPos && currPos <= KChunkBlockEndPos); 
+
+            if(!numEntries || movedUnit)
+                {
+                break; 
+                }
+
+            }
+
+        if(movedUnit && numEntries)
+            {//-- move to the next unit of write granularity
+            ++pChunkInfo;
+            ++cntChunk;
+            ASSERT(cntChunk <= TEntrySetChunkInfo::KMaxChunks);
+            continue;
+            }    
+
+        
+        ASSERT(!numEntries);
+        break;
+        }
+
+    //-- now do bulk deletion, write data based on collected entries chunks.
+    ASSERT(cntChunk > 0);
+ 
+    //-- if it is a rugged FAT, we need to delete DOS entry first; it will be in the last chunk.
+    if(IsRuggedFSys())
+        {
+        const TInt dosEntryChunk = cntChunk-1;
+        DoEraseEntrySetChunkL(chunksInfo[dosEntryChunk]);
+        cntChunk--;
+        }
+
+    //-- it is also possible to joint entryset chunks together here if they belong to the same cluster. 
+    //-- the atomic write here is not required. 
+
+    //-- erase the rest of entries in reamining chunks.
+    for(TInt i=0; i<cntChunk; ++i)
+        {
+        DoEraseEntrySetChunkL(chunksInfo[i]);
+        }
+
+}
+
+//---------------------------------------------------------------------------------
+/**
+    Convert the volume label using the algorithm specified in the current locale-DLL.
+*/
 void  LocaleUtils::ConvertFromUnicodeL(TDes8& aForeign, const TDesC16& aUnicode, TFatUtilityFunctions::TOverflowAction aOverflowAction)
-//
-// Convert the volume label using the algorithm specified in the current locale-DLL.
-//
 	{
 	if(aOverflowAction == TFatUtilityFunctions::EOverflowActionLeave)
 		{
@@ -692,11 +874,11 @@
 		GetCodePage().ConvertFromUnicodeL(aForeign, aUnicode, TCodePageUtils::EOverflowActionTruncate);
 		}
 	}
-
+//---------------------------------------------------------------------------------
+/**
+    Convert the volume label using the algorithm specified in the current locale-DLL.
+*/
 void  LocaleUtils::ConvertToUnicodeL(TDes16& aUnicode, const TDesC8& aForeign, TFatUtilityFunctions::TOverflowAction aOverflowAction)
-//
-// Convert the volume label using the algorithm specified in the current locale-DLL.
-//
 	{
 	if(aOverflowAction == TFatUtilityFunctions::EOverflowActionLeave)
 		{
@@ -708,10 +890,20 @@
 		}
 	}
 
+//---------------------------------------------------------------------------------
+/**
+    Convert the volume label using the algorithm specified in the current locale-DLL.
+*/
 TBool LocaleUtils::IsLegalShortNameCharacter(TUint aCharacter,TBool aUseExtendedChars)
-//
-// Convert the volume label using the algorithm specified in the current locale-DLL.
-//
 	{
 	return GetCodePage().IsLegalShortNameCharacter(aCharacter, aUseExtendedChars);
 	}
+
+
+
+
+
+
+
+
+