<?php
/**
 * Akeeba Engine
 * The modular PHP5 site backup engine
 * @copyright Copyright (c)2009-2014 Nicholas K. Dionysopoulos
 * @license   GNU GPL version 3 or, at your option, any later version
 * @package   akeebaengine
 *
 */

// Protection against direct access
defined('AKEEBAENGINE') or die();

class AEArchiverZip extends AEAbstractArchiver
{

	/** @var string Beginning of central directory record. */
	private $_ctrlDirHeader = "\x50\x4b\x01\x02";

	/** @var string End of central directory record. */
	private $_ctrlDirEnd = "\x50\x4b\x05\x06";

	/** @var string Beginning of file contents. */
	private $_fileHeader = "\x50\x4b\x03\x04";

	/** @var string The name of the temporary file holding the ZIP's Central Directory */
	private $_ctrlDirFileName;

	/** @var string The name of the file holding the ZIP's data, which becomes the final archive */
	private $_dataFileName;

	/** @var integer The total number of files and directories stored in the ZIP archive */
	private $_totalFileEntries;

	/** @var integer The total size of data in the archive. Note: On 32-bit versions of PHP, this will overflow for archives over 2Gb! */
	private $_totalDataSize = 0;

	/** @var integer The chunk size for CRC32 calculations */
	private $AkeebaPackerZIP_CHUNK_SIZE;

	/** @var bool Should I use Split ZIP? */
	private $_useSplitZIP = false;

	/** @var int Maximum fragment size, in bytes */
	private $_fragmentSize = 0;

	/** @var int Current fragment number */
	private $_currentFragment = 1;

	/** @var int Total number of fragments */
	private $_totalFragments = 1;

	/** @var string Archive full path without extension */
	private $_dataFileNameBase = '';

	/** @var bool Should I store symlinks as such (no dereferencing?) */
	private $_symlink_store_target = false;

	/**
	 * Extend the bootstrap code to add some define's used by the ZIP format engine
	 * @see backend/akeeba/abstract/AEAbstractArchiver#__bootstrap_code()
	 */
	protected function __bootstrap_code()
	{
		if (!defined('_AKEEBA_COMPRESSION_THRESHOLD'))
		{
			$config = AEFactory::getConfiguration();
			define("_AKEEBA_COMPRESSION_THRESHOLD", $config->get('engine.archiver.common.big_file_threshold')); // Don't compress files over this size
			define("_AKEEBA_DIRECTORY_READ_CHUNK", $config->get('engine.archiver.zip.cd_glue_chunk_size')); // How much data to read at once when finalizing ZIP archives
		}
		parent::__bootstrap_code();
	}

	/**
	 * Class constructor - initializes internal operating parameters
	 */
	public function __construct()
	{
		AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "AEArchiverZip :: New instance");

		// Get chunk override
		$registry = AEFactory::getConfiguration();
		if ($registry->get('engine.archiver.common.chunk_size', 0) > 0)
		{
			$this->AkeebaPackerZIP_CHUNK_SIZE = AKEEBA_CHUNK;
		}
		else
		{
			// Try to use as much memory as it's possible for CRC32 calculation
			$memLimit = ini_get("memory_limit");

			if (strstr($memLimit, 'M'))
			{
				$memLimit = (int)$memLimit * 1048576;
			}
			elseif (strstr($memLimit, 'K'))
			{
				$memLimit = (int)$memLimit * 1024;
			}
			elseif (strstr($memLimit, 'G'))
			{
				$memLimit = (int)$memLimit * 1073741824;
			}
			else
			{
				$memLimit = (int)$memLimit;
			}

			if (is_numeric($memLimit) && ($memLimit < 0))
			{
				$memLimit = "";
			} // 1.2a3 -- Rare case with memory_limit < 0, e.g. -1Mb!
			if (($memLimit == ""))
			{
				// No memory limit, use 2Mb chunks (fairly large, right?)
				$this->AkeebaPackerZIP_CHUNK_SIZE = 2097152;
			}
			elseif (function_exists("memory_get_usage"))
			{
				// PHP can report memory usage, see if there's enough available memory; Joomla! alone eats about 5-6Mb! This code is called on files <= 1Mb
				$memLimit = $this->_return_bytes($memLimit);
				$availableRAM = $memLimit - memory_get_usage();

				if ($availableRAM <= 0)
				{
					// Some PHP implemenations also return the size of the httpd footprint!
					if (($memLimit - 6291456) > 0)
					{
						$this->AkeebaPackerZIP_CHUNK_SIZE = $memLimit - 6291456;
					}
					else
					{
						$this->AkeebaPackerZIP_CHUNK_SIZE = 2097152;
					}
				}
				else
				{
					$this->AkeebaPackerZIP_CHUNK_SIZE = $availableRAM * 0.5;
				}
			}
			else
			{
				// PHP can't report memory usage, use a conservative 512Kb
				$this->AkeebaPackerZIP_CHUNK_SIZE = 524288;
			}
		}

		// NEW 2.3: Should we enable Split ZIP feature?
		$fragmentsize = $registry->get('engine.archiver.common.part_size', 0);
		if ($fragmentsize >= 65536)
		{
			// If the fragment size is AT LEAST 64Kb, enable Split ZIP
			$this->_useSplitZIP = true;
			$this->_fragmentSize = $fragmentsize;
			// Indicate that we have at least 1 part
			$statistics = AEFactory::getStatistics();
			$statistics->updateMultipart(1);
		}

		// NEW 2.3: Should I use Symlink Target Storage?
		$dereferencesymlinks = $registry->get('engine.archiver.common.dereference_symlinks', true);
		if (!$dereferencesymlinks)
		{
			// We are told not to dereference symlinks. Are we on Windows?
			if (function_exists('php_uname'))
			{
				$isWindows = stristr(php_uname(), 'windows');
			}
			else
			{
				$isWindows = (DIRECTORY_SEPARATOR == '\\');
			}
			// If we are not on Windows, enable symlink target storage
			$this->_symlink_store_target = !$isWindows;
		}


		AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Chunk size for CRC is now " . $this->AkeebaPackerZIP_CHUNK_SIZE . " bytes");

