// fzipup.cpp
// 
// Copyright (c) 2008 - 2010 Accenture. All rights reserved.
// This component and the accompanying materials are made available
// under the terms of the "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:
// Accenture - Initial contribution
//

#include <ezstream.h>
#include <ezcompressor.h>
#include <ezdecompressor.h>
#include <ezfilebuffer.h>
#include <f32file.h>
#include "fzipup.h"

const TInt KDefaultDiskNumber = 0;
const TInt CZipEntry::iOffset = _FOFF(CZipEntry, iLink);
const TUint16 KFZipVersion = 0x0014;
TInt KWindowBits = -CEZCompressor::EMaxWBits; // negative value is an undocumented feature of our zlib implementation, ensuring we do not include the zlib header in the compressed data (which we must do for zip-file format support)
const TInt KCompressedSizeRelativePosition = 18; // iCompressedSize is 18 bytes in from the current entry's LFH

//
// CBufferManager
//
CBufferManager *CBufferManager::NewLC(RFileReadStream& aInput, RFileWriteStream& aOutput, TInt aInputStreamBytes, TInt aBufferSize)
	{
	CBufferManager *bm = new (ELeave) CBufferManager(aInput, aOutput, aInputStreamBytes, aBufferSize);
	CleanupStack::PushL(bm);
	bm->ConstructL();
	return bm;
	}

CBufferManager::CBufferManager(RFileReadStream& aInput, RFileWriteStream& aOutput, TInt aInputStreamBytes, TInt aBufferSize)
:iInput(aInput), iOutput(aOutput), iInputStreamBytes(aInputStreamBytes), iBufferSize(aBufferSize), iInputDescriptor(NULL,0), iOutputDescriptor(NULL,0)
	{
	}

CBufferManager::~CBufferManager()
	{
	delete[] iInputBuffer;
	delete[] iOutputBuffer;
	}

void CBufferManager::ConstructL()
	{
	if (iInputStreamBytes < iBufferSize)
		iBufferSize = iInputStreamBytes;
	if (iBufferSize <=0)
		{
		User::Leave(KErrUnderflow);
		}
	
	iInputBuffer = new (ELeave) TUint8[iBufferSize];
	iOutputBuffer = new (ELeave) TUint8[iBufferSize];
	
	iInputDescriptor.Set(iInputBuffer,0,iBufferSize);
	iOutputDescriptor.Set(iOutputBuffer,0,iBufferSize);
	
	iReadLength = iBufferSize;
	}
	
void CBufferManager::InitializeL(CEZZStream &aZStream)
	{
	ReadInputL();
	aZStream.SetInput(iInputDescriptor);
	aZStream.SetOutput(iOutputDescriptor);
	}

void CBufferManager::NeedInputL(CEZZStream &aZStream)
	{
	ReadInputL();
	aZStream.SetInput(iInputDescriptor);
	}

void CBufferManager::NeedOutputL(CEZZStream &aZStream)
	{
	WriteOutputL(aZStream);
	aZStream.SetOutput(iOutputDescriptor);
	}

void CBufferManager::FinalizeL(CEZZStream &aZStream)
	{
	WriteOutputL(aZStream);
	}

void CBufferManager::ReadInputL()
	{
	iInput.ReadL(iInputDescriptor, iReadLength);
	iInputStreamBytes -= iReadLength;
	if (iInputStreamBytes < iBufferSize)
		{
		iReadLength = iInputStreamBytes;
		}
	if (iReadLength < 0)
		{
		User::Leave(KErrOverflow);
		}
	}

void CBufferManager::WriteOutputL(CEZZStream& aZStream)
	{
	TPtrC8 od = aZStream.OutputDescriptor();
	iOutput.WriteL(od);
	iBytesWritten += od.Size();
	}

//
// CZipEntry
// 
CZipEntry* CZipEntry::NewLC(RFs& aFs, const TDesC& aFilename, const TInt aUid)
	{
	CZipEntry* self = new (ELeave) CZipEntry(aFs, aUid);
	CleanupStack::PushL(self);
	self->ConstructL(aFilename);
	return self;
	}

CZipEntry::CZipEntry(RFs& aFs, const TInt aUid):
iFs(aFs), iUid(aUid)
	{
	iLFH.iCRC32 = crc32(iLFH.iCRC32, NULL, 0);
	}

