securityanddataprivacytools/securitytools/certapp/encdec/encdec.cpp
changeset 0 2c201484c85f
child 8 35751d3474b7
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/securityanddataprivacytools/securitytools/certapp/encdec/encdec.cpp	Wed Jul 08 11:25:26 2009 +0100
@@ -0,0 +1,858 @@
+/*
+* Copyright (c) 2008-2009 Nokia Corporation and/or its subsidiary(-ies).
+* All rights reserved.
+* This component and the accompanying materials are made available
+* under the terms of the License "Eclipse Public License v1.0"
+* which accompanies this distribution, and is available
+* at the URL "http://www.eclipse.org/legal/epl-v10.html".
+*
+* Initial Contributors:
+* Nokia Corporation - initial contribution.
+*
+* Contributors:
+*
+* Description: 
+*
+*/
+
+
+#include "encdec.h"
+#include <sstream>
+#include <errno.h>
+#include <s32ucmp.h>
+#include "stringconv.h"
+#include <stdio.h>
+#include "x509utils.h"
+
+RDecodeReadStream::RDecodeReadStream(CFileStore *aStore, RReadStream &aReadStream)
+	: iStore(aStore), iCertBaseName(), iReadStream(aReadStream), iHumanReadable(false), iToken(), 
+	  iPrefetchedTokenIsValid(false), iPrefetchedToken()
+{
+}
+
+RDecodeReadStream::RDecodeReadStream(const std::string &aCertBaseName, RReadStream &aReadStream)
+	: iStore(0), iCertBaseName(aCertBaseName), iReadStream(aReadStream), iHumanReadable(true), iToken(), 
+	  iPrefetchedTokenIsValid(false), iPrefetchedToken()
+{
+}
+
+void RDecodeReadStream::RawRead(void *aPtr, TUint32 aLength)
+{
+	iReadStream.ReadL((TUint8 *)aPtr, aLength);
+}
+
+void RDecodeReadStream::CheckName(const std::string &aExpected)
+{
+	ReadNextToken();
+
+	if(iToken != aExpected)
+		{
+		dbg << Log::Indent() << "Expected token '" << aExpected <<"' but got token '" << iToken << "'" << Log::Endl();
+		FatalError();
+		}
+	   
+}
+
+TUint32 ReadUnsignedNumber(std::string &aStr, size_t aSize)
+{
+	TUint32 val;
+	std::istringstream ss(aStr);
+
+	if((aStr.length() > 2) && 
+	   (aStr[0] == '0') && 
+	   ((aStr[1] == 'x') || (aStr[1] == 'X')))
+		{
+		// Hex number starting with 0x
+		char ch;
+		ss >> ch >> ch; // Discard the 0x
+		ss >> std::hex >> val;
+		}
+	else
+		{
+		// Decimal number
+		ss >> val;
+		}
+
+	// Now work out if we consumed the entire token without error
+	if(ss.fail())
+		{
+		// Number decode failed
+		dbg << Log::Indent() << "Failed to decode '" << aStr << "' as a number" << Log::Endl();
+		FatalError();
+		}
+
+	// Make sure we consumed all data
+	if(! ss.eof())
+		{
+		// Trailing chars on numeric token
+		FatalError();
+		}
+
+	if(aSize != 4)
+		{
+		// Check range
+		// nb the following check would fail if aSize==4 because x>>32 == x if sizeof(x)==4
+		if((val >> (8*aSize)) != 0)
+			{
+			// Higher order bits are set above the size of the variable we
+			// are returning into
+			FatalError();
+			}
+		}
+return val;
+}
+
+
+TUint32 RDecodeReadStream::ReadUnsignedNumber(size_t aSize)
+{
+BULLSEYE_OFF
+	if((TUint32(aSize)>4))
+		{
+		FatalError();
+		}
+BULLSEYE_RESTORE
+
+	ReadNextToken();
+
+	return ::ReadUnsignedNumber(iToken, aSize);
+}
+
+const std::string &RDecodeReadStream::Token() const
+{
+	return iToken;
+}
+
+void RDecodeReadStream::ReadNextToken()
+{
+	if(iPrefetchedTokenIsValid)
+		{
+		// Copy prefetched token to current token
+		iToken = iPrefetchedToken;
+		iPrefetchedToken.clear();
+		iPrefetchedTokenIsValid = false;
+		return;
+		}
+		
+	GetToken(iToken);
+}
+
+const std::string &RDecodeReadStream::PeakToken()
+{
+	if(!iPrefetchedTokenIsValid)
+		{
+		GetToken(iPrefetchedToken);
+		iPrefetchedTokenIsValid = true;
+		}
+
+	return iPrefetchedToken;
+}
+
+
+
+
+
+bool RDecodeReadStream::HumanReadable() const
+{
+	return iHumanReadable;
+}
+
+
+void RDecodeReadStream::Close()
+{
+	iReadStream.Close();
+}
+
+void RDecodeReadStream::GetToken(std::string &aToken)
+{
+	aToken.clear();
+
+	TUint8 ch;
+	do 
+		{
+		iReadStream >> ch;
+		if(ch == '#')
+			{
+			// Skip comment
+			++ch;
+			while((ch != '\r') && (ch != '\n'))
+				{
+				iReadStream >> ch;
+				} 
+
+			}
+		} while((ch == ' ') || (ch == '\t') || (ch == '\r') || (ch == '\n'));
+	
+	if(ch == '"')
+		{
+		// Read a string
+		iReadStream >> ch; // read first char
+		while(ch != '"')
+			{
+			if(ch=='\\')
+				{
+				// \X causes X to always be saved (even if X is ")
+				iReadStream >> ch;
+				}
+			
+			aToken.push_back(ch);
+			iReadStream >> ch;
+			}
+		// At this point ch contains WS so it can be discarded.
+		return;
+		}
+
+	// Read a non-string token
+	while((ch != ' ') && (ch != '\t') && (ch != '\r') && (ch != '\n'))
+		{
+		aToken.push_back(ch);
+		iReadStream >> ch;
+		}
+	// At this point ch contains WS so it can be discarded.
+	return;
+}
+
+
+
+
+
+REncodeWriteStream::REncodeWriteStream(CFileStore *aStore, RWriteStream &aWriteStream)
+	: iStore(aStore),
+	  iCertBaseName(), // not used for STORE based streams
+	  iWriteStream(&aWriteStream),
+	  iLogStream(0), 
+	  iHumanReadable(false), iPemOut(false), iVerbose(false), iIndentLevel(0)
+{
+}
+
+REncodeWriteStream::REncodeWriteStream(const std::string &aCertBaseName, RWriteStream &aWriteStream)
+	: iStore(0), iCertBaseName(aCertBaseName), iWriteStream(&aWriteStream),
+	  iLogStream(0), 
+	  iHumanReadable(true), iPemOut(false), iVerbose(false), iIndentLevel(0)
+{
+}
+
+REncodeWriteStream::REncodeWriteStream(Log &aLog)
+	: iStore(0), iWriteStream(0), iLogStream(&aLog.Stream()), 
+	  iHumanReadable(true), iPemOut(false), iVerbose(false), iIndentLevel(aLog.IndentLevel())
+{
+}
+
+void REncodeWriteStream::WriteBin(const void *aPtr, TUint32 aLength)
+{
+	if(iWriteStream)
+		{
+		iWriteStream->WriteL((TUint8 *)aPtr, aLength);
+		}
+	if(iLogStream)
+		{
+		iLogStream->write((const char *)aPtr, aLength);
+		iLogStream->flush();
+		}
+}
+
+void REncodeWriteStream::WriteQuotedUtf8(const void *aStr, TUint32 aLength)
+{
+	std::string tmp((const char *)aStr, aLength);
+	
+	// Insert a backslash before any backslash chars
+	size_t pos = 0;
+	while((pos = tmp.find('\\', pos)) != std::string::npos)
+		{
+		tmp.insert(pos, "\\", 1);
+		pos += 2;
+		}
+
+	// Insert a backslash before any double quote chars
+	pos = 0;
+	while((pos = tmp.find('"', pos)) != std::string::npos)
+		{
+		tmp.insert(pos, "\\", 1);
+		pos += 2;
+		}
+
+	if(iWriteStream)
+		{
+		iWriteStream->WriteL((TUint8 *)tmp.data(), tmp.size());
+		}
+	if(iLogStream)
+		{
+		iLogStream->write(tmp.data(), tmp.size());
+		iLogStream->flush();
+		}
+}
+
+void REncodeWriteStream::WriteByte(TUint8 aByte)
+{
+	WriteBin(&aByte, 1);
+}
+
+void REncodeWriteStream::WriteCStr(const void *aCstr)
+{
+	WriteBin(aCstr, strlen((const char *)aCstr));
+}
+
+void REncodeWriteStream::WriteHexNumber(TUint32 aNumber)
+{
+	char buf[20];
+	int len = sprintf(buf, "0x%x", aNumber);
+	WriteBin(buf, len);
+}
+
+
+void REncodeWriteStream::WriteSpace()
+{
+BULLSEYE_OFF
+	if(!iHumanReadable) return;
+BULLSEYE_RESTORE
+	WriteByte(' ');
+}
+
+void REncodeWriteStream::WriteLineEnd()
+{
+BULLSEYE_OFF
+	if(!iHumanReadable) return;
+BULLSEYE_RESTORE
+#ifdef linux
+	WriteByte('\n');
+#else
+	WriteBin("\r\n", 2);
+#endif
+}
+
+
+void REncodeWriteStream::WriteIndent()
+{
+BULLSEYE_OFF
+	if(!iHumanReadable) return;
+BULLSEYE_RESTORE
+	for(int i=0; i<iIndentLevel; ++i)
+		{
+		WriteByte('\t');
+		}
+}
+
+void REncodeWriteStream::IncIndent()
+{
+	prog.IncIndent();
+	++iIndentLevel;
+BULLSEYE_OFF
+	if(iIndentLevel < 0) FatalError();
+BULLSEYE_RESTORE
+}
+
+void REncodeWriteStream::DecIndent()
+{
+	prog.DecIndent();
+	--iIndentLevel;
+BULLSEYE_OFF
+	if(iIndentLevel < 0) FatalError();
+BULLSEYE_RESTORE
+}
+
+bool REncodeWriteStream::HumanReadable() const
+{
+	return iHumanReadable;
+}
+
+bool &REncodeWriteStream::PemOut()
+{
+	return iPemOut;
+}
+
+bool &REncodeWriteStream::Verbose()
+{
+	return iVerbose;
+}
+
+
+
+std::string REncodeWriteStream::CertFileName(TUint32 aFormat, TUint32 aCertNumber)
+{
+	std::stringstream ss;
+	ss << iCertBaseName;
+	ss << "cert";
+	ss << aCertNumber;
+	if(aFormat == EX509Certificate)
+		{
+		if(iPemOut)
+			{
+			ss << ".pem";
+			}
+		else
+			{
+			ss << ".der";
+			}
+		}
+	else
+		{
+		ss << ".dat";
+		}
+	
+	return ss.str();
+}
+
+
+
+void REncodeWriteStream::Close()
+{
+BULLSEYE_OFF
+	if(iWriteStream)
+BULLSEYE_RESTORE
+		{
+		iWriteStream->Close();
+		}
+}
+
+CFileStore *REncodeWriteStream::StoreObject()
+{
+BULLSEYE_OFF
+	if(iStore == 0) FatalError();
+BULLSEYE_RESTORE
+	return iStore;
+}
+
+RWriteStream &REncodeWriteStream::StoreWriteStream()
+{
+BULLSEYE_OFF
+	if(iWriteStream == 0) FatalError();
+BULLSEYE_RESTORE
+	return *iWriteStream;
+}
+
+bool REncodeWriteStream::Quiet() const
+{
+	return iLogStream != 0;
+}
+
+TUint8 fromHex(TUint8 ch)
+	/**
+	   Convert a single hex char from ascii
+	 */
+{
+	// convert to lowercase
+	if((ch >= 'A') && (ch <= 'F'))
+		{
+		ch -= ('A' -'a');
+		}
+	// Handle a-f
+	if((ch >= 'a') && (ch <= 'f'))
+		{
+		return ch - 'a' + 10;
+		}
+
+	// Handle 0-9
+	if((ch >= '0') && (ch <= '9'))
+		{
+		return ch - '0';
+		}
+
+	// Illegal
+	FatalError();
+	return 0xff;
+}
+
+EncDecContainerItem::~EncDecContainerItem()
+{
+}
+
+
+#ifdef _BullseyeCoverage
+#pragma BullseyeCoverage off
+#endif
+std::string EncDecContainerItem::ItemName() const
+{
+	// Should never be called
+	exit(-1);
+	std::string tmp;
+	return tmp;
+}
+
+void EncDecContainerItem::SetItemName(const std::string &)
+{
+	// Should never be called
+	exit(-1);
+}
+#ifdef _BullseyeCoverage
+#pragma BullseyeCoverage restore
+#endif
+
+
+
+EncDecContainer::EncDecContainer(const char *aContainerName, EncDecContainerItemFactoryFunc *aFactory)
+	: iName(aContainerName), iFactory(aFactory), iArray()
+{
+}
+
+EncDecContainer::~EncDecContainer()
+{
+	reset();
+}
+
+void EncDecContainer::push_back(EncDecContainerItem *aItem)
+{
+	iArray.push_back(aItem);
+}
+
+const EncDecContainerItem &EncDecContainer::operator[](TUint32 aIndex) const
+{
+	return *iArray[aIndex];
+}
+
+EncDecContainerItem &EncDecContainer::operator[](TUint32 aIndex)
+{
+	return *iArray[aIndex];
+}
+
+TUint32 EncDecContainer::size() const
+{
+	return iArray.size();
+}
+
+void EncDecContainer::reset()
+{
+	while(!iArray.empty())
+		{
+		EncDecContainerItem *p = iArray.back();
+		iArray.pop_back();
+		delete p;
+		}
+}
+
+
+
+void EncDecContainer::Encode(REncodeWriteStream &aWriteStream) const
+{
+	// Calculate Start/End container names
+	std::string startName("Start");
+	startName.append(iName);
+	std::string endName("End");
+	endName.append(iName);
+
+	prog << Log::Indent() << "Writing " << startName << Log::Endl();
+
+	TUint32 arrayLength = iArray.size();
+
+	if(aWriteStream.HumanReadable())
+		{
+		// Human
+		aWriteStream.WriteIndent();
+		aWriteStream.WriteBin(startName.data(), startName.size());
+		aWriteStream.WriteLineEnd();
+		}
+	else
+		{
+		// Binary
+		// Write length of array
+		aWriteStream.WriteBin(&arrayLength, sizeof(arrayLength));
+		}
+	
+	aWriteStream.IncIndent();
+
+	// Write array entries
+	for(TUint32 i=0; i < arrayLength; ++i)
+		{
+		std::ostringstream entryComment;
+		entryComment << "# Entry " << i+1;
+		prog << Log::Indent() << entryComment.str() << Log::Endl();
+		
+		bool bracketItem = (iArray[i]->ItemType() != 0);
+		if(aWriteStream.HumanReadable())
+			{
+			if(bracketItem)
+				{
+				std::ostringstream itemStart;
+				itemStart << "Start" << iArray[i]->ItemType() << " " << iArray[i]->ItemName();
+				prog << Log::Indent() << itemStart.str() << Log::Endl();
+				
+				aWriteStream.WriteIndent();
+				aWriteStream.WriteCStr("Start");
+				aWriteStream.WriteCStr(iArray[i]->ItemType());
+				aWriteStream.WriteCStr(" \"");
+				aWriteStream.WriteQuotedUtf8(iArray[i]->ItemName().data(), iArray[i]->ItemName().size());
+				aWriteStream.WriteCStr("\"");
+				aWriteStream.WriteLineEnd();
+				}
+			else
+				{
+				aWriteStream.WriteIndent();
+				aWriteStream.WriteBin(entryComment.str().data(), entryComment.str().size());
+				aWriteStream.WriteLineEnd();
+				}
+			}
+		aWriteStream.IncIndent();
+		iArray[i]->Encode(aWriteStream);
+		aWriteStream.DecIndent();
+		if(bracketItem && aWriteStream.HumanReadable())
+			{
+			std::ostringstream tmp;
+			tmp << "End" << iArray[i]->ItemType();
+
+			prog << Log::Indent() << tmp.str() << Log::Endl();
+
+			aWriteStream.WriteIndent();
+			aWriteStream.WriteBin(tmp.str().data(), tmp.str().size());
+			aWriteStream.WriteLineEnd();
+			}
+		}
+
+	aWriteStream.DecIndent();
+
+	if(aWriteStream.HumanReadable())
+		{
+		// Human
+		aWriteStream.WriteIndent();
+		aWriteStream.WriteCStr("End");
+		aWriteStream.WriteBin(iName.data(), iName.size());
+		aWriteStream.WriteLineEnd();
+		}
+
+	prog << Log::Indent() << endName << Log::Endl();
+
+	return;
+}
+
+void EncDecContainer::Decode(RDecodeReadStream &aReadStream)
+{
+	// Calculate Start/End container names
+	std::string startName("Start");
+	startName.append(iName);
+	std::string endName("End");
+	endName.append(iName);
+
+	prog << Log::Indent() << "Reading " << startName << Log::Endl();
+
+	if(aReadStream.HumanReadable())
+		{
+		// Human
+
+		// Check/consume container StartX
+		aReadStream.CheckName(startName);
+
+		prog.IncIndent();
+		
+		// Create items until we find the container EndX
+		TUint32 entryNum=1;
+		while(aReadStream.PeakToken() != endName)
+			{
+			// Progress message
+			prog << Log::Indent() << "# Entry " << std::dec << entryNum++ << std::hex << Log::Endl();
+
+			EncDecContainerItem *item = iFactory();
+			bool bracketedItem = (item->ItemType() != 0);
+			std::string itemName;
+			if(bracketedItem && aReadStream.HumanReadable())
+				{
+				// Bracketed item, so read ItemType tag and ItemName
+				std::string itemStart("Start");
+				itemStart.append(item->ItemType());
+				aReadStream.CheckName(itemStart);
+				prog << Log::Indent() << itemStart;
+
+				// Read item name
+				aReadStream.ReadNextToken();
+				itemName = aReadStream.Token();
+				}
+		
+			prog.IncIndent();
+			item->Decode(aReadStream);
+			iArray.push_back(item);
+			prog.DecIndent();
+			if(bracketedItem && aReadStream.HumanReadable())
+				{
+				// Bracketed item, so read End ItemType tag
+				std::string itemEnd("End");
+				itemEnd.append(item->ItemType());
+				aReadStream.CheckName(itemEnd);
+				// and set item name
+				item->SetItemName(itemName);
+				}
+			}
+
+		// Check/consume container EndX
+		aReadStream.CheckName(endName);
+
+		prog.DecIndent();
+		}
+	else
+		{
+		// Binary
+		// Read number of entries
+		TUint32 arrayLength;
+		aReadStream.RawRead(&arrayLength, sizeof(arrayLength));
+
+		prog.IncIndent();
+		for(TUint32 i=0; i < arrayLength; ++i)
+			{
+			EncDecContainerItem *item = iFactory();
+			prog << Log::Indent() << "# Entry " << std::dec << i+1 << std::hex << Log::Endl();
+			prog.IncIndent();
+			item->Decode(aReadStream);
+			prog.DecIndent();
+			iArray.push_back(item);
+			}
+		prog.DecIndent();
+		}
+
+	prog << Log::Indent() << endName << Log::Endl();
+
+}
+
+
+
+
+REncodeWriteStream& operator<<(REncodeWriteStream& aStream, const EncDecContainer &aContainer)
+{
+	aContainer.Encode(aStream);
+	return aStream;
+}
+
+RDecodeReadStream& operator>>(RDecodeReadStream& aStream,EncDecContainer &aContainer)
+{
+	aContainer.Decode(aStream);
+	return aStream;
+}
+
+//
+// TUid
+//
+void EncodeHuman(REncodeWriteStream& aStream,const TUid &aUid)
+{
+	aStream.WriteHexNumber(aUid.iUid);
+}
+void DecodeHuman(RDecodeReadStream& aStream,TUid &aUid)
+{
+	aUid.iUid = aStream.ReadUnsignedNumber(sizeof(aUid.iUid));
+}
+
+//
+// TName
+//
+void EncodeHuman(REncodeWriteStream& aStream,const TName &aName)
+{
+	// Compress the internal UTF-16 to human readable UTF-8
+	;
+	TInt outputBytes = 0;
+	TUint8 *outBuf = cstrFromUtf16(aName.Ptr(), aName.Length(), outputBytes);
+	
+	aStream.WriteByte('"');
+	aStream.WriteQuotedUtf8(outBuf, outputBytes);
+	aStream.WriteByte('"');
+
+	delete [] outBuf;
+}
+void DecodeHuman(RDecodeReadStream& aStream,TName &aName)
+{
+	aStream.ReadNextToken();
+
+	// Expand UTF-8 into internal UTF-16LE representation
+	TInt outputWords = 0;
+	TText *outputBuf = utf16FromUtf8((const TUint8 *)aStream.Token().data(), aStream.Token().size(), outputWords);
+	memcpy((void *)aName.Ptr(), outputBuf, outputWords*2);
+	aName.SetLength(outputWords);
+	delete [] outputBuf;
+}
+
+void readContainer(const std::string &aFileName, bool aHuman, EncDecContainer &container)
+{
+	if(aHuman)
+		{
+		FileReadStream fileReadStream(aFileName, true);
+		std::string certBaseName(aFileName);
+		size_t dotLoc = certBaseName.rfind('.');
+		if(dotLoc != std::string::npos)
+			{
+			certBaseName.erase(dotLoc);
+			}
+		certBaseName += '_';
+		RDecodeReadStream inStream(certBaseName, fileReadStream); // human readable
+		inStream >> container;
+		inStream.Close();
+		}
+	else
+		{
+		RFs fs;
+		User::LeaveIfError(fs.Connect());
+		CleanupClosePushL(fs);
+		
+		CPermanentFileStore* store = CPermanentFileStore::OpenLC(fs, _L16(aFileName.c_str()),EFileShareAny | EFileRead);
+		store->SetTypeL(KPermanentFileStoreLayoutUid);
+
+		RStoreReadStream rootStream;
+		rootStream.OpenLC(*store, store->Root());
+		TUint32 dataStreamId;
+		rootStream >> dataStreamId;
+		CleanupStack::PopAndDestroy(&rootStream); 
+
+		RStoreReadStream dataStream;
+		dataStream.OpenLC(*store, dataStreamId);
+
+		RDecodeReadStream readStream(store, dataStream); // binary store
+
+		readStream >> container;
+
+		CleanupStack::PopAndDestroy(&dataStream); 
+		CleanupStack::PopAndDestroy(store); 
+		CleanupStack::PopAndDestroy(&fs); 
+		}
+}
+
+
+void writeContainer(const char *aFileName, bool aHuman, bool aPemOut, bool aVerbose, const EncDecContainer &container)
+{
+	prog << Log::Indent() << "Writing output file '" << aFileName << "'" << Log::Endl();
+	prog.IncIndent();
+	if(aHuman)
+		{
+		FileWriteStream fileWriteStream(aFileName);
+		// Calculate based name by striping last .xx extension and appending an underscore.
+		std::string certBaseName(aFileName);
+		size_t dotLoc = certBaseName.rfind('.');
+		if(dotLoc != std::string::npos)
+			{
+			certBaseName.erase(dotLoc);
+			}
+		certBaseName += '_';
+		
+		REncodeWriteStream outStream(certBaseName, fileWriteStream); // human readable
+		outStream.PemOut() = aPemOut;
+		outStream.Verbose() = aVerbose;
+		outStream << container;
+		outStream.Close();
+		}
+	else
+		{
+		RFs fs;
+		User::LeaveIfError(fs.Connect());
+		CleanupClosePushL(fs);
+
+		CFileStore* store = CPermanentFileStore::ReplaceLC(fs,_L16(aFileName),EFileShareAny | EFileRead | EFileWrite);
+		store->SetTypeL(KPermanentFileStoreLayoutUid);
+		
+		RStoreWriteStream dataStream;
+		TStreamId dataStreamId = dataStream.CreateLC(*store);
+
+		REncodeWriteStream outStream(store, dataStream); // binary
+		outStream << container;
+		outStream.Close();
+
+		dataStream.CommitL();
+		dataStream.Close();
+
+		RStoreWriteStream rootStream;
+		TStreamId rootId = rootStream.CreateLC(*store);
+		rootStream << dataStreamId;
+		rootStream.CommitL();
+		rootStream.Close();
+		
+
+		store->SetRootL(rootId);
+		store->CommitL();
+		CleanupStack::PopAndDestroy(&dataStream); 
+		CleanupStack::PopAndDestroy(store); 
+		CleanupStack::PopAndDestroy(&fs); 
+		}
+	prog.DecIndent();
+}
+
+
+// End of file