		parent::__construct();
	}

	/**
	 * Initialises the archiver class, creating the archive from an existent
	 * installer's JPA archive.
	 *
	 * @param string $sourceJPAPath     Absolute path to an installer's JPA archive
	 * @param string $targetArchivePath Absolute path to the generated archive
	 * @param array  $options           A named key array of options (optional). This is currently not supported
	 */
	public function initialize($targetArchivePath, $options = array())
	{
		AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "AEArchiverZip :: initialize - archive $targetArchivePath");

		// Get names of temporary files
		$configuration = AEFactory::getConfiguration();
		$this->_ctrlDirFileName = tempnam($configuration->get('akeeba.basic.output_directory'), 'akzcd');
		$this->_dataFileName = $targetArchivePath;

		// If we use splitting, initialize
		if ($this->_useSplitZIP)
		{
			AEUtilLogger::WriteLog(_AE_LOG_INFO, "AEArchiverZip :: Split ZIP creation enabled");
			$this->_dataFileNameBase = dirname($targetArchivePath) . '/' . basename($targetArchivePath, '.zip');
			$this->_dataFileName = $this->_dataFileNameBase . '.z01';
		}

		$this->_ctrlDirFileName = basename($this->_ctrlDirFileName);
		$pos = strrpos($this->_ctrlDirFileName, '/');
		if ($pos !== false)
		{
			$this->_ctrlDirFileName = substr($this->_ctrlDirFileName, $pos + 1);
		}
		$pos = strrpos($this->_ctrlDirFileName, '\\');
		if ($pos !== false)
		{
			$this->_ctrlDirFileName = substr($this->_ctrlDirFileName, $pos + 1);
		}
		$this->_ctrlDirFileName = AEUtilTempfiles::registerTempFile($this->_ctrlDirFileName);

		AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "AEArchiverZip :: CntDir Tempfile = " . $this->_ctrlDirFileName);

		// Create temporary file
		if (!@touch($this->_ctrlDirFileName))
		{
			$this->setError("Could not open temporary file for ZIP archiver. Please check your temporary directory's permissions!");

			return false;
		}
		if (function_exists('chmod'))
		{
			chmod($this->_ctrlDirFileName, 0666);
		}

		// Try to kill the archive if it exists
		AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "AEArchiverZip :: Killing old archive");
		$this->fp = $this->_fopen($this->_dataFileName, "wb");
		if (!($this->fp === false))
		{
			ftruncate($this->fp, 0);
		}
		else
		{
			@unlink($this->_dataFileName);
		}
		if (!@touch($this->_dataFileName))
		{
			$this->setError("Could not open archive file for ZIP archiver. Please check your output directory's permissions!");

			return false;
		}
		if (function_exists('chmod'))
		{
			chmod($this->_dataFileName, 0666);
		}

		// On split archives, include the "Split ZIP" header, for PKZIP 2.50+ compatibility
		if ($this->_useSplitZIP)
		{
			file_put_contents($this->_dataFileName, "\x50\x4b\x07\x08");
			// Also update the statistics table that we are a multipart archive...
			$statistics = AEFactory::getStatistics();
			$statistics->updateMultipart(1);
		}
	}

	/**
	 * Creates the ZIP file out of its pieces.
	 * Official ZIP file format: http://www.pkware.com/appnote.txt
	 *
	 * @return boolean TRUE on success, FALSE on failure
	 */
	public function finalize()
	{
		// 1. Get size of central directory
		clearstatcache();
		$cdOffset = @filesize($this->_dataFileName);
		$this->_totalDataSize += $cdOffset;
		$cdSize = @filesize($this->_ctrlDirFileName);

		// 2. Append Central Directory to data file and remove the CD temp file afterwards
		if (!is_null($this->fp))
		{
			$this->_fclose($this->fp);
		}

		if (!is_null($this->cdfp))
		{
			$this->_fclose($this->cdfp);
		}

		$this->fp = $this->_fopen($this->_dataFileName, "ab");
		$this->cdfp = $this->_fopen($this->_ctrlDirFileName, "rb");

		if ($this->fp === false)
		{
			$this->setError('Could not open ZIP data file ' . $this->_dataFileName . ' for reading');

			return false;
		}

		if ($this->cdfp === false)
		{
			// Already glued, return
			$this->_fclose($this->fp);
			$this->fp = null;
			$this->cdfp = null;

			return false;
		}

		if (!$this->_useSplitZIP)
		{
			while (!feof($this->cdfp))
			{
				$chunk = fread($this->cdfp, _AKEEBA_DIRECTORY_READ_CHUNK);
				$this->_fwrite($this->fp, $chunk);

				if ($this->getError())
				{
					return;
				}
			}

			unset($chunk);

			$this->_fclose($this->cdfp);
		}
		else
		// Special considerations for Split ZIP
		{
			// Calculate size of Central Directory + EOCD records
			$comment_length = function_exists('mb_strlen') ? mb_strlen($this->_comment, '8bit') : strlen($this->_comment);
			$total_cd_eocd_size = $cdSize + 22 + $comment_length;
			// Free space on the part
			clearstatcache();
			$current_part_size = @filesize($this->_dataFileName);
			$free_space = $this->_fragmentSize - ($current_part_size === false ? 0 : $current_part_size);

			if (($free_space < $total_cd_eocd_size) && ($total_cd_eocd_size > 65536))
			{
				// Not enough space on archive for CD + EOCD, will go on separate part
				// Create new final part
				if (!$this->_createNewPart(true))
				{
					// Die if we couldn't create the new part
					$this->setError('Could not create new ZIP part file ' . basename($this->_dataFileName));

					return false;
				}
				else
				{
					// Close the old data file
					$this->_fclose($this->fp);

					// Open data file for output
					$this->fp = @$this->_fopen($this->_dataFileName, "ab");

					if ($this->fp === false)
					{
						$this->fp = null;
						$this->setError("Could not open archive file {$this->_dataFileName} for append!");

						return false;
					}
					// Write the CD record
					while (!feof($this->cdfp))
					{
						$chunk = fread($this->cdfp, _AKEEBA_DIRECTORY_READ_CHUNK);
						$this->_fwrite($this->fp, $chunk);

						if ($this->getError())
						{
							return;
						}
					}

					unset($chunk);

					$this->_fclose($this->cdfp);
					$this->cdfp = null;
				}
			}
			else
			{
				// Glue the CD + EOCD on the same part if they fit, or anyway if they are less than 64Kb.
				// NOTE: WE *MUST NOT* CREATE FRAGMENTS SMALLER THAN 64Kb!!!!
				while (!feof($this->cdfp))
				{
					$chunk = fread($this->cdfp, _AKEEBA_DIRECTORY_READ_CHUNK);
					$this->_fwrite($this->fp, $chunk);

					if ($this->getError())
					{
						return;
					}
				}

				unset($chunk);
				$this->_fclose($this->cdfp);
				$this->cdfp = null;
			}
		}

		AEUtilTempfiles::unregisterAndDeleteTempFile($this->_ctrlDirFileName);

		// 3. Write the rest of headers to the end of the ZIP file
		$this->_fclose($this->fp);
		$this->fp = null;

		clearstatcache();

		$this->fp = $this->_fopen($this->_dataFileName, "ab");

		if ($this->fp === false)
		{
			$this->setError('Could not open ' . $this->_dataFileName . ' for append');

			return false;
		}

		$this->_fwrite($this->fp, $this->_ctrlDirEnd);

		if ($this->getError())
		{
			return;
		}

		if ($this->_useSplitZIP)
		{
			// Split ZIP files, enter relevant disk number information
			$this->_fwrite($this->fp, pack('v', $this->_totalFragments - 1)); /* Number of this disk. */
			$this->_fwrite($this->fp, pack('v', $this->_totalFragments - 1)); /* Disk with central directory start. */
		}
		else
		{
			// Non-split ZIP files, the disk numbers MUST be 0
			$this->_fwrite($this->fp, pack('V', 0));
		}
		$this->_fwrite($this->fp, pack('v', $this->_totalFileEntries)); /* Total # of entries "on this disk". */
		$this->_fwrite($this->fp, pack('v', $this->_totalFileEntries)); /* Total # of entries overall. */
		$this->_fwrite($this->fp, pack('V', $cdSize)); /* Size of central directory. */
		$this->_fwrite($this->fp, pack('V', $cdOffset)); /* Offset to start of central dir. */
		$sizeOfComment = $comment_length = function_exists('mb_strlen') ? mb_strlen($this->_comment, '8bit') : strlen($this->_comment);
		// 2.0.b2 -- Write a ZIP file comment
		$this->_fwrite($this->fp, pack('v', $sizeOfComment)); /* ZIP file comment length. */
		$this->_fwrite($this->fp, $this->_comment);
		$this->_fclose($this->fp);
		//sleep(2);

		// If Split ZIP and there is no .zip file, rename the last fragment to .ZIP
		if ($this->_useSplitZIP)
		{
			$extension = substr($this->_dataFileName, -3);

			if ($extension != '.zip')
			{
				AEUtilLogger::WriteLog(_AE_LOG_DEBUG, 'Renaming last ZIP part to .ZIP extension');

				$newName = $this->_dataFileNameBase . '.zip';

				if (!@rename($this->_dataFileName, $newName))
				{
					$this->setError('Could not rename last ZIP part to .ZIP extension.');

					return false;
				}

				$this->_dataFileName = $newName;
			}
		}

		// If Split ZIP and only one fragment, change the signature
		if ($this->_useSplitZIP && ($this->_totalFragments == 1))
		{
			$this->fp = $this->_fopen($this->_dataFileName, 'r+b');
			$this->_fwrite($this->fp, "\x50\x4b\x30\x30");
		}

		if (function_exists('chmod'))
		{
			@chmod($this->_dataFileName, 0755);
		}

		return true;
	}


	/**
	 * Returns a string with the extension (including the dot) of the files produced
	 * by this class.
	 * @return string
	 */
	public function getExtension()
	{
		return '.zip';
	}


	/**
	 * The most basic file transaction: add a single entry (file or directory) to
	 * the archive.
	 *
	 * @param bool   $isVirtual        If true, the next parameter contains file data instead of a file name
	 * @param string $sourceNameOrData Absolute file name to read data from or the file data itself is $isVirtual is true
	 * @param string $targetName       The (relative) file name under which to store the file in the archive
	 *
	 * @return True on success, false otherwise
	 */
	protected function _addFile($isVirtual, &$sourceNameOrData, $targetName)
	{
		static $configuration;

		// Note down the starting disk number for Split ZIP archives
		if ($this->_useSplitZIP)
		{
			$starting_disk_number_for_this_file = $this->_currentFragment - 1;
		}
		else
		{
			$starting_disk_number_for_this_file = 0;
		}

		if (!$configuration)
		{
			$configuration = AEFactory::getConfiguration();
		}

		// Open data file for output
		if (is_null($this->fp))
		{
			$this->fp = @$this->_fopen($this->_dataFileName, "ab");
		}

		if ($this->fp === false)
		{
			$this->setError("Could not open archive file {$this->_dataFileName} for append!");

			return false;
		}

		if (!$configuration->get('volatile.engine.archiver.processingfile', false))
		{
			// See if it's a directory
			$isDir = $isVirtual ? false : is_dir($sourceNameOrData);
			// See if it's a symlink (w/out dereference)
			$isSymlink = false;
			if ($this->_symlink_store_target && !$isVirtual)
			{
				$isSymlink = is_link($sourceNameOrData);
			}

			// Get real size before compression
			if ($isVirtual)
			{
				$fileSize = function_exists('mb_strlen') ? mb_strlen($sourceNameOrData, '8bit') : strlen($sourceNameOrData);
			}
			else
			{
				if ($isSymlink)
				{
					$fileSize = function_exists('mb_strlen') ? mb_strlen(@readlink($sourceNameOrData), '8bit') : strlen(@readlink($sourceNameOrData));
				}
				else
				{
					$fileSize = $isDir ? 0 : @filesize($sourceNameOrData);
				}
			}

			// Get last modification time to store in archive
			$ftime = $isVirtual ? time() : @filemtime($sourceNameOrData);

			// Decide if we will compress
			if ($isDir || $isSymlink)
			{
				$compressionMethod = 0; // don't compress directories...
			}
			else
			{
				// Do we have plenty of memory left?
				$memLimit = ini_get("memory_limit");
				if (strstr($memLimit, 'M'))
				{
					$memLimit = (int)$memLimit * 1048576;
				}
				elseif (strstr($memLimit, 'K'))
				{
					$memLimit = (int)$memLimit * 1024;
				}
				elseif (strstr($memLimit, 'G'))
				{
					$memLimit = (int)$memLimit * 1073741824;
				}
				else
				{
					$memLimit = (int)$memLimit;
				}
				if (($memLimit == "") || ($fileSize >= _AKEEBA_COMPRESSION_THRESHOLD))
				{
					// No memory limit, or over 1Mb files => always compress up to 1Mb files (otherwise it times out)
					$compressionMethod = ($fileSize <= _AKEEBA_COMPRESSION_THRESHOLD) ? 8 : 0;
				}
				elseif (function_exists("memory_get_usage"))
				{
					// PHP can report memory usage, see if there's enough available memory; Joomla! alone eats about 5-6Mb! This code is called on files <= 1Mb
					$memLimit = $this->_return_bytes($memLimit);
					$availableRAM = $memLimit - memory_get_usage();
					$compressionMethod = (($availableRAM / 2.5) >= $fileSize) ? 8 : 0;
				}
				else
				{
					// PHP can't report memory usage, compress only files up to 512Kb (conservative approach) and hope it doesn't break
					$compressionMethod = ($fileSize <= 524288) ? 8 : 0;;
				}
			}

			$compressionMethod = function_exists("gzcompress") ? $compressionMethod : 0;

			$storedName = $targetName;

			if ($isVirtual)
			{
				AEUtilLogger::WriteLog(_AE_LOG_DEBUG, '  Virtual add:' . $storedName . ' (' . $fileSize . ') - ' . $compressionMethod);
			}

			/* "Local file header" segment. */
			$unc_len = $fileSize; // File size

			if (!$isDir)
			{
				// Get CRC for regular files, not dirs
				if ($isVirtual)
				{
					$crc = crc32($sourceNameOrData);
				}
				else
				{
					$crcCalculator = new AECRC32CalcClass;
					$crc = $crcCalculator->crc32_file($sourceNameOrData, $this->AkeebaPackerZIP_CHUNK_SIZE); // This is supposed to be the fast way to calculate CRC32 of a (large) file.
					unset($crcCalculator);

					// If the file was unreadable, $crc will be false, so we skip the file
					if ($crc === false)
					{
						$this->setWarning('Could not calculate CRC32 for ' . $sourceNameOrData);

						return false;
					}
				}
			}
			else if ($isSymlink)
			{
				$crc = crc32(@readlink($sourceNameOrData));
			}
			else
			{
				// Dummy CRC for dirs
				$crc = 0;
				$storedName .= "/";
				$unc_len = 0;
			}

			// If we have to compress, read the data in memory and compress it
			if ($compressionMethod == 8)
			{
				// Get uncompressed data
				if ($isVirtual)
				{
					$udata =& $sourceNameOrData;
				}
				else
				{
					$udata = @file_get_contents($sourceNameOrData); // PHP > 4.3.0 saves us the trouble
				}

				if ($udata === false)
				{
					// Unreadable file, skip it. Normally, we should have exited on CRC code above
					$this->setWarning('Unreadable file ' . $sourceNameOrData . '. Check permissions');

					return false;
				}
				else
				{
					// Proceed with compression
					$zdata = @gzcompress($udata);
					if ($zdata === false)
					{
						// If compression fails, let it behave like no compression was available
						$c_len = $unc_len;
						$compressionMethod = 0;
					}
					else
					{
						unset($udata);

						$zdata = substr(substr($zdata, 0, -4), 2);
						$c_len = (function_exists('mb_strlen') ? mb_strlen($zdata, '8bit') : strlen($zdata));
					}
				}
			}
			else
			{
				$c_len = $unc_len;
			}

			/* Get the hex time. */
			$dtime = dechex($this->_unix2DosTime($ftime));

			if ((function_exists('mb_strlen') ? mb_strlen($dtime, '8bit') : strlen($dtime)) < 8)
			{
				$dtime = "00000000";
			}
			$hexdtime = chr(hexdec($dtime[6] . $dtime[7])) .
				chr(hexdec($dtime[4] . $dtime[5])) .
				chr(hexdec($dtime[2] . $dtime[3])) .
				chr(hexdec($dtime[0] . $dtime[1]));

			// If it's a split ZIP file, we've got to make sure that the header can fit in the part
			if ($this->_useSplitZIP)
			{
				// Get header size, taking into account any extra header necessary
				$header_size = 30 + (function_exists('mb_strlen') ? mb_strlen($storedName, '8bit') : strlen($storedName));
				// Compare to free part space
				clearstatcache();
				$current_part_size = @filesize($this->_dataFileName);
				$free_space = $this->_fragmentSize - ($current_part_size === false ? 0 : $current_part_size);
				if ($free_space <= $header_size)
				{
					// Not enough space on current part, create new part
					if (!$this->_createNewPart())
					{
						$this->setError('Could not create new ZIP part file ' . basename($this->_dataFileName));

						return false;
					}
				}
			}

			/**/
			$old_offset = @ftell($this->fp);
			/**
			$seek_result = @fseek($this->fp, 0, SEEK_END);
			$old_offset = ($seek_result == -1) ? false : @ftell($this->fp);
			if ($old_offset === false)
			{
				@clearstatcache();
				$old_offset = @filesize($this->_dataFileName);
			}
			/**/

			// Get the file name length in bytes
			if (function_exists('mb_strlen'))
			{
				$fn_length = mb_strlen($storedName, '8bit');
			}
			else
			{
				$fn_length = strlen($storedName);
			}

			$this->_fwrite($this->fp, $this->_fileHeader); /* Begin creating the ZIP data. */
			if (!$isSymlink)
			{
				$this->_fwrite($this->fp, "\x14\x00"); /* Version needed to extract. */
			}
			else
			{
				$this->_fwrite($this->fp, "\x0a\x03"); /* Version needed to extract. */
			}
			$this->_fwrite($this->fp, pack('v', 2048)); /* General purpose bit flag. Bit 11 set = use UTF-8 encoding for filenames & comments */
			$this->_fwrite($this->fp, ($compressionMethod == 8) ? "\x08\x00" : "\x00\x00"); /* Compression method. */
			$this->_fwrite($this->fp, $hexdtime); /* Last modification time/date. */
			$this->_fwrite($this->fp, pack('V', $crc)); /* CRC 32 information. */
			if (!isset($c_len))
			{
				$c_len = $unc_len;
			}
			$this->_fwrite($this->fp, pack('V', $c_len)); /* Compressed filesize. */
			$this->_fwrite($this->fp, pack('V', $unc_len)); /* Uncompressed filesize. */
			$this->_fwrite($this->fp, pack('v', $fn_length)); /* Length of filename. */
			$this->_fwrite($this->fp, pack('v', 0)); /* Extra field length. */
			$this->_fwrite($this->fp, $storedName); /* File name. */

			// Cache useful information about the file
			if (!$isDir && !$isSymlink && !$isVirtual)
			{
				$configuration->set('volatile.engine.archiver.unc_len', $unc_len);
				$configuration->set('volatile.engine.archiver.hexdtime', $hexdtime);
				$configuration->set('volatile.engine.archiver.crc', $crc);
				$configuration->set('volatile.engine.archiver.c_len', $c_len);
				$configuration->set('volatile.engine.archiver.fn_length', $fn_length);
				$configuration->set('volatile.engine.archiver.old_offset', $old_offset);
				$configuration->set('volatile.engine.archiver.storedName', $storedName);
				$configuration->set('volatile.engine.archiver.sourceNameOrData', $sourceNameOrData);
			}
		}
		else
		{
			// Since we are continuing archiving, it's an uncompressed regular file. Set up the variables.
			$compressionMethod = 1;
			$isDir = false;
			$isSymlink = false;
			$unc_len = $configuration->get('volatile.engine.archiver.unc_len');
			$hexdtime = $configuration->get('volatile.engine.archiver.hexdtime');
			$crc = $configuration->get('volatile.engine.archiver.crc');
			$c_len = $configuration->get('volatile.engine.archiver.c_len');
			$fn_length = $configuration->get('volatile.engine.archiver.fn_length');
			$old_offset = $configuration->get('volatile.engine.archiver.old_offset');
			$storedName = $configuration->get('volatile.engine.archiver.storedName');
		}


		/* "File data" segment. */
		if ($compressionMethod == 8)
		{
			// Just dump the compressed data
			if (!$this->_useSplitZIP)
			{
				$this->_fwrite($this->fp, $zdata);
				if ($this->getError())
				{
					return;
				}
			}
			else
			{
				// Split ZIP. Check if we need to split the part in the middle of the data.
				clearstatcache();
				$current_part_size = @filesize($this->_dataFileName);
				$free_space = $this->_fragmentSize - ($current_part_size === false ? 0 : $current_part_size);
				if ($free_space >= (function_exists('mb_strlen') ? mb_strlen($zdata, '8bit') : strlen($zdata)))
				{
					// Write in one part
					$this->_fwrite($this->fp, $zdata);
					if ($this->getError())
					{
						return;
					}
				}
				else
				{
					$bytes_left = (function_exists('mb_strlen') ? mb_strlen($zdata, '8bit') : strlen($zdata));

					while ($bytes_left > 0)
					{
						clearstatcache();
						$current_part_size = @filesize($this->_dataFileName);
						$free_space = $this->_fragmentSize - ($current_part_size === false ? 0 : $current_part_size);

						// Split between parts - Write a part
						$this->_fwrite($this->fp, $zdata, min((function_exists('mb_strlen') ? mb_strlen($zdata, '8bit') : strlen($zdata)), $free_space));
						if ($this->getError())
						{
							return;
						}

						// Get the rest of the data
						$bytes_left = (function_exists('mb_strlen') ? mb_strlen($zdata, '8bit') : strlen($zdata)) - $free_space;

						if ($bytes_left > 0)
						{
							$this->_fclose($this->fp);
							$this->fp = null;

							// Create new part
							if (!$this->_createNewPart())
							{
								// Die if we couldn't create the new part
								$this->setError('Could not create new ZIP part file ' . basename($this->_dataFileName));

								return false;
							}
							else
							{
								// Open data file for output
								$this->fp = @$this->_fopen($this->_dataFileName, "ab");
								if ($this->fp === false)
								{
									$this->setError("Could not open archive file {$this->_dataFileName} for append!");

									return false;
								}
							}
							$zdata = substr($zdata, -$bytes_left);
						}
					}
				}
			}
			unset($zdata);
		}
		elseif (!($isDir || $isSymlink))
		{
			// Virtual file, just write the data!
			if ($isVirtual)
			{
				// Just dump the data
				if (!$this->_useSplitZIP)
				{
					$this->_fwrite($this->fp, $sourceNameOrData);
					if ($this->getError())
					{
						return;
					}
				}
				else
				{
					// Split ZIP. Check if we need to split the part in the middle of the data.
					clearstatcache();
					$current_part_size = @filesize($this->_dataFileName);
					$free_space = $this->_fragmentSize - ($current_part_size === false ? 0 : $current_part_size);
					if ($free_space >= (function_exists('mb_strlen') ? mb_strlen($sourceNameOrData, '8bit') : strlen($sourceNameOrData)))
					{
						// Write in one part
						$this->_fwrite($this->fp, $sourceNameOrData);
						if ($this->getError())
						{
							return;
						}
					}
					else
					{
						$bytes_left = (function_exists('mb_strlen') ? mb_strlen($sourceNameOrData, '8bit') : strlen($sourceNameOrData));

						while ($bytes_left > 0)
						{
							clearstatcache();
							$current_part_size = @filesize($this->_dataFileName);
							$free_space = $this->_fragmentSize - ($current_part_size === false ? 0 : $current_part_size);
							// Split between parts - Write first part
							$this->_fwrite($this->fp, $sourceNameOrData, min((function_exists('mb_strlen') ? mb_strlen($zdata, '8bit') : strlen($zdata)), $free_space));
							if ($this->getError())
							{
								return;
							}
							// Get the rest of the data
							$rest_size = (function_exists('mb_strlen') ? mb_strlen($sourceNameOrData, '8bit') : strlen($sourceNameOrData)) - $free_space;
							if ($rest_size > 0)
							{
								$this->_fclose($this->fp);
								$this->fp = null;

								// Create new part if required
								if (!$this->_createNewPart())
								{
									// Die if we couldn't create the new part
									$this->setError('Could not create new ZIP part file ' . basename($this->_dataFileName));

									return false;
								}
								else
								{
									// Open data file for output
									$this->fp = @$this->_fopen($this->_dataFileName, "ab");
									if ($this->fp === false)
									{
										$this->setError("Could not open archive file {$this->_dataFileName} for append!");

										return false;
									}
								}
								// Get the rest of the compressed data
								$zdata = substr($sourceNameOrData, -$rest_size);
							}
							$bytes_left = $rest_size;
						}
					}
				}
			}
			else
			{
				// IMPORTANT! Only this case can be spanned across steps: uncompressed, non-virtual data
				if ($configuration->get('volatile.engine.archiver.processingfile', false))
				{
					$sourceNameOrData = $configuration->get('volatile.engine.archiver.sourceNameOrData', '');
					$unc_len = $configuration->get('volatile.engine.archiver.unc_len', 0);
					$resume = $configuration->get('volatile.engine.archiver.resume', 0);
				}

				// Copy the file contents, ignore directories
				$zdatafp = @fopen($sourceNameOrData, "rb");
				if ($zdatafp === false)
				{
					$this->setWarning('Unreadable file ' . $sourceNameOrData . '. Check permissions');

					return false;
				}
				else
				{
					$timer = AEFactory::getTimer();
					// Seek to the resume point if required
					if ($configuration->get('volatile.engine.archiver.processingfile', false))
					{
						// Seek to new offset
						$seek_result = @fseek($zdatafp, $resume);
						if ($seek_result === -1)
						{
							// What?! We can't resume!
							$this->setError(sprintf('Could not resume packing of file %s. Your archive is damaged!', $sourceNameOrData));

							return false;
						}

						// Doctor the uncompressed size to match the remainder of the data
						$unc_len = $unc_len - $resume;
					}

					if (!$this->_useSplitZIP)
					{
						// For non Split ZIP, just dump the file very fast
						while (!feof($zdatafp) && ($timer->getTimeLeft() > 0) && ($unc_len > 0))
						{
							$zdata = fread($zdatafp, AKEEBA_CHUNK);
							$this->_fwrite($this->fp, $zdata, min((function_exists('mb_strlen') ? mb_strlen($zdata, '8bit') : strlen($zdata)), AKEEBA_CHUNK));
							$unc_len -= AKEEBA_CHUNK;
							if ($this->getError())
							{
								return;
							}
						}
						if (!feof($zdatafp) && ($unc_len != 0))
						{
							// We have to break, or we'll time out!
							$resume = @ftell($zdatafp);
							$configuration->set('volatile.engine.archiver.resume', $resume);
							$configuration->set('volatile.engine.archiver.processingfile', true);

							return true;
						}
					}
					else
					{
						// Split ZIP - Do we have enough space to host the whole file?
						clearstatcache();
						$current_part_size = @filesize($this->_dataFileName);
						$free_space = $this->_fragmentSize - ($current_part_size === false ? 0 : $current_part_size);
						if ($free_space >= $unc_len)
						{
							// Yes, it will fit inside this part, do quick copy
							while (!feof($zdatafp) && ($timer->getTimeLeft() > 0) && ($unc_len > 0))
							{
								$zdata = fread($zdatafp, AKEEBA_CHUNK);
								$this->_fwrite($this->fp, $zdata, min((function_exists('mb_strlen') ? mb_strlen($zdata, '8bit') : strlen($zdata)), AKEEBA_CHUNK));
								$unc_len -= AKEEBA_CHUNK;
								if ($this->getError())
								{
									return;
								}

							}
							if (!feof($zdatafp) && ($unc_len != 0))
							{
								// We have to break, or we'll time out!
								$resume = @ftell($zdatafp);
								$configuration->set('volatile.engine.archiver.resume', $resume);
								$configuration->set('volatile.engine.archiver.processingfile', true);

								return true;
							}
						}
						else
						{
							// No, we'll have to split between parts. We'll loop until we run
							// out of space.
							while (!feof($zdatafp) && ($timer->getTimeLeft() > 0))
							{
								// No, we'll have to split between parts. Write the first part
								// Find optimal chunk size
								clearstatcache();
								$current_part_size = @filesize($this->_dataFileName);
								$free_space = $this->_fragmentSize - ($current_part_size === false ? 0 : $current_part_size);

								$chunk_size_primary = min(AKEEBA_CHUNK, $free_space);
								if ($chunk_size_primary <= 0)
								{
									$chunk_size_primary = max(AKEEBA_CHUNK, $free_space);
								}
								// Calculate if we have to read some more data (smaller chunk size)
								// and how many times we must read w/ the primary chunk size
								$chunk_size_secondary = $free_space % $chunk_size_primary;
								$loop_times = ($free_space - $chunk_size_secondary) / $chunk_size_primary;
								// Read and write with the primary chunk size
								for ($i = 1; $i <= $loop_times; $i++)
								{
									$zdata = fread($zdatafp, $chunk_size_primary);
									$this->_fwrite($this->fp, $zdata, min((function_exists('mb_strlen') ? mb_strlen($zdata, '8bit') : strlen($zdata)), $chunk_size_primary));
									$unc_len -= $chunk_size_primary;
									if ($this->getError())
									{
										return;
									}

									// Do we have enough time to proceed?
									if ((!feof($zdatafp)) && ($unc_len != 0) && ($timer->getTimeLeft() <= 0))
									{
										// No, we have to break, or we'll time out!
										$resume = @ftell($zdatafp);
										$configuration->set('volatile.engine.archiver.resume', $resume);
										$configuration->set('volatile.engine.archiver.processingfile', true);

										return true;
									}
								}
								// Read and write w/ secondary chunk size, if non-zero
								if ($chunk_size_secondary > 0)
								{
									$zdata = fread($zdatafp, $chunk_size_secondary);
									$this->_fwrite($this->fp, $zdata, min((function_exists('mb_strlen') ? mb_strlen($zdata, '8bit') : strlen($zdata)), $chunk_size_secondary));
									$unc_len -= $chunk_size_secondary;
									if ($this->getError())
									{
										return;
									}
								}

								// Do we have enough time to proceed?
								if ((!feof($zdatafp)) && ($unc_len != 0) && ($timer->getTimeLeft() <= 0))
								{
									// No, we have to break, or we'll time out!
									$resume = @ftell($zdatafp);
									$configuration->set('volatile.engine.archiver.resume', $resume);
									$configuration->set('volatile.engine.archiver.processingfile', true);

									// ...and create a new part as well
									if (!$this->_createNewPart())
									{
										// Die if we couldn't create the new part
										$this->setError('Could not create new ZIP part file ' . basename($this->_dataFileName));

										return false;
									}

									// ...then, return
									return true;
								}

								// Create new ZIP part, but only if we'll have more data to write
								if (!feof($zdatafp) && ($unc_len > 0))
								{
									// Create new ZIP part
									if (!$this->_createNewPart())
									{
										// Die if we couldn't create the new part
										$this->setError('Could not create new ZIP part file ' . basename($this->_dataFileName));

										return false;
									}
									else
									{
										// Close the old data file
										$this->_fclose($this->fp);
										$this->fp = null;

										// We have created the part. If the user asked for immediate post-proc, break step now.
										if ($configuration->get('engine.postproc.common.after_part', 0))
										{
											$resume = @ftell($zdatafp);
											$configuration->set('volatile.engine.archiver.resume', $resume);
											$configuration->set('volatile.engine.archiver.processingfile', true);

											$configuration->set('volatile.breakflag', true);
											@fclose($zdatafp);

											return true;
										}

										// Open data file for output
										$this->fp = @$this->_fopen($this->_dataFileName, "ab");
										if ($this->fp === false)
										{
											$this->setError("Could not open archive file {$this->_dataFileName} for append!");

											return false;
										}
									}
								}

							} // end while

						}
					}

					@fclose($zdatafp);
				}
			}
		}
		elseif ($isSymlink)
		{
			$this->_fwrite($this->fp, @readlink($sourceNameOrData));
		}

		// Open the central directory file for append
		if (is_null($this->cdfp))
		{
			$this->cdfp = @$this->_fopen($this->_ctrlDirFileName, "ab");
		}

		if ($this->cdfp === false)
		{
			$this->setError("Could not open Central Directory temporary file for append!");

			return false;
		}

		$this->_fwrite($this->cdfp, $this->_ctrlDirHeader);

		if (!$isSymlink)
		{
			$this->_fwrite($this->cdfp, "\x14\x00"); /* Version made by (always set to 2.0). */
			$this->_fwrite($this->cdfp, "\x14\x00"); /* Version needed to extract */
			$this->_fwrite($this->cdfp, pack('v', 2048)); /* General purpose bit flag */
			$this->_fwrite($this->cdfp, ($compressionMethod == 8) ? "\x08\x00" : "\x00\x00"); /* Compression method. */
		}
		else
		{
			// Symlinks get special treatment
			$this->_fwrite($this->cdfp, "\x14\x03"); /* Version made by (version 2.0 with UNIX extensions). */
			$this->_fwrite($this->cdfp, "\x0a\x03"); /* Version needed to extract */
			$this->_fwrite($this->cdfp, pack('v', 2048)); /* General purpose bit flag */
			$this->_fwrite($this->cdfp, "\x00\x00"); /* Compression method. */
		}

		$this->_fwrite($this->cdfp, $hexdtime); /* Last mod time/date. */
		$this->_fwrite($this->cdfp, pack('V', $crc)); /* CRC 32 information. */
		$this->_fwrite($this->cdfp, pack('V', $c_len)); /* Compressed filesize. */
		if ($compressionMethod == 0)
		{
			// When we are not compressing, $unc_len is being reduced to 0 while backing up.
			// With this trick, we always store the correct length, as in this case the compressed
			// and uncompressed length is always the same.
			$this->_fwrite($this->cdfp, pack('V', $c_len)); /* Uncompressed filesize. */
		}
		else
		{
			// When compressing, the uncompressed length differs from compressed length
			// and this line writes the correct value.
			$this->_fwrite($this->cdfp, pack('V', $unc_len)); /* Uncompressed filesize. */
		}
		$this->_fwrite($this->cdfp, pack('v', $fn_length)); /* Length of filename. */
		$this->_fwrite($this->cdfp, pack('v', 0)); /* Extra field length. */
		$this->_fwrite($this->cdfp, pack('v', 0)); /* File comment length. */
		$this->_fwrite($this->cdfp, pack('v', $starting_disk_number_for_this_file)); /* Disk number start. */
		$this->_fwrite($this->cdfp, pack('v', 0)); /* Internal file attributes. */
		if (!$isSymlink)
		{
			$this->_fwrite($this->cdfp, pack('V', $isDir ? 0x41FF0010 : 0xFE49FFE0)); /* External file attributes -   'archive' bit set. */
		}
		else
		{
			// For SymLinks we store UNIX file attributes
			$this->_fwrite($this->cdfp, "\x20\x80\xFF\xA1"); /* External file attributes for Symlink. */
		}
		$this->_fwrite($this->cdfp, pack('V', $old_offset)); /* Relative offset of local header. */
		$this->_fwrite($this->cdfp, $storedName); /* File name. */
		/* Optional extra field, file comment goes here. */

		// Finally, increase the file counter by one
		$this->_totalFileEntries++;

		// Uncache data
		$configuration->set('volatile.engine.archiver.sourceNameOrData', null);
		$configuration->set('volatile.engine.archiver.unc_len', null);
		$configuration->set('volatile.engine.archiver.resume', null);
		$configuration->set('volatile.engine.archiver.hexdtime', null);
		$configuration->set('volatile.engine.archiver.crc', null);
		$configuration->set('volatile.engine.archiver.c_len', null);
		$configuration->set('volatile.engine.archiver.fn_length', null);
		$configuration->set('volatile.engine.archiver.old_offset', null);
		$configuration->set('volatile.engine.archiver.storedName', null);
		$configuration->set('volatile.engine.archiver.sourceNameOrData', null);

		$configuration->set('volatile.engine.archiver.processingfile', false);

		// ... and return TRUE = success
		return true;
	}

	// ------------------------------------------------------------------------
	// Archiver-specific utility functions
	// ------------------------------------------------------------------------

	/**
	 * Converts a UNIX timestamp to a 4-byte DOS date and time format
	 * (date in high 2-bytes, time in low 2-bytes allowing magnitude
	 * comparison).
	 *
	 * @param integer $unixtime The current UNIX timestamp.
	 *
	 * @return integer  The current date in a 4-byte DOS format.
	 */
	private function _unix2DOSTime($unixtime = null)
	{
		$timearray = (is_null($unixtime)) ? getdate() : getdate($unixtime);

		if ($timearray['year'] < 1980)
		{
			$timearray['year'] = 1980;
			$timearray['mon'] = 1;
			$timearray['mday'] = 1;
			$timearray['hours'] = 0;
			$timearray['minutes'] = 0;
			$timearray['seconds'] = 0;
		}

		return (($timearray['year'] - 1980) << 25) |
		($timearray['mon'] << 21) |
		($timearray['mday'] << 16) |
		($timearray['hours'] << 11) |
		($timearray['minutes'] << 5) |
		($timearray['seconds'] >> 1);
	}

	private function _createNewPart($finalPart = false)
	{
		// Close any open file pointers
		if (is_resource($this->fp))
		{
			$this->_fclose($this->fp);
		}

		if (is_resource($this->cdfp))
		{
			$this->_fclose($this->cdfp);
		}

		// Remove the just finished part from the list of resumable offsets
		$this->_removeFromOffsetsList($this->_dataFileName);

		// Set the file pointers to null
		$this->fp = null;
		$this->cdfp = null;

		// Push the previous part if we have to post-process it immediately
		$configuration = AEFactory::getConfiguration();
		if ($configuration->get('engine.postproc.common.after_part', 0))
		{
			$this->finishedPart[] = $this->_dataFileName;
		}

		// Add the part's size to our rolling sum
		clearstatcache();
		$this->_totalDataSize += filesize($this->_dataFileName);

		$this->_totalFragments++;
		$this->_currentFragment = $this->_totalFragments;
		if ($finalPart)
		{
			$this->_dataFileName = $this->_dataFileNameBase . '.zip';
		}
		else
		{
			$this->_dataFileName = $this->_dataFileNameBase . '.z' . sprintf('%02d', $this->_currentFragment);
		}
		AEUtilLogger::WriteLog(_AE_LOG_INFO, 'Creating new ZIP part #' . $this->_currentFragment . ', file ' . $this->_dataFileName);
		// Inform CUBE that we have changed the multipart number
		$statistics = AEFactory::getStatistics();
		$statistics->updateMultipart($this->_totalFragments);
		// Try to remove any existing file
		@unlink($this->_dataFileName);
		// Touch the new file
		$result = @touch($this->_dataFileName);
		if (function_exists('chmod'))
		{
			chmod($this->_dataFileName, 0666);
		}

		return $result;
	}
}