CZipEntry::~CZipEntry()
	{
	if (iFileData)
		delete iFileData;
	if (iInput.SubSessionHandle() > 0)
		iInput.Close();
	}
	
void CZipEntry::ConstructL(const TDesC& aFilename)
	{
	User::LeaveIfError(iInput.Open(iFs, aFilename, EFileRead | EFileShareReadersOnly));

	// ensure aFileName is stored in accordance with zip file-format specification
	TFileName2 file(aFilename);
	if (file.HasDriveLetter())
		{
		file.Delete(0, 3); // remove '<drive>:\'
		}
	if (file.HasLeadingSlash())
		{
		file.Delete(0, 1); // remove '\'
		}
	iAsciiName.Copy(file);
	TUint8* ptr = const_cast<TUint8*> (iAsciiName.Ptr());
	TInt count = 0;
	do
		{
		if (*ptr == '\\')
			{
			*ptr = '/';
			}
		ptr++;
		} while (count++ < iAsciiName.Length());
	
	// config. local file header
	iLFH.iSignature = KLocalHeaderSignature;
	iLFH.iVersionNeeded = KFZipVersion;
	iLFH.iFlags = 0x0002; // best (-exx/-ex) compression was used
	iLFH.iCompressionMethod = EDeflated;
	TTime time;
	User::LeaveIfError(iInput.Modified(time));
	TDateTime td = time.DateTime();
	TUint16 param1 = td.Year() - 1980;
	TUint16 param2 = td.Month() + 1;
	TUint16 param3 = td.Day() + 1;
	iLFH.iLastModifiedFileDate = (param1 << 9) | (param2 << 5) | param3;
	param1 = td.Hour() + 1;
	param2 = td.Minute() + 1;
	param3 = td.Second() + 1;
	iLFH.iLastModifiedFileTime = (param1 << 11) | (param2 << 5) | (param3 >> 1);
	TInt uSize = 0;
	User::LeaveIfError(iInput.Size(uSize));
	if (uSize <= 0)
		{
		User::Leave(KErrAbort);
		}
	iLFH.iUncompressedSize = uSize;
	iLFH.iCompressedSize = uSize; // note: to be adjusted later once the compression is performed
	CalcCRCL();
	iLFH.iFileNameLength = iAsciiName.Size();
	iLFH.iExtraFieldLength = 0;
	
	// config. file header
	iFH.iSignature = KCentralDirectoryHeaderSignature;
	iFH.iMadeBy = KFZipVersion;
	iFH.iRequired = KFZipVersion;
	iFH.iFlags = iLFH.iFlags;
	iFH.iCompressionMethod = iLFH.iCompressionMethod;
	iFH.iLastModifiedFileTime = iLFH.iLastModifiedFileTime;
	iFH.iLastModifiedFileDate = iLFH.iLastModifiedFileDate;
	iFH.iCRC32 = iLFH.iCRC32;
	iFH.iCompressedSize = iLFH.iCompressedSize;	// note: to be adjusted later once the compression is performed
	iFH.iUncompressedSize = iLFH.iUncompressedSize;
	iFH.iFileNameLength = iLFH.iFileNameLength;
	iFH.iExtraFieldLength = iLFH.iExtraFieldLength;
	iFH.iFileCommentLength = 0x0000;
	iFH.iDiskNumberStart = KDefaultDiskNumber;
	iFH.iInternalFileAttributes = 0x0001;
	iFH.iExternalFileAttributes = 0x00000020;	// hardcoded to 'Archive'
	iFH.iLocalHeaderOffset = 0x00000000; // to be adjusted later (if necessary)
	}

void CZipEntry::SetOffset(TUint32 aOffset)
	{
	iFH.iLocalHeaderOffset = aOffset;
	}

TUint32 CZipEntry::ReturnOffset()
	{
	TUint32 result = iFH.iLocalHeaderOffset; // start with the offset of the LFH
	result += KLocalHeaderFixedLength; // add the size of the LFH itself
	result += iAsciiName.Size(); // add the size of the filename
	result += iLFH.iCompressedSize; // add any compressed data
	return result;
	}

TUint32 CZipEntry::FileHeaderSize()
	{
	TUint32 result = KCentralDirectoryHeaderFixedLength;
	result += iAsciiName.Size();
	return result;
	}