// ===================================================================================================

/**
 * A handy class to abstract the calculation of CRC32 of files under various
 * server conditions and versions of PHP.
 */
class AECRC32CalcClass
{
	/**
	 * Returns the CRC32 of a file, selecting the more appropriate algorithm.
	 *
	 * @param string  $filename                   Absolute path to the file being processed
	 * @param integer $AkeebaPackerZIP_CHUNK_SIZE Obsoleted
	 *
	 * @return integer The CRC32 in numerical form
	 */
	public function crc32_file($filename, $AkeebaPackerZIP_CHUNK_SIZE)
	{
		static $configuration;

		if (!$configuration)
		{
			$configuration = AEFactory::getConfiguration();
		}

		if (function_exists("hash_file"))
		{
			$res = $this->crc32_file_php512($filename);
			AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "File $filename - CRC32 = " . dechex($res) . " [PHP512]");
		}
		else if (function_exists("file_get_contents") && (@filesize($filename) <= $AkeebaPackerZIP_CHUNK_SIZE))
		{
			$res = $this->crc32_file_getcontents($filename);
			AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "File $filename - CRC32 = " . dechex($res) . " [GETCONTENTS]");
		}
		else
		{
			$res = $this->crc32_file_php4($filename, $AkeebaPackerZIP_CHUNK_SIZE);
			AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "File $filename - CRC32 = " . dechex($res) . " [PHP4]");
		}

		if ($res === false)
		{
			$this->setWarning("File $filename - NOT READABLE: CRC32 IS WRONG!");
		}

		return $res;
	}

	/**
	 * Very efficient CRC32 calculation for PHP 5.1.2 and greater, requiring
	 * the 'hash' PECL extension
	 *
	 * @param string $filename Absolute filepath
	 *
	 * @return integer The CRC32
	 */
	private function crc32_file_php512($filename)
	{
		// Detection of buggy PHP hosts
		static $mustInvert = null;
		if (is_null($mustInvert))
		{
			$test_crc = @hash('crc32b', 'test', false);
			$mustInvert = (strtolower($test_crc) == '0c7e7fd8'); // Normally, it's D87F7E0C :)
			if ($mustInvert)
			{
				AEUtilLogger::WriteLog(_AE_LOG_WARNING, 'Your server has a buggy PHP version which produces inverted CRC32 values. Attempting a workaround. ZIP files may appear as corrupt.');
			}
		}

		$res = @hash_file('crc32b', $filename, false);
		if ($mustInvert)
		{
			// Workaround for buggy PHP versions (I think before 5.1.8) which produce inverted CRC32 sums
			$res2 = substr($res, 6, 2) . substr($res, 4, 2) . substr($res, 2, 2) . substr($res, 0, 2);
			$res = $res2;
		}
		$res = hexdec($res);

		return $res;
	}

	/**
	 * A compatible CRC32 calculation using file_get_contents, utilizing immense
	 * ammounts of RAM
	 *
	 * @param string $filename
	 *
	 * @return integer
	 */
	private function crc32_file_getcontents($filename)
	{
		return crc32(@file_get_contents($filename));
	}

	/**
	 * There used to be a workaround for large files under PHP4. However, it never
	 * really worked, so it is removed and a warning is posted instead.
	 *
	 * @param string  $filename
	 * @param integer $AkeebaPackerZIP_CHUNK_SIZE
	 *
	 * @return integer
	 */
	private function crc32_file_php4($filename, $AkeebaPackerZIP_CHUNK_SIZE)
	{
		$this->setWarning("Function hash_file not detected processing the 'large'' file $filename; it will appear as seemingly corrupt in the archive. Only the CRC32 is invalid, though. Please read our forum announcement for explanation of this message.");

		return 0;
	}
}