//
// CZipEntry::CalcCRCL
// mem. efficient means of calculating the crc on uncompressed data
// we require a maximum of KDefaultZipBufferLength heap space regardless size of data
//
void CZipEntry::CalcCRCL()
	{
	ASSERT(iInput.SubSessionHandle() > 0);
	TInt bytesRead = 0;
	TInt length = KDefaultZipBufferLength; // 32Kb
	if (iLFH.iUncompressedSize < KDefaultZipBufferLength)
		{
		length = iLFH.iUncompressedSize;
		}
	
	iLFH.iCRC32 = crc32(0, NULL, 0);
	HBufC8* crc = HBufC8::NewLC(length);
	TPtr8 crcPtr = crc->Des();
	User::LeaveIfError(iInput.Seek(ESeekStart, bytesRead));
	do
		{
		User::LeaveIfError(iInput.Read(crcPtr, length));
		iLFH.iCRC32 = crc32(iLFH.iCRC32, crcPtr.Ptr(), length);
		bytesRead += length;
		if ((iLFH.iUncompressedSize - bytesRead) < KDefaultZipBufferLength)
			{
			length = iLFH.iUncompressedSize - bytesRead;
			}
		} while (length > 0);
	CleanupStack::PopAndDestroy(1); // crc	
	}

//
// CZipItUp
// 
CZipItUp* CZipItUp::NewLC(RFs& aFs, TDesC& aArchive)
	{
	CZipItUp* self = new (ELeave) CZipItUp(aFs, aArchive);
	CleanupStack::PushL(self);
	self->ConstructL();
	return self;
	}		

CZipItUp::CZipItUp(RFs& aFs, TDesC& aArchive):
iFs(aFs), iFileName(aArchive), iZipMemberLinkedList(CZipEntry::iOffset), iZipMemberLinkedListIter(iZipMemberLinkedList)
	{
	}

CZipItUp::~CZipItUp()
	{
	if (iFile.SubSessionHandle() > 0)
		{
		iFile.Close();
		}
	while (!iZipMemberLinkedList.IsEmpty())
		{
		CZipEntry* entry = iZipMemberLinkedList.First();
		iZipMemberLinkedList.Remove(*entry);
		delete entry;	
		}
	iZipMemberLinkedList.Reset();
	}
	
void CZipItUp::ConstructL()
	{
	User::LeaveIfError(iFile.Create(iFs, iFileName, EFileStream | EFileShareExclusive));
	}
	
//
// CZipItUp::AddFileL
//
void CZipItUp::AddFileL(const TDesC& aFile)
	{
	if (!DuplicateEntryL(aFile))
		{
		// spawn a new zip entry, compress the file, add it to the list of zip file contained in the archive
		CZipEntry* zipEntry = CZipEntry::NewLC(iFs, aFile, ++iEntryUid);
		AddEntryL(*zipEntry);
		CleanupStack::Pop(zipEntry);
		}
	}

//
// CZipItUp::DuplicateEntryL
// run a check against the zip archive's current entries (if any!) for duplicate files
// return ETrue if a duplicate is found
//
TBool CZipItUp::DuplicateEntryL(const TDesC& aFile)
	{
	if (iZipMemberLinkedList.IsEmpty())
		{
		return EFalse;
		}
	
	// turn aFile into a zip-file format filename
	TFileName2 file(aFile);
	if (file.HasDriveLetter())
		{
		file.Delete(0, 3); // remove '<drive>:\'
		}
	if (file.HasLeadingSlash())
		{
		file.Delete(0, 1); // remove '\'
		}
	TBuf8<KMaxFileName> ascii;
	ascii.Copy(file);
	TUint8* ptr = const_cast<TUint8*> (ascii.Ptr());
	TInt count = 0;
	do
		{
		if (*ptr == '\\')
			{
			*ptr = '/';
			}
		ptr++;
		} while (count++ < ascii.Length());
	
	// iterate through zip archive entries looking for a duplicate 
	iZipMemberLinkedListIter.SetToFirst();
	CZipEntry* entry = iZipMemberLinkedListIter++;
	while (entry != NULL)
		{
		if (entry->iAsciiName == ascii)
			{
			return ETrue;
			}
		entry = iZipMemberLinkedListIter++;
		}
	return EFalse;
	}

//
// CZipItUp::AddEntryL
// Add an entry to the zip archive.
//
void CZipItUp::AddEntryL(CZipEntry& aEntry)
	{
	// append the new entry to the queue
	if (!iZipMemberLinkedList.IsEmpty())
		{
		// prior entries in the list therefore need to adjust this entry's local file header offset
		CZipEntry* lastEntry = iZipMemberLinkedList.Last();
		User::LeaveIfNull(lastEntry);
		}
	iZipMemberLinkedList.AddLast(aEntry);
	}

//
// CZipItUp::CreateZipL
// Creates the actual file, including all the previously compressed zip files in the archive
//
void CZipItUp::CreateZipL()
	{
	ASSERT(iFile.SubSessionHandle() > 0);
	if (iZipMemberLinkedList.IsEmpty())
		{
		User::Leave(KErrGeneral);
		}
	RFileWriteStream stream(iFile);
	stream.PushL();
	
	// initialise central directory header
	iCDT.iSignature = CZipArchive::KCentralDirectorySignature;
	iCDT.iDiskNumber = KDefaultDiskNumber;
	iCDT.iStartDiskNumber = KDefaultDiskNumber;
	iCDT.iLocalEntryCount = 0;
	iCDT.iTotalEntryCount = 0;
	iCDT.iSize = 0;
	iCDT.iCommentLength = 0;
	
	// iterate through each zip entry, appending local file header (LFH), file data (FD) to file
	iZipMemberLinkedListIter.SetToFirst();
	CZipEntry* entry = iZipMemberLinkedListIter++;
	CZipEntry* prior = NULL;
	User::LeaveIfNull(entry);
	do
		{
		if (prior)
			{
			// adjust file header local offset if necessary
			entry->SetOffset(prior->ReturnOffset());
			}

		// insert entry's Local File Header
		AppendLocalFileHeaderL(stream, *entry);
		
		// insert entry's File Data
		AppendCompressedDataL(stream, *entry);
		
		// adjust cdt parameters
		iCDT.iLocalEntryCount++;
		iCDT.iTotalEntryCount++;
		iCDT.iSize += entry->FileHeaderSize();
		
		// move onto the next entry
		prior = entry;
		entry = iZipMemberLinkedListIter++;
		} while (entry != NULL);
		
	// adjust cdt offset parameter
	iCDT.iOffset = iZipMemberLinkedList.Last()->ReturnOffset();
	
	// iterate through each zip entry, appending the central directory File Headers to file (these must go after LFH, FD entries)
	iZipMemberLinkedListIter.SetToFirst();
	entry = iZipMemberLinkedListIter++;
	
	do
		{
		// central directory File Header
		AppendCentralDirectoryFileHeaderL(stream, *entry);
		
		// move onto the next entry
		entry = iZipMemberLinkedListIter++;
		} while (entry != NULL);
	
	// append the central directory trailer
	AppendCentralDirectoryTrailerL(stream);	
	
	// commit the transaction
	stream.CommitL();
	stream.Pop();
	stream.Close();
	
	// go back through & adjust compressed size values for each zip entry post-mortem
	ASSERT(iFile.SubSessionHandle() == 0);
	User::LeaveIfError(iFile.Open(iFs, iFileName, EFileWrite | EFileShareExclusive));
	TInt pos = 0;
	const TInt remainder = 8; // CZipArchive::KLocalHeaderFixedLength - KCompressedSizeRelativePosition
	User::LeaveIfError(iFile.Seek(ESeekStart, pos));
	iZipMemberLinkedListIter.SetToFirst();
	entry = iZipMemberLinkedListIter++;
	do
		{
		pos += KCompressedSizeRelativePosition; // step to iCompressedSize location in the current entry's LFH
		TUint32 cs = entry->iLFH.iCompressedSize;
		TUint32 KMyByte = 0x000000FF;
		TInt bytecount = 0;
		TBuf8<8> compSize;
		do
			{
			TUint32 csChar((cs & KMyByte));
			compSize.Append(csChar);
			cs >>= 8;
			} while (++bytecount < 4); // zip-file format allows us 4 bytes only for the compressed size value
		User::LeaveIfError(iFile.Write(pos, compSize));
		pos += bytecount; // the 4 bytes we just wrote
		
		// step over remaing bytes to the beginning of the next entry's LFH
		pos += remainder; 
		pos += entry->iAsciiName.Size();
		pos += entry->iLFH.iCompressedSize;
		
		// we're now positioned at the next entry's LFH
		entry = iZipMemberLinkedListIter++;
		} while (entry != NULL);
	}

void CZipItUp::AppendLocalFileHeaderL(RFileWriteStream& aStream, CZipEntry& aZipEntry)
	{
	aStream.WriteUint32L(aZipEntry.iLFH.iSignature);
	aStream.WriteUint16L(aZipEntry.iLFH.iVersionNeeded);
	aStream.WriteUint16L(aZipEntry.iLFH.iFlags);
	aStream.WriteUint16L(aZipEntry.iLFH.iCompressionMethod);
	aStream.WriteUint16L(aZipEntry.iLFH.iLastModifiedFileTime);
	aStream.WriteUint16L(aZipEntry.iLFH.iLastModifiedFileDate);
	aStream.WriteUint32L(aZipEntry.iLFH.iCRC32);
	aStream.WriteUint32L(aZipEntry.iLFH.iCompressedSize);
	aStream.WriteUint32L(aZipEntry.iLFH.iUncompressedSize);
	aStream.WriteUint16L(aZipEntry.iLFH.iFileNameLength);
	aStream.WriteUint16L(aZipEntry.iLFH.iExtraFieldLength);
	aStream.WriteL(aZipEntry.iAsciiName);
	}

void CZipItUp::AppendCompressedDataL(RFileWriteStream& aStream, CZipEntry& aZipEntry)
	{
	// compress data & stream it to the zip archive (aStream)
	RFileReadStream inStream;
	inStream.Attach(aZipEntry.iInput); // note takes ownership of iInput subsession handle
	CBufferManager* fb = CBufferManager::NewLC(inStream, aStream, aZipEntry.iLFH.iUncompressedSize, KDefaultZipBufferLength);
	CEZCompressor* compressor = CEZCompressor::NewLC(*fb, CEZCompressor::EBestCompression, KWindowBits, CEZCompressor::EDefMemLevel, CEZCompressor::EDefaultStrategy);
	while (compressor->DeflateL()){/* do nothing */}
	
	// update the entry's LocalFileHeader and FileHeader
	aZipEntry.iLFH.iCompressedSize = fb->TotalBytesOut();
	aZipEntry.iFH.iCompressedSize = aZipEntry.iLFH.iCompressedSize;
	
	// cleanup
	CleanupStack::PopAndDestroy(2, fb); // compressor, fb
	inStream.Close();
	}

void CZipItUp::AppendCentralDirectoryFileHeaderL(RFileWriteStream& aStream, CZipEntry& aEntry)
	{
	aStream.WriteUint32L(aEntry.iFH.iSignature);
	aStream.WriteUint16L(aEntry.iFH.iMadeBy);
	aStream.WriteUint16L(aEntry.iFH.iRequired);
	aStream.WriteUint16L(aEntry.iFH.iFlags);
	aStream.WriteUint16L(aEntry.iFH.iCompressionMethod);
	aStream.WriteUint16L(aEntry.iFH.iLastModifiedFileTime);
	aStream.WriteUint16L(aEntry.iFH.iLastModifiedFileDate);
	aStream.WriteUint32L(aEntry.iFH.iCRC32);
	aStream.WriteUint32L(aEntry.iFH.iCompressedSize);
	aStream.WriteUint32L(aEntry.iFH.iUncompressedSize);
	aStream.WriteUint16L(aEntry.iFH.iFileNameLength);
	aStream.WriteUint16L(aEntry.iFH.iExtraFieldLength);
	aStream.WriteUint16L(aEntry.iFH.iFileCommentLength);
	aStream.WriteUint16L(aEntry.iFH.iDiskNumberStart);
	aStream.WriteUint16L(aEntry.iFH.iInternalFileAttributes);
	aStream.WriteUint32L(aEntry.iFH.iExternalFileAttributes);
	aStream.WriteUint32L(aEntry.iFH.iLocalHeaderOffset);
	aStream.WriteL(aEntry.iAsciiName);
	}

void CZipItUp::AppendCentralDirectoryTrailerL(RFileWriteStream& aStream)
	{
	aStream.WriteUint32L(iCDT.iSignature);
	aStream.WriteUint16L(iCDT.iDiskNumber);
	aStream.WriteUint16L(iCDT.iStartDiskNumber);
	aStream.WriteUint16L(iCDT.iLocalEntryCount);
	aStream.WriteUint16L(iCDT.iTotalEntryCount);
	aStream.WriteUint32L(iCDT.iSize);
	aStream.WriteUint32L(iCDT.iOffset);
	aStream.WriteUint16L(iCDT.iCommentLength);	
	}
