/*
  +----------------------------------------------------------------------+
  | phar php single-file executable PHP extension                        |
  +----------------------------------------------------------------------+
  | Copyright © The PHP Group and Contributors.                          |
  +----------------------------------------------------------------------+
  | This source file is subject to the Modified BSD License that is      |
  | bundled with this package in the file LICENSE, and is available      |
  | through the World Wide Web at <https://www.php.net/license/>.        |
  |                                                                      |
  | SPDX-License-Identifier: BSD-3-Clause                                |
  +----------------------------------------------------------------------+
  | Authors: Gregory Beaver <cellog@php.net>                             |
  |          Marcus Boerger <helly@php.net>                              |
  +----------------------------------------------------------------------+
*/

#define PHAR_MAIN 1
#include "phar_internal.h"
#include "php_phar.h"
#include "SAPI.h"
#include "func_interceptors.h"
#include "ext/standard/crc32.h"
#include "ext/standard/php_var.h"
#include "ext/standard/php_string.h" /* For php_stristr() */
#include "ext/standard/info.h"
#include "zend_smart_str.h"

static void destroy_phar_data(zval *zv);

ZEND_DECLARE_MODULE_GLOBALS(phar)
static zend_string *(*phar_save_resolve_path)(zend_string *filename);

/**
 * set's phar->is_writeable based on the current INI value
 */
static int phar_set_writeable_bit(zval *zv, void *argument) /* {{{ */
{
	bool keep = *(bool *)argument;
	phar_archive_data *phar = (phar_archive_data *)Z_PTR_P(zv);

	if (!phar->is_data) {
		phar->is_writeable = !keep;
	}

	return ZEND_HASH_APPLY_KEEP;
}
/* }}} */

/* if the original value is 0 (disabled), then allow setting/unsetting at will. Otherwise only allow 1 (enabled), and error on disabling */
ZEND_INI_MH(phar_ini_modify_handler) /* {{{ */
{
	bool old, ini;

	if (ZSTR_LEN(entry->name) == sizeof("phar.readonly")-1) {
		old = PHAR_G(readonly_orig);
	} else {
		old = PHAR_G(require_hash_orig);
	}

	ini = zend_ini_parse_bool(new_value);

	/* do not allow unsetting in runtime */
	if (stage == ZEND_INI_STAGE_STARTUP) {
		if (ZSTR_LEN(entry->name) == sizeof("phar.readonly")-1) {
			PHAR_G(readonly_orig) = ini;
		} else {
			PHAR_G(require_hash_orig) = ini;
		}
	} else if (old && !ini) {
		return FAILURE;
	}

	if (ZSTR_LEN(entry->name) == sizeof("phar.readonly")-1) {
		PHAR_G(readonly) = ini;
		if (PHAR_G(request_init) && HT_IS_INITIALIZED(&PHAR_G(phar_fname_map))) {
			zend_hash_apply_with_argument(&(PHAR_G(phar_fname_map)), phar_set_writeable_bit, (void *)&ini);
		}
	} else {
		PHAR_G(require_hash) = ini;
	}

	return SUCCESS;
}
/* }}}*/

/* this global stores the global cached pre-parsed manifests */
HashTable cached_phars;
HashTable cached_alias;

static void phar_split_cache_list(void) /* {{{ */
{
	char *tmp;
	char *key, *lasts;
	char ds[2];
	phar_archive_data *phar;
	uint32_t i = 0;

	if (!PHAR_G(cache_list) || !(PHAR_G(cache_list)[0])) {
		return;
	}

	ds[0] = DEFAULT_DIR_SEPARATOR;
	ds[1] = '\0';
	tmp = estrdup(PHAR_G(cache_list));

	/* fake request startup */
	PHAR_G(request_init) = 1;
	zend_init_rsrc_list();
	EG(regular_list).nNextFreeElement=1;	/* we don't want resource id 0 */

	PHAR_G(has_bz2) = zend_hash_str_exists(&module_registry, "bz2", sizeof("bz2")-1);
	PHAR_G(has_zlib) = zend_hash_str_exists(&module_registry, "zlib", sizeof("zlib")-1);
	/* these two are dummies and will be destroyed later */
	zend_hash_init(&cached_phars, sizeof(phar_archive_data*), zend_get_hash_value, destroy_phar_data,  1);
	zend_hash_init(&cached_alias, sizeof(phar_archive_data*), zend_get_hash_value, NULL, 1);
	/* these two are real and will be copied over cached_phars/cached_alias later */
	zend_hash_init(&(PHAR_G(phar_fname_map)), sizeof(phar_archive_data*), zend_get_hash_value, destroy_phar_data,  1);
	zend_hash_init(&(PHAR_G(phar_alias_map)), sizeof(phar_archive_data*), zend_get_hash_value, NULL, 1);
	PHAR_G(manifest_cached) = 1;
	PHAR_G(persist) = 1;

	for (key = php_strtok_r(tmp, ds, &lasts);
			key;
			key = php_strtok_r(NULL, ds, &lasts)) {
		size_t len;
		const char *end = strchr(key, DEFAULT_DIR_SEPARATOR);
		if (end) {
			len = end - key;
		} else {
			len = strlen(key);
		}

		if (SUCCESS == phar_open_from_filename(key, len, NULL, 0, &phar, NULL)) {
			phar->phar_pos = i++;
			php_stream_close(phar->fp);
			phar->fp = NULL;
		} else {
			PHAR_G(persist) = 0;
			PHAR_G(manifest_cached) = 0;
			efree(tmp);
			zend_hash_destroy(&(PHAR_G(phar_fname_map)));
			HT_INVALIDATE(&PHAR_G(phar_fname_map));
			zend_hash_destroy(&(PHAR_G(phar_alias_map)));
			HT_INVALIDATE(&PHAR_G(phar_alias_map));
			zend_hash_destroy(&cached_phars);
			zend_hash_destroy(&cached_alias);
			zend_hash_graceful_reverse_destroy(&EG(regular_list));
			memset(&EG(regular_list), 0, sizeof(HashTable));
			/* free cached manifests */
			PHAR_G(request_init) = 0;
			return;
		}
	}

	PHAR_G(persist) = 0;
	PHAR_G(request_init) = 0;
	/* destroy dummy values from before */
	zend_hash_destroy(&cached_phars);
	zend_hash_destroy(&cached_alias);
	cached_phars = PHAR_G(phar_fname_map);
	cached_alias = PHAR_G(phar_alias_map);
	HT_INVALIDATE(&PHAR_G(phar_fname_map));
	HT_INVALIDATE(&PHAR_G(phar_alias_map));
	zend_hash_graceful_reverse_destroy(&EG(regular_list));
	memset(&EG(regular_list), 0, sizeof(HashTable));
	efree(tmp);
}
/* }}} */

ZEND_INI_MH(phar_ini_cache_list) /* {{{ */
{
	PHAR_G(cache_list) = ZSTR_VAL(new_value);

	if (stage == ZEND_INI_STAGE_STARTUP) {
		phar_split_cache_list();
	}

	return SUCCESS;
}
/* }}} */

PHP_INI_BEGIN()
	STD_PHP_INI_BOOLEAN("phar.readonly", "1", PHP_INI_ALL, phar_ini_modify_handler, readonly, zend_phar_globals, phar_globals)
	STD_PHP_INI_BOOLEAN("phar.require_hash", "1", PHP_INI_ALL, phar_ini_modify_handler, require_hash, zend_phar_globals, phar_globals)
	STD_PHP_INI_ENTRY("phar.cache_list", "", PHP_INI_SYSTEM, phar_ini_cache_list, cache_list, zend_phar_globals, phar_globals)
PHP_INI_END()

/**
 * When all uses of a phar have been concluded, this frees the manifest
 * and the phar slot
 */
void phar_destroy_phar_data(phar_archive_data *phar) /* {{{ */
{
	if (phar->alias) {
		pefree(phar->alias, phar->is_persistent);
		phar->alias = NULL;
	}

	if (phar->fname) {
		zend_string_release_ex(phar->fname, phar->is_persistent);
		phar->fname = NULL;
	}

	if (phar->signature) {
		pefree(phar->signature, phar->is_persistent);
		phar->signature = NULL;
	}

	if (HT_IS_INITIALIZED(&phar->manifest)) {
		zend_hash_destroy(&phar->manifest);
		HT_INVALIDATE(&phar->manifest);
	}

	if (HT_IS_INITIALIZED(&phar->mounted_dirs)) {
		zend_hash_destroy(&phar->mounted_dirs);
		HT_INVALIDATE(&phar->mounted_dirs);
	}

	if (HT_IS_INITIALIZED(&phar->virtual_dirs)) {
		zend_hash_destroy(&phar->virtual_dirs);
		HT_INVALIDATE(&phar->virtual_dirs);
	}

	phar_metadata_tracker_free(&phar->metadata_tracker, phar->is_persistent);

	if (phar->fp) {
		php_stream_close(phar->fp);
		phar->fp = 0;
	}

	if (phar->ufp) {
		php_stream_close(phar->ufp);
		phar->ufp = 0;
	}

	pefree(phar, phar->is_persistent);
}
/* }}}*/

/**
 * Delete refcount and destruct if needed. On destruct return 1 else 0.
 */
bool phar_archive_delref(phar_archive_data *phar) /* {{{ */
{
	if (phar->is_persistent) {
		return false;
	}

	if (--phar->refcount < 0) {
		if (PHAR_G(request_done)
		|| zend_hash_del(&(PHAR_G(phar_fname_map)), phar->fname) != SUCCESS) {
			phar_destroy_phar_data(phar);
		}
		return true;
	} else if (!phar->refcount) {
		/* invalidate phar cache */
		PHAR_G(last_phar) = NULL;
		PHAR_G(last_alias) = NULL;
		PHAR_G(last_phar_name) = NULL;

		/* This is a new phar that has perhaps had an alias/metadata set, but has never been flushed. */
		bool remove_fname_cache = !zend_hash_num_elements(&phar->manifest);

		if (phar->fp && (!(phar->flags & PHAR_FILE_COMPRESSION_MASK) || !phar->alias)) {
			/* close open file handle - allows removal or rename of
			the file on windows, which has greedy locking
			only close if the archive was not already compressed.
			We're also closing compressed files to save resources, but only if the archive isn't aliased.
			If it was compressed, then the fp does not refer to the original compressed file:
			it refers to the **uncompressed** filtered file stream.
			Therefore, upon closing a compressed file we need to invalidate the phar archive such
			that the code that reopens the phar will not try to use the **compressed** file as if it was uncompressed.
			That would result in treating compressed file data as if it were compressed and using uncompressed file offsets
			on the compressed file. */
			php_stream_close(phar->fp);
			phar->fp = NULL;
			remove_fname_cache |= phar->flags & PHAR_FILE_COMPRESSION_MASK;
		}

		if (remove_fname_cache) {
			if (zend_hash_del(&(PHAR_G(phar_fname_map)), phar->fname) != SUCCESS) {
				phar_destroy_phar_data(phar);
			}
			return true;
		}
	}
	return false;
}
/* }}}*/

/**
 * Destroy phar's in shutdown, here we don't care about aliases
 */
static void destroy_phar_data_only(zval *zv) /* {{{ */
{
	phar_archive_data *phar_data = (phar_archive_data *) Z_PTR_P(zv);

	if (EG(exception) || --phar_data->refcount < 0) {
		phar_destroy_phar_data(phar_data);
	}
}
/* }}}*/

/**
 * Delete aliases to phar's that got kicked out of the global table
 */
static int phar_unalias_apply(zval *zv, void *argument) /* {{{ */
{
	return Z_PTR_P(zv) == argument ? ZEND_HASH_APPLY_REMOVE : ZEND_HASH_APPLY_KEEP;
}
/* }}} */

/**
 * Delete aliases to phar's that got kicked out of the global table
 */
static int phar_tmpclose_apply(zval *zv) /* {{{ */
{
	phar_entry_info *entry = (phar_entry_info *) Z_PTR_P(zv);

	if (entry->fp_type != PHAR_TMP) {
		return ZEND_HASH_APPLY_KEEP;
	}

	if (entry->fp && !entry->fp_refcount) {
		php_stream_close(entry->fp);
		entry->fp = NULL;
	}

	return ZEND_HASH_APPLY_KEEP;
}
/* }}} */

/**
 * Filename map destructor
 */
static void destroy_phar_data(zval *zv) /* {{{ */
{
	phar_archive_data *phar_data = (phar_archive_data *)Z_PTR_P(zv);

	if (PHAR_G(request_ends)) {
		/* first, iterate over the manifest and close all PHAR_TMP entry fp handles,
		this prevents unnecessary unfreed stream resources */
		zend_hash_apply(&(phar_data->manifest), phar_tmpclose_apply);
		destroy_phar_data_only(zv);
		return;
	}

	zend_hash_apply_with_argument(&(PHAR_G(phar_alias_map)), phar_unalias_apply, phar_data);

	if (--phar_data->refcount < 0) {
		phar_destroy_phar_data(phar_data);
	}
}
/* }}}*/

/**
 * destructor for the manifest hash, frees each file's entry
 */
void destroy_phar_manifest_entry_int(phar_entry_info *entry) /* {{{ */
{

	if (entry->cfp) {
		php_stream_close(entry->cfp);
		entry->cfp = 0;
	}

	if (entry->fp) {
		php_stream_close(entry->fp);
		entry->fp = 0;
	}

	phar_metadata_tracker_free(&entry->metadata_tracker, entry->is_persistent);

	zend_string_release_ex(entry->filename, entry->is_persistent);

	if (entry->symlink) {
		zend_string_release_ex(entry->symlink, entry->is_persistent);
		entry->symlink = NULL;
	}

	if (entry->tmp) {
		pefree(entry->tmp, entry->is_persistent);
		entry->tmp = 0;
	}
}
/* }}} */

void destroy_phar_manifest_entry(zval *zv) /* {{{ */
{
	phar_entry_info *entry = Z_PTR_P(zv);
	destroy_phar_manifest_entry_int(entry);
	pefree(entry, entry->is_persistent);
}
/* }}} */

void phar_entry_delref(phar_entry_data *idata) /* {{{ */
{
	if (idata->internal_file && !idata->internal_file->is_persistent) {
		if (--idata->internal_file->fp_refcount < 0) {
			idata->internal_file->fp_refcount = 0;
		}

		if (idata->fp && idata->fp != idata->phar->fp && idata->fp != idata->phar->ufp && idata->fp != idata->internal_file->fp) {
			php_stream_close(idata->fp);
		}
		/* if phar_get_or_create_entry_data returns a sub-directory, we have to free it */
		if (idata->internal_file->is_temp_dir) {
			destroy_phar_manifest_entry_int(idata->internal_file);
			efree(idata->internal_file);
		}
	}

	phar_archive_delref(idata->phar);
	efree(idata);
}
/* }}} */

/**
 * Removes an entry, either by actually removing it or by marking it.
 */
ZEND_ATTRIBUTE_NONNULL void phar_entry_remove(phar_entry_data *idata, char **error) /* {{{ */
{
	phar_archive_data *phar;

	phar = idata->phar;

	if (idata->internal_file->fp_refcount < 2 && idata->internal_file->fileinfo_lock_count == 0) {
		if (idata->fp && idata->fp != idata->phar->fp && idata->fp != idata->phar->ufp && idata->fp != idata->internal_file->fp) {
			php_stream_close(idata->fp);
		}
		zend_hash_del(&idata->phar->manifest, idata->internal_file->filename);
		idata->phar->refcount--;
		efree(idata);
	} else {
		idata->internal_file->is_deleted = 1;
		phar_entry_delref(idata);
	}

	if (!phar->donotflush) {
		phar_flush(phar, error);
	}
}
/* }}} */

#define MAPPHAR_ALLOC_FAIL(msg) \
	if (fp) {\
		php_stream_close(fp);\
	}\
	if (error) {\
		spprintf(error, 0, msg, fname);\
	}\
	return FAILURE;

#define MAPPHAR_FAIL(msg) \
	efree(savebuf);\
	if (mydata) {\
		phar_destroy_phar_data(mydata);\
	}\
	if (signature) {\
		pefree(signature, PHAR_G(persist));\
	}\
	MAPPHAR_ALLOC_FAIL(msg)

#ifdef WORDS_BIGENDIAN
# define PHAR_GET_32(buffer, var) \
	var = ((uint32_t)(((unsigned char*)(buffer))[3]) << 24) \
		| ((uint32_t)(((unsigned char*)(buffer))[2]) << 16) \
		| ((uint32_t)(((unsigned char*)(buffer))[1]) <<  8) \
		| ((uint32_t)((unsigned char*)(buffer))[0]); \
	(buffer) += 4
#else
# define PHAR_GET_32(buffer, var) \
	memcpy(&var, buffer, sizeof(var)); \
	buffer += 4
#endif
#define PHAR_ZIP_16(var) ((uint16_t)((((uint16_t)var[0]) & 0xff) | \
	(((uint16_t)var[1]) & 0xff) << 8))
#define PHAR_ZIP_32(var) ((uint32_t)((((uint32_t)var[0]) & 0xff) | \
	(((uint32_t)var[1]) & 0xff) << 8 | \
	(((uint32_t)var[2]) & 0xff) << 16 | \
	(((uint32_t)var[3]) & 0xff) << 24))

/**
 * Open an already loaded phar
 */
static zend_result phar_open_parsed_phar(
	char *fname,
	size_t fname_len,
	/* copyable & hash update */ zend_string *alias,
	bool is_data,
	uint32_t options,
	phar_archive_data** pphar,
	char **error
) {
#ifdef PHP_WIN32
	char *save_fname;
	ALLOCA_FLAG(fname_use_heap)
#endif

	if (error) {
		*error = NULL;
	}
#ifdef PHP_WIN32
	save_fname = fname;
	if (memchr(fname, '\\', fname_len)) {
		fname = do_alloca(fname_len + 1, fname_use_heap);
		memcpy(fname, save_fname, fname_len);
		fname[fname_len] = '\0';
		phar_unixify_path_separators(fname, fname_len);
	}
#endif
	const char *alias_cstr = alias ? ZSTR_VAL(alias) : NULL;
	size_t alias_len = alias ? ZSTR_LEN(alias) : 0;
	phar_archive_data *phar = phar_get_archive(fname, fname_len, alias_cstr, alias_len, error);
	/* logic is as follows:
	   - If no alias was passed in, then it can match either and be valid
	   - If an explicit alias was requested, ensure the filename passed in matches the phar's filename.
	 */
	bool process_phar = phar && (!alias || zend_string_equals_cstr(phar->fname, fname, fname_len));
#ifdef PHP_WIN32
	if (fname != save_fname) {
		free_alloca(fname, fname_use_heap);
		fname = save_fname;
	}
#endif

	if (process_phar) {
		/* logic above is as follows:
		   If an explicit alias was requested, ensure the filename passed in
		   matches the phar's filename.
		   If no alias was passed in, then it can match either and be valid
		 */

		if (!is_data) {
			/* prevent any ".phar" without a stub getting through */
			if (!phar->halt_offset && !phar->is_brandnew && (phar->is_tar || phar->is_zip)) {
				if (PHAR_G(readonly) && !zend_hash_str_exists(&(phar->manifest), ZEND_STRL(".phar/stub.php"))) {
					if (error) {
						spprintf(error, 0, "'%s' is not a phar archive. Use PharData::__construct() for a standard zip or tar archive", fname);
					}
					return FAILURE;
				}
			}
		}

		if (pphar) {
			*pphar = phar;
		}

		return SUCCESS;
	} else {
		if (pphar) {
			*pphar = NULL;
		}

		if (phar && error && !(options & REPORT_ERRORS)) {
			efree(error);
		}

		return FAILURE;
	}
}

/**
 * Attempt to serialize the data.
 * Callers are responsible for handling EG(exception) if one occurs.
 */
void phar_metadata_tracker_try_ensure_has_serialized_data(phar_metadata_tracker *tracker, bool persistent) /* {{{ */
{
	php_serialize_data_t metadata_hash;
	smart_str metadata_str = {0};
	if (tracker->str || Z_ISUNDEF(tracker->val)) {
		/* Already has serialized the value or there is no value */
		return;
	}
	/* Assert it should not be possible to create raw zvals in a persistent phar (i.e. from cache_list) */
	ZEND_ASSERT(!persistent);

	PHP_VAR_SERIALIZE_INIT(metadata_hash);
	php_var_serialize(&metadata_str, &tracker->val, &metadata_hash);
	PHP_VAR_SERIALIZE_DESTROY(metadata_hash);
	if (!metadata_str.s) {
		return;
	}
	tracker->str = metadata_str.s;
}
/* }}} */

/**
 * Parse out metadata when phar_metadata_tracker_has_data is true.
 *
 * Precondition: phar_metadata_tracker_has_data is true
 */
zend_result phar_metadata_tracker_unserialize_or_copy(phar_metadata_tracker *tracker, zval *metadata, bool persistent, HashTable *unserialize_options, const char* method_name) /* {{{ */
{
	const bool has_unserialize_options = unserialize_options != NULL && zend_hash_num_elements(unserialize_options) > 0;
	/* It should be impossible to create a zval in a persistent phar/entry. */
	ZEND_ASSERT(!persistent || Z_ISUNDEF(tracker->val));
	ZEND_ASSERT(!EG(exception));

	if (Z_ISUNDEF(tracker->val) || has_unserialize_options) {
		/* Persistent phars should always be unserialized. */
		const char *start;
		/* Assert it should not be possible to create raw data in a persistent phar (i.e. from cache_list) */

		/* Precondition: This has serialized data, either from setMetadata or the phar file. */
		ZEND_ASSERT(tracker->str != NULL);
		ZVAL_NULL(metadata);
		start = ZSTR_VAL(tracker->str);

		php_unserialize_with_options(metadata, start, ZSTR_LEN(tracker->str), unserialize_options, method_name);
		if (EG(exception)) {
			zval_ptr_dtor(metadata);
			ZVAL_UNDEF(metadata);
			return FAILURE;
		}
		return SUCCESS;
	} else {
		/* TODO: what is the current/expected behavior when fetching an object set with setMetadata then getting it
		 * with getMetadata() and modifying a property? Previously, it was underdefined, and probably unimportant to support. */
		ZVAL_COPY(metadata, &tracker->val);
	}

	return SUCCESS;
}
/* }}}*/

/**
 * Check if this has any data, serialized or as a raw value.
 */
bool phar_metadata_tracker_has_data(const phar_metadata_tracker *tracker, bool persistent) /* {{{ */
{
	ZEND_ASSERT(!persistent || Z_ISUNDEF(tracker->val));
	return !Z_ISUNDEF(tracker->val) || tracker->str != NULL;
}
/* }}} */

/**
 * Free memory used to track the metadata and set all fields to be null/undef.
 */
void phar_metadata_tracker_free(phar_metadata_tracker *tracker, bool persistent) /* {{{ */
{
	/* Free the string before the zval in case the zval's destructor modifies the metadata */
	if (tracker->str) {
		zend_string_release(tracker->str);
		tracker->str = NULL;
	}
	if (!Z_ISUNDEF(tracker->val)) {
		/* Here, copy the original zval to a different pointer without incrementing the refcount in case something uses the original while it's being freed. */
		zval zval_copy;

		ZEND_ASSERT(!persistent);
		ZVAL_COPY_VALUE(&zval_copy, &tracker->val);
		ZVAL_UNDEF(&tracker->val);
		zval_ptr_dtor(&zval_copy);
	}
}
/* }}} */

/**
 * Free memory used to track the metadata and set all fields to be null/undef.
 */
void phar_metadata_tracker_copy(phar_metadata_tracker *dest, const phar_metadata_tracker *source, bool persistent) /* {{{ */
{
	ZEND_ASSERT(dest != source);
	phar_metadata_tracker_free(dest, persistent);

	if (!Z_ISUNDEF(source->val)) {
		ZEND_ASSERT(!persistent);
		ZVAL_COPY(&dest->val, &source->val);
	}
	if (source->str) {
		dest->str = zend_string_copy(source->str);
	}
}
/* }}} */

/**
 * Copy constructor for a non-persistent clone.
 */
void phar_metadata_tracker_clone(phar_metadata_tracker *tracker) /* {{{ */
{
	Z_TRY_ADDREF_P(&tracker->val);
	if (tracker->str) {
		/* Duplicate the string, as the original may have been persistent. */
		tracker->str = zend_string_dup(tracker->str, false);
	}
}
/* }}} */

/**
 * Parse out metadata from the manifest for a single file, saving it into a string.
 *
 * Meta-data is in this format:
 * [len32][data...]
 *
 * data is the serialized zval
 */
void phar_parse_metadata_lazy(const char *buffer, phar_metadata_tracker *tracker, uint32_t zip_metadata_len, bool persistent) /* {{{ */
{
	phar_metadata_tracker_free(tracker, persistent);
	if (zip_metadata_len) {
		/* lazy init metadata */
		tracker->str = zend_string_init(buffer, zip_metadata_len, persistent);
	}
}
/* }}}*/

/**
 * Size of fixed fields in the manifest.
 * See: https://www.php.net/manual/en/phar.fileformat.phar.php
 */
#define MANIFEST_FIXED_LEN	18

#define SAFE_PHAR_GET_32(buffer, endbuffer, var) \
	if (UNEXPECTED(buffer + 4 > endbuffer)) { \
		MAPPHAR_FAIL("internal corruption of phar \"%s\" (truncated manifest header)"); \
	} \
	PHAR_GET_32(buffer, var);

/**
 * Does not check for a previously opened phar in the cache.
 *
 * Parse a new one and add it to the cache, returning either SUCCESS or
 * FAILURE, and setting pphar to the pointer to the manifest entry
 *
 * This is used by phar_open_from_filename to process the manifest, but can be called
 * directly.
 */
static zend_result phar_parse_pharfile(php_stream *fp, const char *fname, size_t fname_len, const char *alias, size_t alias_len, zend_long halt_offset, phar_archive_data** pphar, uint32_t compression, char **error) /* {{{ */
{
	char b32[4], *buffer, *endbuffer, *savebuf;
	phar_archive_data *mydata = NULL;
	phar_entry_info entry;
	uint32_t manifest_len, manifest_count, manifest_flags, manifest_index, tmp_len, sig_flags;
	uint16_t manifest_ver;
	uint32_t len;
	zend_long offset;
	size_t sig_len;
	bool register_alias = false, temp_alias = false;
	char *signature = NULL;
	zend_string *str;

	if (pphar) {
		*pphar = NULL;
	}

	if (error) {
		*error = NULL;
	}

	/* check for ?>\n and increment accordingly */
	if (-1 == php_stream_seek(fp, halt_offset, SEEK_SET)) {
		MAPPHAR_ALLOC_FAIL("cannot seek to __HALT_COMPILER(); location in phar \"%s\"")
	}

	buffer = b32;

	if (3 != php_stream_read(fp, buffer, 3)) {
		MAPPHAR_ALLOC_FAIL("internal corruption of phar \"%s\" (truncated manifest at stub end)")
	}

	if ((*buffer == ' ' || *buffer == '\n') && *(buffer + 1) == '?' && *(buffer + 2) == '>') {
		int nextchar;
		halt_offset += 3;
		if (EOF == (nextchar = php_stream_getc(fp))) {
			MAPPHAR_ALLOC_FAIL("internal corruption of phar \"%s\" (truncated manifest at stub end)")
		}

		if ((char) nextchar == '\r') {
			/* if we have an \r we require an \n as well */
			if (EOF == (nextchar = php_stream_getc(fp)) || (char)nextchar != '\n') {
				MAPPHAR_ALLOC_FAIL("internal corruption of phar \"%s\" (truncated manifest at stub end)")
			}
			++halt_offset;
		}

		if ((char) nextchar == '\n') {
			++halt_offset;
		}
	}

	/* make sure we are at the right location to read the manifest */
	if (-1 == php_stream_seek(fp, halt_offset, SEEK_SET)) {
		MAPPHAR_ALLOC_FAIL("cannot seek to __HALT_COMPILER(); location in phar \"%s\"")
	}

	/* read in manifest */
	buffer = b32;

	if (4 != php_stream_read(fp, buffer, 4)) {
		MAPPHAR_ALLOC_FAIL("internal corruption of phar \"%s\" (truncated manifest at manifest length)")
	}

	PHAR_GET_32(buffer, manifest_len);

	if (manifest_len > 1048576 * 100) {
		/* prevent serious memory issues by limiting manifest to at most 100 MB in length */
		MAPPHAR_ALLOC_FAIL("manifest cannot be larger than 100 MB in phar \"%s\"")
	}

	buffer = (char *)emalloc(manifest_len);
	savebuf = buffer;
	endbuffer = buffer + manifest_len;

	if (manifest_len < MANIFEST_FIXED_LEN || manifest_len != php_stream_read(fp, buffer, manifest_len)) {
		MAPPHAR_FAIL("internal corruption of phar \"%s\" (truncated manifest header)")
	}

	/* extract the number of entries */
	SAFE_PHAR_GET_32(buffer, endbuffer, manifest_count);

	if (manifest_count == 0) {
		MAPPHAR_FAIL("in phar \"%s\", manifest claims to have zero entries.  Phars must have at least 1 entry");
	}

	/* extract API version, lowest nibble currently unused */
	manifest_ver = (((unsigned char)buffer[0]) << 8)
				 + ((unsigned char)buffer[1]);
	buffer += 2;

	if ((manifest_ver & PHAR_API_VER_MASK) < PHAR_API_MIN_READ) {
		efree(savebuf);
		php_stream_close(fp);
		if (error) {
			spprintf(error, 0, "phar \"%s\" is API version %1.u.%1.u.%1.u, and cannot be processed", fname, manifest_ver >> 12, (manifest_ver >> 8) & 0xF, (manifest_ver >> 4) & 0x0F);
		}
		return FAILURE;
	}

	SAFE_PHAR_GET_32(buffer, endbuffer, manifest_flags);

	manifest_flags &= ~PHAR_FILE_COMPRESSION_MASK;
	/* remember whether this entire phar was compressed with gz/bzip2 */
	manifest_flags |= compression;

	/* The lowest nibble contains the phar wide flags. The compression flags can */
	/* be ignored on reading because it is being generated anyways. */
	if (manifest_flags & PHAR_HDR_SIGNATURE) {
		char sig_buf[8], *sig_ptr = sig_buf;
		zend_off_t read_len;
		size_t end_of_phar;

		if (-1 == php_stream_seek(fp, -8, SEEK_END)
		|| (read_len = php_stream_tell(fp)) < 20
		|| 8 != php_stream_read(fp, sig_buf, 8)
		|| memcmp(sig_buf+4, "GBMB", 4)) {
			efree(savebuf);
			php_stream_close(fp);
			if (error) {
				spprintf(error, 0, "phar \"%s\" has a broken signature", fname);
			}
			return FAILURE;
		}

		PHAR_GET_32(sig_ptr, sig_flags);

		switch(sig_flags) {
			case PHAR_SIG_OPENSSL_SHA512:
			case PHAR_SIG_OPENSSL_SHA256:
			case PHAR_SIG_OPENSSL: {
				uint32_t signature_len;
				char *sig;
				zend_off_t whence;

				/* we store the signature followed by the signature length */
				if (-1 == php_stream_seek(fp, -12, SEEK_CUR)
				|| 4 != php_stream_read(fp, sig_buf, 4)) {
					efree(savebuf);
					php_stream_close(fp);
					if (error) {
						spprintf(error, 0, "phar \"%s\" openssl signature length could not be read", fname);
					}
					return FAILURE;
				}

				sig_ptr = sig_buf;
				PHAR_GET_32(sig_ptr, signature_len);
				sig = (char *) emalloc(signature_len);
				whence = signature_len + 4;
				whence = -whence;

				if (-1 == php_stream_seek(fp, whence, SEEK_CUR)
				|| !(end_of_phar = php_stream_tell(fp))
				|| signature_len != php_stream_read(fp, sig, signature_len)) {
					efree(savebuf);
					efree(sig);
					php_stream_close(fp);
					if (error) {
						spprintf(error, 0, "phar \"%s\" openssl signature could not be read", fname);
					}
					return FAILURE;
				}

				if (FAILURE == phar_verify_signature(fp, end_of_phar, sig_flags, sig, signature_len, fname, &signature, &sig_len, error)) {
					efree(savebuf);
					efree(sig);
					php_stream_close(fp);
					if (error) {
						char *save = *error;
						spprintf(error, 0, "phar \"%s\" openssl signature could not be verified: %s", fname, *error);
						efree(save);
					}
					return FAILURE;
				}
				efree(sig);
			}
			break;
			case PHAR_SIG_SHA512: {
				unsigned char digest[64];

				php_stream_seek(fp, -(8 + 64), SEEK_END);
				read_len = php_stream_tell(fp);

				if (php_stream_read(fp, (char*)digest, sizeof(digest)) != sizeof(digest)) {
					efree(savebuf);
					php_stream_close(fp);
					if (error) {
						spprintf(error, 0, "phar \"%s\" has a broken signature", fname);
					}
					return FAILURE;
				}

				if (FAILURE == phar_verify_signature(fp, read_len, PHAR_SIG_SHA512, (char *)digest, 64, fname, &signature, &sig_len, error)) {
					efree(savebuf);
					php_stream_close(fp);
					if (error) {
						char *save = *error;
						spprintf(error, 0, "phar \"%s\" SHA512 signature could not be verified: %s", fname, *error);
						efree(save);
					}
					return FAILURE;
				}
				break;
			}
			case PHAR_SIG_SHA256: {
				unsigned char digest[32];

				php_stream_seek(fp, -(8 + 32), SEEK_END);
				read_len = php_stream_tell(fp);

				if (php_stream_read(fp, (char*)digest, sizeof(digest)) != sizeof(digest)) {
					efree(savebuf);
					php_stream_close(fp);
					if (error) {
						spprintf(error, 0, "phar \"%s\" has a broken signature", fname);
					}
					return FAILURE;
				}

				if (FAILURE == phar_verify_signature(fp, read_len, PHAR_SIG_SHA256, (char *)digest, 32, fname, &signature, &sig_len, error)) {
					efree(savebuf);
					php_stream_close(fp);
					if (error) {
						char *save = *error;
						spprintf(error, 0, "phar \"%s\" SHA256 signature could not be verified: %s", fname, *error);
						efree(save);
					}
					return FAILURE;
				}
				break;
			}
			case PHAR_SIG_SHA1: {
				unsigned char digest[20];

				php_stream_seek(fp, -(8 + 20), SEEK_END);
				read_len = php_stream_tell(fp);

				if (php_stream_read(fp, (char*)digest, sizeof(digest)) != sizeof(digest)) {
					efree(savebuf);
					php_stream_close(fp);
					if (error) {
						spprintf(error, 0, "phar \"%s\" has a broken signature", fname);
					}
					return FAILURE;
				}

				if (FAILURE == phar_verify_signature(fp, read_len, PHAR_SIG_SHA1, (char *)digest, 20, fname, &signature, &sig_len, error)) {
					efree(savebuf);
					php_stream_close(fp);
					if (error) {
						char *save = *error;
						spprintf(error, 0, "phar \"%s\" SHA1 signature could not be verified: %s", fname, *error);
						efree(save);
					}
					return FAILURE;
				}
				break;
			}
			case PHAR_SIG_MD5: {
				unsigned char digest[16];

				php_stream_seek(fp, -(8 + 16), SEEK_END);
				read_len = php_stream_tell(fp);

				if (php_stream_read(fp, (char*)digest, sizeof(digest)) != sizeof(digest)) {
					efree(savebuf);
					php_stream_close(fp);
					if (error) {
						spprintf(error, 0, "phar \"%s\" has a broken signature", fname);
					}
					return FAILURE;
				}

				if (FAILURE == phar_verify_signature(fp, read_len, PHAR_SIG_MD5, (char *)digest, 16, fname, &signature, &sig_len, error)) {
					efree(savebuf);
					php_stream_close(fp);
					if (error) {
						char *save = *error;
						spprintf(error, 0, "phar \"%s\" MD5 signature could not be verified: %s", fname, *error);
						efree(save);
					}
					return FAILURE;
				}
				break;
			}
			default:
				efree(savebuf);
				php_stream_close(fp);

				if (error) {
					spprintf(error, 0, "phar \"%s\" has a broken or unsupported signature", fname);
				}
				return FAILURE;
		}
	} else if (PHAR_G(require_hash)) {
		efree(savebuf);
		php_stream_close(fp);

		if (error) {
			spprintf(error, 0, "phar \"%s\" does not have a signature", fname);
		}
		return FAILURE;
	} else {
		sig_flags = 0;
		sig_len = 0;
	}

	/* extract alias */
	SAFE_PHAR_GET_32(buffer, endbuffer, tmp_len);

	if (buffer + tmp_len > endbuffer) {
		MAPPHAR_FAIL("internal corruption of phar \"%s\" (buffer overrun)");
	}

	if (manifest_len < MANIFEST_FIXED_LEN + tmp_len) {
		MAPPHAR_FAIL("internal corruption of phar \"%s\" (truncated manifest header)")
	}

	/* tmp_len = 0 says alias length is 0, which means the alias is not stored in the phar */
	if (tmp_len) {
		/* if the alias is stored we enforce it (implicit overrides explicit) */
		if (alias && alias_len && (alias_len != tmp_len || strncmp(alias, buffer, tmp_len)))
		{
			php_stream_close(fp);

			if (signature) {
				efree(signature);
			}

			if (error) {
				spprintf(error, 0, "cannot load phar \"%s\" with implicit alias \"%.*s\" under different alias \"%s\"", fname, tmp_len, buffer, alias);
			}

			efree(savebuf);
			return FAILURE;
		}

		alias_len = tmp_len;
		alias = buffer;
		buffer += tmp_len;
		register_alias = true;
	} else if (!alias_len || !alias) {
		/* if we neither have an explicit nor an implicit alias, we use the filename */
		alias = NULL;
		alias_len = 0;
		register_alias = false;
	} else if (alias_len) {
		register_alias = true;
		temp_alias = true;
	}

	/* we have 5 32-bit items plus 1 byte at least */
	if (manifest_count > ((manifest_len - MANIFEST_FIXED_LEN - tmp_len) / (5 * 4 + 1))) {
		/* prevent serious memory issues */
		MAPPHAR_FAIL("internal corruption of phar \"%s\" (too many manifest entries for size of manifest)")
	}

	mydata = pecalloc(1, sizeof(phar_archive_data), PHAR_G(persist));
	mydata->is_persistent = PHAR_G(persist);
	HT_INVALIDATE(&mydata->manifest);
	HT_INVALIDATE(&mydata->mounted_dirs);
	HT_INVALIDATE(&mydata->virtual_dirs);

	/* check whether we have meta data, zero check works regardless of byte order */
	SAFE_PHAR_GET_32(buffer, endbuffer, len);
	if (mydata->is_persistent) {
		if (!len) {
			/* FIXME: not sure why this is needed but removing it breaks tests */
			SAFE_PHAR_GET_32(buffer, endbuffer, len);
		}
	}
	if(len > (size_t)(endbuffer - buffer)) {
		MAPPHAR_FAIL("internal corruption of phar \"%s\" (trying to read past buffer end)");
	}
	/* Don't implicitly call unserialize() on potentially untrusted input unless getMetadata() is called directly. */
	phar_parse_metadata_lazy(buffer, &mydata->metadata_tracker, len, mydata->is_persistent);
	buffer += len;

	/* set up our manifest */
	zend_hash_init(&mydata->manifest, manifest_count,
		zend_get_hash_value, destroy_phar_manifest_entry, mydata->is_persistent);
	zend_hash_init(&mydata->mounted_dirs, 5,
		zend_get_hash_value, NULL, mydata->is_persistent);
	zend_hash_init(&mydata->virtual_dirs, manifest_count * 2,
		zend_get_hash_value, NULL, mydata->is_persistent);
	mydata->fname = zend_string_init(fname, fname_len, mydata->is_persistent);
	if (mydata->is_persistent) {
		GC_MAKE_PERSISTENT_LOCAL(mydata->fname);
	}
#ifdef PHP_WIN32
	phar_unixify_path_separators(ZSTR_VAL(mydata->fname), ZSTR_LEN(mydata->fname));
#endif
	offset = halt_offset + manifest_len + 4;
	memset(&entry, 0, sizeof(phar_entry_info));
	entry.phar = mydata;
	entry.fp_type = PHAR_FP;
	entry.is_persistent = mydata->is_persistent;

	for (manifest_index = 0; manifest_index < manifest_count; ++manifest_index) {
		if (buffer + 28 > endbuffer) {
			MAPPHAR_FAIL("internal corruption of phar \"%s\" (truncated manifest entry)")
		}

		uint32_t filename_len;
		PHAR_GET_32(buffer, filename_len);

		if (filename_len == 0) {
			MAPPHAR_FAIL("zero-length filename encountered in phar \"%s\"");
		}

		if (entry.is_persistent) {
			entry.manifest_pos = manifest_index;
		}

		if (filename_len > (size_t)(endbuffer - buffer - 24)) {
			MAPPHAR_FAIL("internal corruption of phar \"%s\" (truncated manifest entry)");
		}

		if ((manifest_ver & PHAR_API_VER_MASK) >= PHAR_API_MIN_DIR && buffer[filename_len - 1] == '/') {
			entry.is_dir = 1;
		} else {
			entry.is_dir = 0;
		}

		phar_add_virtual_dirs(mydata, buffer, filename_len);
		const char *filename_raw = buffer;
		buffer += filename_len;
		PHAR_GET_32(buffer, entry.uncompressed_filesize);
		PHAR_GET_32(buffer, entry.timestamp);

		if (offset == halt_offset + manifest_len + 4
		 || mydata->max_timestamp < entry.timestamp) {
			mydata->max_timestamp = entry.timestamp;
		}

		PHAR_GET_32(buffer, entry.compressed_filesize);
		PHAR_GET_32(buffer, entry.crc32);
		PHAR_GET_32(buffer, entry.flags);

		if (entry.is_dir) {
			filename_len--;
			entry.flags |= PHAR_ENT_PERM_DEF_DIR;
		}

		entry.filename = zend_string_init(filename_raw, filename_len, entry.is_persistent);
		if (entry.is_persistent) {
			GC_MAKE_PERSISTENT_LOCAL(entry.filename);
		}

		PHAR_GET_32(buffer, len);
		if (len > (size_t)(endbuffer - buffer)) {
			zend_string_free(entry.filename);
			MAPPHAR_FAIL("internal corruption of phar \"%s\" (truncated manifest entry)");
		}
		/* Don't implicitly call unserialize() on potentially untrusted input unless getMetadata() is called directly. */
		/* The same local variable entry is reused in a loop, so reset the state before reading data. */
		ZVAL_UNDEF(&entry.metadata_tracker.val);
		entry.metadata_tracker.str = NULL;
		phar_parse_metadata_lazy(buffer, &entry.metadata_tracker, len, entry.is_persistent);
		buffer += len;

		entry.offset = entry.offset_abs = offset;
		offset += entry.compressed_filesize;

		switch (entry.flags & PHAR_ENT_COMPRESSION_MASK) {
			case PHAR_ENT_COMPRESSED_GZ:
				if (!PHAR_G(has_zlib)) {
					phar_metadata_tracker_free(&entry.metadata_tracker, entry.is_persistent);
					zend_string_free(entry.filename);
					MAPPHAR_FAIL("zlib extension is required for gz compressed .phar file \"%s\"");
				}
				break;
			case PHAR_ENT_COMPRESSED_BZ2:
				if (!PHAR_G(has_bz2)) {
					phar_metadata_tracker_free(&entry.metadata_tracker, entry.is_persistent);
					zend_string_free(entry.filename);
					MAPPHAR_FAIL("bz2 extension is required for bzip2 compressed .phar file \"%s\"");
				}
				break;
			default:
				if (entry.uncompressed_filesize != entry.compressed_filesize) {
					phar_metadata_tracker_free(&entry.metadata_tracker, entry.is_persistent);
					zend_string_free(entry.filename);
					MAPPHAR_FAIL("internal corruption of phar \"%s\" (compressed and uncompressed size does not match for uncompressed entry)");
				}
				break;
		}

		manifest_flags |= (entry.flags & PHAR_ENT_COMPRESSION_MASK);
		/* if signature matched, no need to check CRC32 for each file */
		entry.is_crc_checked = (manifest_flags & PHAR_HDR_SIGNATURE ? 1 : 0);
		phar_set_inode(&entry);
		if (mydata->is_persistent) {
			str = zend_string_init_interned(ZSTR_VAL(entry.filename), ZSTR_LEN(entry.filename), 1);
		} else {
			str = entry.filename;
		}
		zend_hash_add_mem(&mydata->manifest, str, (void*)&entry, sizeof(phar_entry_info));
		if (mydata->is_persistent) {
			zend_string_release(str);
		}
	}

	snprintf(mydata->version, sizeof(mydata->version), "%u.%u.%u", manifest_ver >> 12, (manifest_ver >> 8) & 0xF, (manifest_ver >> 4) & 0xF);
	mydata->halt_offset = halt_offset;
	mydata->flags = manifest_flags;
	endbuffer = strrchr(ZSTR_VAL(mydata->fname), '/');

	if (endbuffer) {
		mydata->ext = memchr(endbuffer, '.', (ZSTR_VAL(mydata->fname) + ZSTR_LEN(mydata->fname)) - endbuffer);
		if (mydata->ext == endbuffer) {
			mydata->ext = memchr(endbuffer + 1, '.', (ZSTR_VAL(mydata->fname) + ZSTR_LEN(mydata->fname)) - endbuffer - 1);
		}
		if (mydata->ext) {
			mydata->ext_len = (ZSTR_VAL(mydata->fname) + ZSTR_LEN(mydata->fname)) - mydata->ext;
		}
	}

	mydata->alias = alias ?
		pestrndup(alias, alias_len, mydata->is_persistent) :
		pestrndup(ZSTR_VAL(mydata->fname), ZSTR_LEN(mydata->fname), mydata->is_persistent);
	mydata->alias_len = alias ? alias_len : ZSTR_LEN(mydata->fname);
	mydata->sig_flags = sig_flags;
	mydata->fp = fp;
	mydata->sig_len = sig_len;
	mydata->signature = signature;
	phar_request_initialize();

	if (register_alias) {
		phar_archive_data *fd_ptr;

		mydata->is_temporary_alias = temp_alias;

		if (!phar_validate_alias(mydata->alias, mydata->alias_len)) {
			signature = NULL;
			fp = NULL;
			MAPPHAR_FAIL("Cannot open archive \"%s\", invalid alias");
		}

		fd_ptr = zend_hash_str_find_ptr(&(PHAR_G(phar_alias_map)), alias, alias_len);
		if (fd_ptr) {
			if (SUCCESS != phar_free_alias(fd_ptr)) {
				signature = NULL;
				fp = NULL;
				MAPPHAR_FAIL("Cannot open archive \"%s\", alias is already in use by existing archive");
			}
		}

		if (mydata->is_persistent) {
			str = zend_string_init_interned(alias, alias_len, 1);
		} else {
			str = zend_string_init(alias, alias_len, 0);
		}
		zend_hash_add_ptr(&(PHAR_G(phar_alias_map)), str, mydata);
		zend_string_release(str);
	} else {
		mydata->is_temporary_alias = 1;
	}

	if (mydata->is_persistent) {
		str = zend_string_init_interned(ZSTR_VAL(mydata->fname), ZSTR_LEN(mydata->fname), true);
		zend_hash_add_ptr(&(PHAR_G(phar_fname_map)), str, mydata);
		zend_string_release(str);
	} else {
		zend_hash_add_ptr(&(PHAR_G(phar_fname_map)), mydata->fname, mydata);
	}
	efree(savebuf);

	if (pphar) {
		*pphar = mydata;
	}

	return SUCCESS;
}
/* }}} */

/**
 * Create or open a phar for writing
 */
ZEND_ATTRIBUTE_NONNULL_ARGS(1, 5, 6) zend_result phar_open_or_create_filename(
	zend_string *fname,
	/* copyable & hash update */ zend_string *alias,
	bool is_data,
	uint32_t options,
	phar_archive_data** pphar,
	char **error
) {
	const char *ext_str, *z;
	char *my_error;
	size_t ext_len;
	phar_archive_data **test, *unused = NULL;

	test = &unused;

	*error = NULL;

	/* first try to open an existing file */
	if (phar_detect_phar_fname_ext(ZSTR_VAL(fname), ZSTR_LEN(fname), &ext_str, &ext_len, !is_data, 0, true) == SUCCESS) {
		goto check_file;
	}

	/* next try to create a new file */
	if (FAILURE == phar_detect_phar_fname_ext(ZSTR_VAL(fname), ZSTR_LEN(fname), &ext_str, &ext_len, !is_data, 1, true)) {
		if (ext_len == -2) {
			spprintf(error, 0, "Cannot create a phar archive from a URL like \"%s\". Phar objects can only be created from local files", ZSTR_VAL(fname));
		} else {
			spprintf(error, 0, "Cannot create phar '%s', file extension (or combination) not recognised or the directory does not exist", ZSTR_VAL(fname));
		}
		return FAILURE;
	}
check_file:
	if (phar_open_parsed_phar(ZSTR_VAL(fname), ZSTR_LEN(fname), alias, is_data, options, test, &my_error) == SUCCESS) {
		*pphar = *test;

		if ((*test)->is_data && !(*test)->is_tar && !(*test)->is_zip) {
			spprintf(error, 0, "Cannot open '%s' as a PharData object. Use Phar::__construct() for executable archives", ZSTR_VAL(fname));
			return FAILURE;
		}

		if (PHAR_G(readonly) && !(*test)->is_data && ((*test)->is_tar || (*test)->is_zip)) {
			if (!zend_hash_str_exists(&((*test)->manifest), ZEND_STRL(".phar/stub.php"))) {
				spprintf(error, 0, "'%s' is not a phar archive. Use PharData::__construct() for a standard zip or tar archive", ZSTR_VAL(fname));
				return FAILURE;
			}
		}

		if (!PHAR_G(readonly) || (*test)->is_data) {
			(*test)->is_writeable = 1;
		}
		return SUCCESS;
	} else if (my_error) {
		*error = my_error;
		return FAILURE;
	}

	if (ext_len > 3 && (z = memchr(ext_str, 'z', ext_len)) && ((ext_str + ext_len) - z >= 2) && !memcmp(z + 1, "ip", 2)) {
		/* assume zip-based phar */
		return phar_open_or_create_zip(fname, alias, is_data, options, pphar, error);
	}

	if (ext_len > 3 && (z = memchr(ext_str, 't', ext_len)) && ((ext_str + ext_len) - z >= 2) && !memcmp(z + 1, "ar", 2)) {
		/* assume tar-based phar */
		return phar_open_or_create_tar(fname, alias, is_data, options, pphar, error);
	}

	return phar_create_or_parse_filename(fname, alias, is_data, options, pphar, error);
}

static zend_result phar_open_from_fp(php_stream* fp, const char *fname, size_t fname_len, /* copyable & hash update */ zend_string *alias, uint32_t options, phar_archive_data** pphar, char **error);

ZEND_ATTRIBUTE_NONNULL_ARGS(1, 5, 6) zend_result phar_create_or_parse_filename(
	zend_string *fname,
	/* copyable & hash update */ zend_string *alias,
	bool is_data,
	uint32_t options,
	phar_archive_data** pphar,
	char **error
) {
	php_stream *fp;
	zend_string *actual = NULL;
	zend_string *save_fname = fname;
	char *p;

	if (php_check_open_basedir(ZSTR_VAL(fname))) {
		return FAILURE;
	}

	/* first open readonly so it won't be created if not present */
	fp = php_stream_open_wrapper(ZSTR_VAL(fname), "rb", IGNORE_URL|STREAM_MUST_SEEK|0, &actual);

	if (actual) {
		fname = actual;
	}

	if (fp) {
		if (phar_open_from_fp(fp, ZSTR_VAL(fname), ZSTR_LEN(fname), alias, options, pphar, error) == SUCCESS) {
			if ((*pphar)->is_data || !PHAR_G(readonly)) {
				(*pphar)->is_writeable = 1;
			}
			if (actual) {
				zend_string_release_ex(actual, 0);
			}
			return SUCCESS;
		} else {
			/* file exists, but is either corrupt or not a phar archive */
			if (actual) {
				zend_string_release_ex(actual, 0);
			}
			return FAILURE;
		}
	}

	if (actual) {
		zend_string_release_ex(actual, 0);
		fname = save_fname;
	}

	if (PHAR_G(readonly) && !is_data) {
		if (options & REPORT_ERRORS) {
			spprintf(error, 0, "creating archive \"%s\" disabled by the php.ini setting phar.readonly", ZSTR_VAL(fname));
		}
		return FAILURE;
	}

	/* set up our manifest */
	phar_archive_data *mydata = ecalloc(1, sizeof(phar_archive_data));
	/* TODO: prevent reallocation via a new expand_filepath() API? */
	char *new_fname = expand_filepath(ZSTR_VAL(fname), NULL);
	if (new_fname == NULL) {
		efree(mydata);
		return FAILURE;
	}
	size_t new_fname_len = strlen(new_fname);
#ifdef PHP_WIN32
	phar_unixify_path_separators(new_fname, new_fname_len);
#endif
	p = strrchr(new_fname, '/');

	if (p) {
		mydata->ext = memchr(p, '.', (new_fname + new_fname_len) - p);
		if (mydata->ext == p) {
			mydata->ext = memchr(p + 1, '.', (new_fname + new_fname_len) - p - 1);
		}
		if (mydata->ext) {
			mydata->ext_len = (new_fname + new_fname_len) - mydata->ext;
		}
	}

	zend_hash_init(&mydata->manifest, sizeof(phar_entry_info),
		zend_get_hash_value, destroy_phar_manifest_entry, 0);
	zend_hash_init(&mydata->mounted_dirs, sizeof(char *),
		zend_get_hash_value, NULL, 0);
	zend_hash_init(&mydata->virtual_dirs, sizeof(char *),
		zend_get_hash_value, NULL, mydata->is_persistent);
	mydata->fname = zend_string_init(new_fname, new_fname_len, false);
	efree(new_fname);
	snprintf(mydata->version, sizeof(mydata->version), "%s", PHP_PHAR_API_VERSION);
	mydata->is_temporary_alias = alias ? 0 : 1;
	mydata->fp = NULL;
	mydata->is_writeable = 1;
	mydata->is_brandnew = 1;
	phar_request_initialize();
	zend_hash_add_ptr(&(PHAR_G(phar_fname_map)), mydata->fname, mydata);

	if (is_data) {
		alias = NULL;
		mydata->is_data = 1;
		/* assume tar format, PharData can specify other */
		mydata->is_tar = 1;
	} else {
		if (alias) {
			const phar_archive_data *fd_ptr = zend_hash_find_ptr(&(PHAR_G(phar_alias_map)), alias);
			if (fd_ptr && SUCCESS != phar_free_alias(fd_ptr)) {
				spprintf(error, 4096, "phar error: phar \"%s\" cannot set alias \"%s\", already in use by another phar archive", ZSTR_VAL(mydata->fname), ZSTR_VAL(alias));

				zend_hash_del(&(PHAR_G(phar_fname_map)), mydata->fname);

				*pphar = NULL;

				return FAILURE;
			}
		}

		ZEND_ASSERT(!mydata->is_persistent);
		mydata->alias = alias ? estrndup(ZSTR_VAL(alias), ZSTR_LEN(alias)) : estrndup(ZSTR_VAL(mydata->fname), ZSTR_LEN(mydata->fname));
		mydata->alias_len = alias ? ZSTR_LEN(alias) : ZSTR_LEN(mydata->fname);
	}

	if (alias) {
		if (NULL == zend_hash_add_ptr(&(PHAR_G(phar_alias_map)), alias, mydata)) {
			if (options & REPORT_ERRORS) {
				spprintf(error, 0, "archive \"%s\" cannot be associated with alias \"%s\", already in use", ZSTR_VAL(fname), ZSTR_VAL(alias));
			}

			zend_hash_del(&(PHAR_G(phar_fname_map)), mydata->fname);

			*pphar = NULL;

			return FAILURE;
		}
	}

	*pphar = mydata;
	return SUCCESS;
}

/**
 * Return an already opened filename.
 *
 * Or scan a phar file for the required __HALT_COMPILER(); ?> token and verify
 * that the manifest is proper, then pass it to phar_parse_pharfile().  SUCCESS
 * or FAILURE is returned and pphar is set to a pointer to the phar's manifest
 */
zend_result phar_open_from_filename(
	char *fname,
	size_t fname_len,
	/* copyable & hash update */ zend_string *alias,
	uint32_t options,
	phar_archive_data** pphar,
	char **error
) {
	php_stream *fp;
	zend_string *actual;
	bool is_data = false;

	if (error) {
		*error = NULL;
	}

	if (!strstr(fname, ".phar")) {
		is_data = true;
	}

	if (phar_open_parsed_phar(fname, fname_len, alias, is_data, options, pphar, error) == SUCCESS) {
		return SUCCESS;
	} else if (error && *error) {
		return FAILURE;
	}
	if (php_check_open_basedir(fname)) {
		return FAILURE;
	}

	fp = php_stream_open_wrapper(fname, "rb", IGNORE_URL|STREAM_MUST_SEEK, &actual);

	if (!fp) {
		if (options & REPORT_ERRORS) {
			if (error) {
				spprintf(error, 0, "unable to open phar for reading \"%s\"", fname);
			}
		}
		if (actual) {
			zend_string_release_ex(actual, 0);
		}
		return FAILURE;
	}

	if (actual) {
		fname = ZSTR_VAL(actual);
		fname_len = ZSTR_LEN(actual);
	}

	zend_result ret = phar_open_from_fp(fp, fname, fname_len, alias, options, pphar, error);

	if (actual) {
		zend_string_release_ex(actual, 0);
	}

	return ret;
}

/**
 * Scan an open fp for the required __HALT_COMPILER(); ?> token and verify
 * that the manifest is proper, then pass it to phar_parse_pharfile().  SUCCESS
 * or FAILURE is returned and pphar is set to a pointer to the phar's manifest
 */
static zend_result phar_open_from_fp(
	php_stream* fp,
	const char *fname,
	size_t fname_len,
	/* copyable & hash update */ zend_string *alias,
	uint32_t options,
	phar_archive_data** pphar,
	char **error
) {
	static const char token[] = "__HALT_COMPILER();";
	static const char zip_magic[] = "PK\x03\x04";
	static const char gz_magic[] = "\x1f\x8b\x08";
	static const char bz_magic[] = "BZh";
	const char *pos;
	char test = '\0';
	int recursion_count = 3; // arbitrary limit to avoid too deep or even infinite recursion
	const int window_size = 1024;
	char buffer[1024 + sizeof(token)]; /* a 1024 byte window + the size of the halt_compiler token (moving window) */
	const zend_long readsize = sizeof(buffer) - sizeof(token);
	const zend_long tokenlen = sizeof(token) - 1;
	zend_long halt_offset;
	ssize_t got;
	uint32_t compression = PHAR_FILE_COMPRESSED_NONE;

	if (error) {
		*error = NULL;
	}

	if (-1 == php_stream_rewind(fp)) {
		MAPPHAR_ALLOC_FAIL("cannot rewind phar \"%s\"")
	}

	buffer[sizeof(buffer)-1] = '\0';
	memset(buffer, 32, sizeof(token));
	halt_offset = 0;

	/* Maybe it's better to compile the file instead of just searching,  */
	/* but we only want the offset. So we want a .re scanner to find it. */
	while(!php_stream_eof(fp)) {
		if ((got = php_stream_read(fp, buffer+tokenlen, readsize)) < tokenlen) {
			MAPPHAR_ALLOC_FAIL("internal corruption of phar \"%s\" (truncated entry)")
		}

		if (!test && recursion_count) {
			test = '\1';
			pos = buffer+tokenlen;
			if (!memcmp(pos, gz_magic, 3)) {
				char err = 0;
				php_stream_filter *filter;
				php_stream *temp;
				/* to properly decompress, we have to tell zlib to look for a zlib or gzip header */
				zval filterparams;

				if (!PHAR_G(has_zlib)) {
					MAPPHAR_ALLOC_FAIL("unable to decompress gzipped phar archive \"%s\" to temporary file, enable zlib extension in php.ini")
				}

				/* entire file is gzip-compressed, uncompress to temporary file */
				if (!(temp = php_stream_fopen_tmpfile())) {
					MAPPHAR_ALLOC_FAIL("unable to create temporary file for decompression of gzipped phar archive \"%s\"")
				}

				array_init(&filterparams);
/* this is defined in zlib's zconf.h */
#ifndef MAX_WBITS
#define MAX_WBITS 15
#endif
				add_assoc_long_ex(&filterparams, "window", sizeof("window") - 1, MAX_WBITS + 32);

				php_stream_rewind(fp);
				filter = php_stream_filter_create("zlib.inflate", &filterparams, php_stream_is_persistent(fp));

				if (!filter) {
					err = 1;
					add_assoc_long_ex(&filterparams, "window", sizeof("window") - 1, MAX_WBITS);
					filter = php_stream_filter_create("zlib.inflate", &filterparams, php_stream_is_persistent(fp));
					zend_array_destroy(Z_ARR(filterparams));

					if (!filter) {
						php_stream_close(temp);
						MAPPHAR_ALLOC_FAIL("unable to decompress gzipped phar archive \"%s\", ext/zlib is buggy in PHP versions older than 5.2.6")
					}
				} else {
					zend_array_destroy(Z_ARR(filterparams));
				}

				php_stream_filter_append(&temp->writefilters, filter);

				if (SUCCESS != php_stream_copy_to_stream_ex(fp, temp, PHP_STREAM_COPY_ALL, NULL)) {
					php_stream_filter_remove(filter, 1);
					if (err) {
						php_stream_close(temp);
						MAPPHAR_ALLOC_FAIL("unable to decompress gzipped phar archive \"%s\", ext/zlib is buggy in PHP versions older than 5.2.6")
					}
					php_stream_close(temp);
					MAPPHAR_ALLOC_FAIL("unable to decompress gzipped phar archive \"%s\" to temporary file")
				}

				php_stream_filter_flush(filter, 1);
				php_stream_filter_remove(filter, 1);
				php_stream_close(fp);
				fp = temp;
				php_stream_rewind(fp);
				compression = PHAR_FILE_COMPRESSED_GZ;

				/* now, start over */
				test = '\0';
				if (!--recursion_count) {
					MAPPHAR_ALLOC_FAIL("unable to decompress gzipped phar archive \"%s\"");
					break;
				}
				continue;
			} else if (!memcmp(pos, bz_magic, 3)) {
				php_stream_filter *filter;

				if (!PHAR_G(has_bz2)) {
					MAPPHAR_ALLOC_FAIL("unable to decompress bzipped phar archive \"%s\" to temporary file, enable bz2 extension in php.ini")
				}

				/* entire file is bzip-compressed, uncompress to temporary file */
				php_stream *temp = php_stream_fopen_tmpfile();
				if (!temp) {
					MAPPHAR_ALLOC_FAIL("unable to create temporary file for decompression of bzipped phar archive \"%s\"")
				}

				php_stream_rewind(fp);
				filter = php_stream_filter_create("bzip2.decompress", NULL, php_stream_is_persistent(fp));

				if (!filter) {
					php_stream_close(temp);
					MAPPHAR_ALLOC_FAIL("unable to decompress bzipped phar archive \"%s\", filter creation failed")
				}

				php_stream_filter_append(&temp->writefilters, filter);

				if (SUCCESS != php_stream_copy_to_stream_ex(fp, temp, PHP_STREAM_COPY_ALL, NULL)) {
					php_stream_filter_remove(filter, 1);
					php_stream_close(temp);
					MAPPHAR_ALLOC_FAIL("unable to decompress bzipped phar archive \"%s\" to temporary file")
				}

				php_stream_filter_flush(filter, 1);
				php_stream_filter_remove(filter, 1);
				php_stream_close(fp);
				fp = temp;
				php_stream_rewind(fp);
				compression = PHAR_FILE_COMPRESSED_BZ2;

				/* now, start over */
				test = '\0';
				if (!--recursion_count) {
					MAPPHAR_ALLOC_FAIL("unable to decompress bzipped phar archive \"%s\"");
					break;
				}
				continue;
			}

			if (!memcmp(pos, zip_magic, 4)) {
				php_stream_seek(fp, 0, SEEK_END);
				return phar_parse_zipfile(fp, fname, fname_len, alias, pphar, error);
			}

			if (got >= 512) {
				if (phar_is_tar(pos, fname)) {
					php_stream_rewind(fp);
					return phar_parse_tarfile(fp, fname, fname_len, alias, pphar, compression, error);
				}
			}
		}

		if (got > 0 && (pos = php_memnistr(buffer, token, tokenlen, buffer + got + sizeof(token))) != NULL) {
			halt_offset += (pos - buffer); /* no -tokenlen+tokenlen here */
			const char *alias_cstr = alias ? ZSTR_VAL(alias) : NULL;
			size_t alias_len = alias ? ZSTR_LEN(alias) : 0;
			return phar_parse_pharfile(fp, fname, fname_len, alias_cstr, alias_len, halt_offset, pphar, compression, error);
		}

		halt_offset += got;
		memmove(buffer, buffer + window_size, tokenlen); /* move the memory buffer by the size of the window */
	}

	MAPPHAR_ALLOC_FAIL("internal corruption of phar \"%s\" (__HALT_COMPILER(); not found)")
}

/*
 * given the location of the file extension and the start of the file path,
 * determine the end of the portion of the path (i.e. /path/to/file.ext/blah
 * grabs "/path/to/file.ext" as does the straight /path/to/file.ext),
 * stat it to determine if it exists.
 * if so, check to see if it is a directory and fail if so
 * if not, check to see if its dirname() exists (i.e. "/path/to") and is a directory
 * succeed if we are creating the file, otherwise fail.
 */
static zend_result phar_analyze_path(const char *fname, const char *ext, size_t ext_len, int for_create) /* {{{ */
{
	php_stream_statbuf ssb;
	char *filename = estrndup(fname, (ext - fname) + ext_len);

	char *realpath = expand_filepath(filename, NULL);
	if (realpath) {
#ifdef PHP_WIN32
		phar_unixify_path_separators(realpath, strlen(realpath));
#endif
		if (zend_hash_str_exists(&(PHAR_G(phar_fname_map)), realpath, strlen(realpath))) {
			efree(realpath);
			efree(filename);
			return SUCCESS;
		}

		if (PHAR_G(manifest_cached) && zend_hash_str_exists(&cached_phars, realpath, strlen(realpath))) {
			efree(realpath);
			efree(filename);
			return SUCCESS;
		}
		efree(realpath);
	}

	if (SUCCESS == php_stream_stat_path((char *) filename, &ssb)) {

		efree(filename);

		if (ssb.sb.st_mode & S_IFDIR) {
			return FAILURE;
		}

		if (for_create == 1) {
			return FAILURE;
		}

		return SUCCESS;
	} else {
		char *slash;

		if (!for_create) {
			efree(filename);
			return FAILURE;
		}

		slash = (char *) strrchr(filename, '/');

		if (slash) {
			*slash = '\0';
		}

		if (SUCCESS != php_stream_stat_path((char *) filename, &ssb)) {
			if (!slash) {
				realpath = expand_filepath(filename, NULL);
				if (!realpath) {
					efree(filename);
					return FAILURE;
				}
#ifdef PHP_WIN32
				phar_unixify_path_separators(realpath, strlen(realpath));
#endif
				slash = strstr(realpath, filename);
				if (slash) {
					slash += ((ext - fname) + ext_len);
					*slash = '\0';
				}
				slash = strrchr(realpath, '/');

				if (slash) {
					*slash = '\0';
				} else {
					efree(realpath);
					efree(filename);
					return FAILURE;
				}

				if (SUCCESS != php_stream_stat_path(realpath, &ssb)) {
					efree(realpath);
					efree(filename);
					return FAILURE;
				}

				efree(realpath);

				if (ssb.sb.st_mode & S_IFDIR) {
					efree(filename);
					return SUCCESS;
				}
			}

			efree(filename);
			return FAILURE;
		}

		efree(filename);

		if (ssb.sb.st_mode & S_IFDIR) {
			return SUCCESS;
		}

		return FAILURE;
	}
}
/* }}} */

/* check for ".phar" in extension */
static zend_result phar_check_str(const char *fname, const char *ext_str, size_t ext_len, int executable, int for_create) /* {{{ */
{
	const char *pos;

	if (ext_len >= 50) {
		return FAILURE;
	}
	if (executable == 1) {
		/* executable phars must contain ".phar" as a valid extension (phar://.pharmy/oops is invalid) */
		/* (phar://hi/there/.phar/oops is also invalid) */
		pos = strstr(ext_str, ".phar");

		if (!pos
			|| (pos != ext_str && (*(pos - 1) == '/'))
			|| (ext_len - (pos - ext_str)) < 5
			|| !(pos += 5)
			|| !(*pos == '\0' || *pos == '/' || *pos == '.')) {
			return FAILURE;
		}
		return phar_analyze_path(fname, ext_str, ext_len, for_create);
	}

	/* data phars need only contain a single non-"." to be valid */
	if (!executable) {
		pos = strstr(ext_str, ".phar");
		if (!(pos && (*(pos - 1) != '/')
					&& (pos += 5) && (*pos == '\0' || *pos == '/' || *pos == '.')) && *(ext_str + 1) != '.' && *(ext_str + 1) != '/' && *(ext_str + 1) != '\0') {
			return phar_analyze_path(fname, ext_str, ext_len, for_create);
		}
	} else {
		if (*(ext_str + 1) != '.' && *(ext_str + 1) != '/' && *(ext_str + 1) != '\0') {
			return phar_analyze_path(fname, ext_str, ext_len, for_create);
		}
	}

	return FAILURE;
}
/* }}} */

/*
 * if executable is 1, only returns SUCCESS if the extension is one of the tar/zip .phar extensions
 * if executable is 0, it returns SUCCESS only if the filename does *not* contain ".phar" anywhere, and treats
 * the first extension as the filename extension
 *
 * if an extension is found, it sets ext_str to the location of the file extension in filename,
 * and ext_len to the length of the extension.
 * for urls like "phar://alias/oops" it instead sets ext_len to -1 and returns FAILURE, which tells
 * the calling function to use "alias" as the phar alias
 *
 * the last parameter should be set to tell the thing to assume that filename is the full path, and only to check the
 * extension rules, not to iterate.
 */
zend_result phar_detect_phar_fname_ext(const char *filename, size_t filename_len, const char **ext_str, size_t *ext_len, int executable, int for_create, bool is_complete) /* {{{ */
{
	const char *pos, *slash;

	*ext_str = NULL;
	*ext_len = 0;

	if (filename_len <= 1) {
		return FAILURE;
	}

	phar_request_initialize();
	/* first check for alias in first segment */
	pos = memchr(filename, '/', filename_len);

	if (pos && pos != filename) {
		/* check for url like http:// or phar:// */
		if (*(pos - 1) == ':' && (size_t)(pos - filename) < filename_len - 1 && *(pos + 1) == '/') {
			*ext_len = -2;
			*ext_str = NULL;
			return FAILURE;
		}
		if (zend_hash_str_exists(&(PHAR_G(phar_alias_map)), (char *) filename, pos - filename)) {
			*ext_str = pos;
			*ext_len = -1;
			return FAILURE;
		}

		if (PHAR_G(manifest_cached) && zend_hash_str_exists(&cached_alias, (char *) filename, pos - filename)) {
			*ext_str = pos;
			*ext_len = -1;
			return FAILURE;
		}
	}

	if (zend_hash_num_elements(&(PHAR_G(phar_fname_map))) || PHAR_G(manifest_cached)) {
		phar_archive_data *pphar;

		if (is_complete) {
			pphar = zend_hash_str_find_ptr(&(PHAR_G(phar_fname_map)), filename, filename_len);
			if (pphar) {
				*ext_str = filename + (filename_len - pphar->ext_len);
woohoo:
				*ext_len = pphar->ext_len;

				if (executable == 2) {
					return SUCCESS;
				}

				if (executable == 1 && !pphar->is_data) {
					return SUCCESS;
				}

				if (!executable && pphar->is_data) {
					return SUCCESS;
				}

				return FAILURE;
			}

			pphar = PHAR_G(manifest_cached) ? zend_hash_str_find_ptr(&cached_phars, filename, filename_len) : NULL;
			if (pphar) {
				*ext_str = filename + (filename_len - pphar->ext_len);
				goto woohoo;
			}
		} else {
			zend_string *str_key;

			ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&PHAR_G(phar_fname_map), str_key, pphar) {
				if (ZSTR_LEN(str_key) > filename_len) {
					continue;
				}

				if (!memcmp(filename, ZSTR_VAL(str_key), ZSTR_LEN(str_key)) && (filename_len == ZSTR_LEN(str_key)
					|| filename[ZSTR_LEN(str_key)] == '/' || filename[ZSTR_LEN(str_key)] == '\0')) {
					*ext_str = filename + (ZSTR_LEN(str_key) - pphar->ext_len);
					goto woohoo;
				}
			} ZEND_HASH_FOREACH_END();

			if (PHAR_G(manifest_cached)) {
				ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&cached_phars, str_key, pphar) {
					if (ZSTR_LEN(str_key) > filename_len) {
						continue;
					}

					if (!memcmp(filename, ZSTR_VAL(str_key), ZSTR_LEN(str_key)) && (filename_len == ZSTR_LEN(str_key)
						|| filename[ZSTR_LEN(str_key)] == '/' || filename[ZSTR_LEN(str_key)] == '\0')) {
						*ext_str = filename + (ZSTR_LEN(str_key) - pphar->ext_len);
						goto woohoo;
					}
				} ZEND_HASH_FOREACH_END();
			}
		}
	}

	pos = memchr(filename + 1, '.', filename_len);
	while (pos) {
		while (pos != filename && (*(pos - 1) == '/' || *(pos - 1) == '\0')) {
			pos = memchr(pos + 1, '.', filename_len - (pos - filename) - 1);
			if (!pos) {
				return FAILURE;
			}
		}

		slash = memchr(pos, '/', filename_len - (pos - filename));

		if (!slash) {
			/* this is a url like "phar://blah.phar" with no directory */
			*ext_str = pos;
			*ext_len = strlen(pos);

			/* file extension must contain "phar" */
			return phar_check_str(filename, *ext_str, *ext_len, executable, for_create);
		}

		/* we've found an extension that ends at a directory separator */
		*ext_str = pos;
		*ext_len = slash - pos;

		if (phar_check_str(filename, *ext_str, *ext_len, executable, for_create) == SUCCESS) {
			return SUCCESS;
		}

		/* look for more extensions */
		pos = strchr(pos + 1, '.');
		if (pos) {
			*ext_str = NULL;
			*ext_len = 0;
		}
	}

	return FAILURE;
}
/* }}} */

static bool php_check_dots(const char *element, size_t n) /* {{{ */
{
	for(n-- ; n != SIZE_MAX; --n) {
		if (element[n] != '.') {
			return true;
		}
	}
	return false;
}
/* }}} */

#define IS_DIRECTORY_UP(element, len) \
	(len >= 2 && !php_check_dots(element, len))

#define IS_DIRECTORY_CURRENT(element, len) \
	(len == 1 && element[0] == '.')

#define IS_BACKSLASH(c) ((c) == '/')

/**
 * Remove .. and . references within a phar filename
 */
zend_string* phar_fix_filepath(const char *path, size_t path_length, bool use_cwd) /* {{{ */
{
	zend_string *new_path;
	size_t new_path_len;
	size_t ptr_length;

	if (use_cwd && PHAR_G(cwd_len) && path_length > 2 && path[0] == '.' && path[1] == '/') {
		new_path = zend_string_alloc(path_length + PHAR_G(cwd_len) + 1, false);
		new_path_len = PHAR_G(cwd_len);
		memcpy(ZSTR_VAL(new_path), PHAR_G(cwd), PHAR_G(cwd_len));
	} else {
		new_path = zend_string_alloc(path_length + 2, false);
		ZSTR_VAL(new_path)[0] = '/';
		new_path_len = 1;
	}

	const char *ptr = path;

	if (*ptr == '/') {
		++ptr;
	}

	const char *tok = ptr;

	do {
		ptr = memchr(ptr, '/', path_length - (ptr - path));
	} while (ptr && ptr - tok == 0 && *ptr == '/' && ++ptr && ++tok);

	if (!ptr && (path_length - (tok - path))) {
		switch (path_length - (tok - path)) {
			case 1:
				if (*tok == '.') {
					zend_string_release_ex(new_path, false);
					return ZSTR_CHAR('/');
				}
				break;
			case 2:
				if (tok[0] == '.' && tok[1] == '.') {
					zend_string_release_ex(new_path, false);
					return ZSTR_CHAR('/');
				}
		}
		zend_string_release_ex(new_path, false);
		return zend_string_init(path, path_length, false);
	}

	while (ptr) {
		ptr_length = ptr - tok;
last_time:
		if (IS_DIRECTORY_UP(tok, ptr_length)) {
			while (new_path_len > 1 && !IS_BACKSLASH(ZSTR_VAL(new_path)[new_path_len - 1])) {
				new_path_len--;
			}

			if (ZSTR_VAL(new_path)[0] != '/') {
				ZSTR_VAL(new_path)[new_path_len] = '\0';
			} else if (new_path_len > 1) {
				--new_path_len;
			}
		} else if (!IS_DIRECTORY_CURRENT(tok, ptr_length)) {
			if (new_path_len > 1) {
				ZSTR_VAL(new_path)[new_path_len++] = '/';
				memcpy(ZSTR_VAL(new_path) + new_path_len, tok, ptr_length+1);
			} else {
				memcpy(ZSTR_VAL(new_path) + new_path_len, tok, ptr_length+1);
			}

			new_path_len += ptr_length;
		}

		if (ptr == path + path_length) {
			break;
		}

		tok = ++ptr;

		do {
			ptr = memchr(ptr, '/', path_length - (ptr - path));
		} while (ptr && ptr - tok == 0 && *ptr == '/' && ++ptr && ++tok);

		if (!ptr && (path_length - (tok - path))) {
			ptr_length = path_length - (tok - path);
			ptr = path + path_length;
			goto last_time;
		}
	}

	ZSTR_VAL(new_path)[new_path_len] = '\0';
	return zend_string_realloc(new_path, new_path_len, false);
}
/* }}} */

/**
 * Process a phar stream name, ensuring we can handle any of:
 *
 * - whatever.phar
 * - whatever.phar.gz
 * - whatever.phar.bz2
 * - whatever.phar.php
 *
 * Optionally the name might start with 'phar://'
 *
 * This is used by phar_parse_url()
 */
zend_string* phar_split_fname_ex(const char *filename, size_t filename_len, zend_string **entry, int executable, int for_create, const char **error) /* {{{ */
{
	const char *ext_str;
#ifdef PHP_WIN32
	char *save;
#endif
	size_t ext_len;

	if (error) {
		*error = NULL;
	}
	if (zend_char_has_nul_byte(filename, filename_len)) {
		return NULL;
	}

	if (!strncasecmp(filename, "phar://", 7)) {
		filename += 7;
		filename_len -= 7;
	}

	ext_len = 0;
#ifdef PHP_WIN32
	save = (char *)filename;
	if (memchr(filename, '\\', filename_len)) {
		filename = estrndup(filename, filename_len);
		phar_unixify_path_separators((char *)filename, filename_len);
	}
#endif
	if (phar_detect_phar_fname_ext(filename, filename_len, &ext_str, &ext_len, executable, for_create, false) == FAILURE) {
		if (ext_len != -1) {
			if (!ext_str && error) {
				/* no / detected, restore arch for error message */
#ifdef PHP_WIN32
				*error = save;
#else
				*error = filename;
#endif
			}

#ifdef PHP_WIN32
			if (filename != save) {
				efree((char *)filename);
			}
#endif
			return NULL;
		}

		ext_len = 0;
		/* no extension detected - instead we are dealing with an alias */
	}

	size_t arch_len = ext_str - filename + ext_len;
	zend_string *arch = zend_string_init(filename, arch_len, false);

	if (entry) {
		if (ext_str[ext_len]) {
			size_t computed_entry_len = filename_len - arch_len;
			/* We don't need to unixify the path on Windows,
			 * as ext_str is derived from filename that was already unixify */
			*entry = phar_fix_filepath(ext_str+ext_len, computed_entry_len, false);
		} else {
			*entry = ZSTR_CHAR('/');
		}
	}

#ifdef PHP_WIN32
	if (filename != save) {
		efree((char *)filename);
	}
#endif

	return arch;
}
/* }}} */

zend_string* phar_split_fname(const char *filename, size_t filename_len, zend_string **entry, int executable, int for_create) {
	return phar_split_fname_ex(filename, filename_len, entry, executable, for_create, NULL);
}

/**
 * Invoked when a user calls Phar::mapPhar() from within an executing .phar
 * to set up its manifest directly
 */
ZEND_ATTRIBUTE_NONNULL_ARGS(2) zend_result phar_open_executed_filename(/* copyable & hash update */ zend_string *alias, char **error) /* {{{ */
{
	*error = NULL;

	zend_string *fname = zend_get_executed_filename_ex();

	if (!fname) {
		*error = estrdup("cannot initialize a phar outside of PHP execution");
		return FAILURE;
	}

	if (phar_open_parsed_phar(ZSTR_VAL(fname), ZSTR_LEN(fname), alias, false, REPORT_ERRORS, NULL, NULL) == SUCCESS) {
		return SUCCESS;
	}

	if (NULL == zend_get_constant_str("__COMPILER_HALT_OFFSET__", sizeof("__COMPILER_HALT_OFFSET__")-1)) {
		*error = estrdup("__HALT_COMPILER(); must be declared in a phar");
		return FAILURE;
	}

	if (php_check_open_basedir(ZSTR_VAL(fname))) {
		return FAILURE;
	}

	zend_string *actual = NULL;
	php_stream *fp;
	fp = php_stream_open_wrapper(ZSTR_VAL(fname), "rb", IGNORE_URL|STREAM_MUST_SEEK|REPORT_ERRORS, &actual);

	if (!fp) {
		spprintf(error, 0, "unable to open phar for reading \"%s\"", ZSTR_VAL(fname));
		if (actual) {
			zend_string_release_ex(actual, 0);
		}
		return FAILURE;
	}

	if (actual) {
		fname = actual;
	}

	zend_result ret = phar_open_from_fp(fp, ZSTR_VAL(fname), ZSTR_LEN(fname), alias, REPORT_ERRORS, NULL, error);

	if (actual) {
		zend_string_release_ex(actual, 0);
	}

	return ret;
}
/* }}} */

/**
 * Validate the CRC32 of a file opened from within the phar
 */
ZEND_ATTRIBUTE_NONNULL zend_result phar_postprocess_file(phar_entry_data *idata, uint32_t crc32, char **error, int process_zip) /* {{{ */
{
	php_stream *fp = idata->fp;
	phar_entry_info *entry = idata->internal_file;

	*error = NULL;

	if (entry->is_zip && process_zip > 0) {
		/* verify local file header */
		phar_zip_file_header local;
		phar_zip_data_desc desc;
		php_stream *stream = phar_open_archive_fp(idata->phar);

		if (!stream) {
			spprintf(error, 0, "phar error: unable to open zip-based phar archive \"%s\" to verify local file header for file \"%s\"",
				ZSTR_VAL(idata->phar->fname), ZSTR_VAL(entry->filename));
			return FAILURE;
		}
		php_stream_seek(stream, entry->header_offset, SEEK_SET);

		if (sizeof(local) != php_stream_read(stream, (char *) &local, sizeof(local))) {
			spprintf(error, 0, "phar error: internal corruption of zip-based phar \"%s\" (cannot read local file header for file \"%s\")",
				ZSTR_VAL(idata->phar->fname), ZSTR_VAL(entry->filename));
			return FAILURE;
		}

		/* check for data descriptor */
		if (((PHAR_ZIP_16(local.flags)) & 0x8) == 0x8) {
			php_stream_seek(stream,
					entry->header_offset + sizeof(local) +
					PHAR_ZIP_16(local.filename_len) +
					PHAR_ZIP_16(local.extra_len) +
					entry->compressed_filesize, SEEK_SET);
			if (sizeof(desc) != php_stream_read(stream,
							    (char *) &desc, sizeof(desc))) {
				spprintf(error, 0, "phar error: internal corruption of zip-based phar \"%s\" (cannot read local data descriptor for file \"%s\")",
					ZSTR_VAL(idata->phar->fname), ZSTR_VAL(entry->filename));
				return FAILURE;
			}
			if (desc.signature[0] == 'P' && desc.signature[1] == 'K') {
				memcpy(&(local.crc32), &(desc.crc32), 12);
			} else {
				/* old data descriptors have no signature */
				memcpy(&(local.crc32), &desc, 12);
			}
		}
		/* verify local header */
		if (ZSTR_LEN(entry->filename) != PHAR_ZIP_16(local.filename_len) || entry->crc32 != PHAR_ZIP_32(local.crc32) || entry->uncompressed_filesize != PHAR_ZIP_32(local.uncompsize) || entry->compressed_filesize != PHAR_ZIP_32(local.compsize)) {
			spprintf(error, 0, "phar error: internal corruption of zip-based phar \"%s\" (local header of file \"%s\" does not match central directory)",
				ZSTR_VAL(idata->phar->fname), ZSTR_VAL(entry->filename));
			return FAILURE;
		}

		/* construct actual offset to file start - local extra_len can be different from central extra_len */
		entry->offset = entry->offset_abs =
			sizeof(local) + entry->header_offset + PHAR_ZIP_16(local.filename_len) + PHAR_ZIP_16(local.extra_len);

		if (idata->zero && idata->zero != entry->offset_abs) {
			idata->zero = entry->offset_abs;
		}
	}

	if (process_zip == 1) {
		return SUCCESS;
	}

	php_stream_seek(fp, idata->zero, SEEK_SET);

	uint32_t crc = php_crc32_bulk_init();
	zend_result ret = php_crc32_stream_bulk_update(&crc, fp, idata->internal_file->uncompressed_filesize);

	php_stream_seek(fp, idata->zero, SEEK_SET);

	if (SUCCESS == ret && php_crc32_bulk_end(crc) == crc32) {
		entry->is_crc_checked = 1;
		return SUCCESS;
	} else {
		spprintf(error, 0, "phar error: internal corruption of phar \"%s\" (crc32 mismatch on file \"%s\")",
			ZSTR_VAL(idata->phar->fname), ZSTR_VAL(entry->filename));
		return FAILURE;
	}
}
/* }}} */

static inline void phar_set_32(char *buffer, uint32_t var) /* {{{ */
{
#ifdef WORDS_BIGENDIAN
	*((buffer) + 3) = (unsigned char) (((var) >> 24) & 0xFF);
	*((buffer) + 2) = (unsigned char) (((var) >> 16) & 0xFF);
	*((buffer) + 1) = (unsigned char) (((var) >> 8) & 0xFF);
	*((buffer) + 0) = (unsigned char) ((var) & 0xFF);
#else
	 memcpy(buffer, &var, sizeof(var));
#endif
} /* }}} */

static int phar_flush_clean_deleted_apply(zval *zv) /* {{{ */
{
	phar_entry_info *entry = (phar_entry_info *)Z_PTR_P(zv);

	if (entry->is_deleted && phar_entry_can_remove(entry)) {
		return ZEND_HASH_APPLY_REMOVE;
	} else {
		return ZEND_HASH_APPLY_KEEP;
	}
}
/* }}} */

#include "stub.h" /* Generated phar_get_stub() function from makestub.php script */

zend_string *phar_create_default_stub(const zend_string *php_index_str, const zend_string *web_index_str, char **error) /* {{{ */
{
	const char *php_index;
	const char *web_index;
	size_t php_len, web_len;

	if (error) {
		*error = NULL;
	}

	if (!php_index_str) {
		php_index = "index.php";
		php_len = strlen("index.php");
	} else {
		php_index = ZSTR_VAL(php_index_str);
		php_len = ZSTR_LEN(php_index_str);
		if (php_len > 400) {
			/* ridiculous size not allowed for index.php startup filename */
			if (error) {
				spprintf(error, 0, "Illegal filename passed in for stub creation, was %zd characters long, and only 400 or less is allowed", php_len);
			}
			return NULL;
		}
	}

	if (!web_index_str) {
		web_index = "index.php";
		web_len = strlen("index.php");
	} else {
		web_index = ZSTR_VAL(web_index_str);
		web_len = ZSTR_LEN(web_index_str);

		if (web_len > 400) {
			/* ridiculous size not allowed for index.php startup filename */
			if (error) {
				spprintf(error, 0, "Illegal web filename passed in for stub creation, was %zd characters long, and only 400 or less is allowed", web_len);
			}
			return NULL;
		}
	}

	return phar_get_stub(php_index, web_index, php_len+1, web_len+1);
}
/* }}} */

ZEND_ATTRIBUTE_NONNULL int phar_flush(phar_archive_data *phar, char **error) {
	return phar_flush_ex(phar, NULL, false, error);
}

/**
 * Save phar contents to disk
 *
 * if user_stub is NULL the default or existing stub should be used
 */
ZEND_ATTRIBUTE_NONNULL_ARGS(1, 4) int phar_flush_ex(phar_archive_data *phar, zend_string *user_stub, bool is_default_stub, char **error) /* {{{ */
{
	static const char halt_stub[] = "__HALT_COMPILER();";

	phar_entry_info *entry, *newentry;
	size_t halt_offset;
	uint32_t restore_alias_len;
	uint32_t global_flags = 0;
	bool must_close_old_file = false;
	bool has_dirs = false;
	char manifest[18], entry_buffer[24];
	zend_off_t manifest_ftell;
	zend_long offset;
	size_t wrote;
	uint32_t manifest_len, mytime, new_manifest_count;
	uint32_t newcrc32;
	php_stream *file, *oldfile, *newfile;
	php_stream_filter *filter;
	php_serialize_data_t metadata_hash;
	smart_str main_metadata_str = {0};
	bool free_fp = true;
	bool free_ufp = true;
	bool manifest_hack = false;
	php_stream *shared_cfp = NULL;

	if (phar->is_persistent) {
		spprintf(error, 0, "internal error: attempt to flush cached zip-based phar \"%s\"", ZSTR_VAL(phar->fname));
		return EOF;
	}

	*error = NULL;

	if (!zend_hash_num_elements(&phar->manifest) && !user_stub) {
		return EOF;
	}

	zend_hash_clean(&phar->virtual_dirs);

	if (phar->is_zip) {
		return phar_zip_flush(phar, user_stub, is_default_stub, error);
	}

	if (phar->is_tar) {
		return phar_tar_flush(phar, user_stub, is_default_stub, error);
	}

	if (PHAR_G(readonly)) {
		return EOF;
	}

	if (phar->fp && !phar->is_brandnew) {
		oldfile = phar->fp;
		must_close_old_file = false;
		php_stream_rewind(oldfile);
	} else {
		oldfile = php_stream_open_wrapper(ZSTR_VAL(phar->fname), "rb", 0, NULL);
		must_close_old_file = oldfile != NULL;
	}
	newfile = php_stream_fopen_tmpfile();
	if (!newfile) {
		*error = estrdup("unable to create temporary file");
		if (must_close_old_file) {
			php_stream_close(oldfile);
		}
		return EOF;
	}

	if (user_stub) {
		char *pos = php_stristr(ZSTR_VAL(user_stub), halt_stub, ZSTR_LEN(user_stub), strlen(halt_stub));

		if (pos == NULL) {
			if (must_close_old_file) {
				php_stream_close(oldfile);
			}
			php_stream_close(newfile);
			spprintf(error, 0, "illegal stub for phar \"%s\" (__HALT_COMPILER(); is missing)", ZSTR_VAL(phar->fname));
			return EOF;
		}

		size_t len = pos - ZSTR_VAL(user_stub) + strlen(halt_stub);
		const char end_sequence[] = " ?>\r\n";
		size_t end_sequence_len = strlen(end_sequence);

		if (
			len != php_stream_write(newfile, ZSTR_VAL(user_stub), len)
			|| end_sequence_len != php_stream_write(newfile, end_sequence, end_sequence_len)
		) {
			if (must_close_old_file) {
				php_stream_close(oldfile);
			}
			php_stream_close(newfile);
			spprintf(error, 0, "unable to create stub from string in new phar \"%s\"", ZSTR_VAL(phar->fname));
			return EOF;
		}
		phar->halt_offset = len + end_sequence_len;
	} else {
		size_t written;
		zend_string *new_stub = NULL;

		if (!user_stub && phar->halt_offset && oldfile && !phar->is_brandnew) {
			php_stream_copy_to_stream_ex(oldfile, newfile, phar->halt_offset, &written);
		} else {
			/* this is either a brand new phar or a default stub overwrite */
			new_stub = phar_create_default_stub(NULL, NULL, NULL);
			phar->halt_offset = ZSTR_LEN(new_stub);
			written = php_stream_write(newfile, ZSTR_VAL(new_stub), phar->halt_offset);
		}
		if (phar->halt_offset != written) {
			if (must_close_old_file) {
				php_stream_close(oldfile);
			}
			php_stream_close(newfile);
			if (new_stub) {
				spprintf(error, 0, "unable to create stub in new phar \"%s\"", ZSTR_VAL(phar->fname));
			} else {
				spprintf(error, 0, "unable to copy stub of old phar to new phar \"%s\"", ZSTR_VAL(phar->fname));
			}
			if (new_stub) {
				zend_string_free(new_stub);
			}
			return EOF;
		}
		if (new_stub) {
			zend_string_free(new_stub);
		}
	}
	manifest_ftell = php_stream_tell(newfile);
	halt_offset = manifest_ftell;

	/* Check whether we can get rid of some of the deleted entries which are
	 * unused. However some might still be in use so even after this clean-up
	 * we need to skip entries marked is_deleted. */
	zend_hash_apply(&phar->manifest, phar_flush_clean_deleted_apply);

	/* compress as necessary, calculate crcs, serialize meta-data, manifest size, and file sizes */
	main_metadata_str.s = NULL;
	if (phar->metadata_tracker.str) {
		smart_str_append(&main_metadata_str, phar->metadata_tracker.str);
	} else if (!Z_ISUNDEF(phar->metadata_tracker.val)) {
		PHP_VAR_SERIALIZE_INIT(metadata_hash);
		php_var_serialize(&main_metadata_str, &phar->metadata_tracker.val, &metadata_hash);
		PHP_VAR_SERIALIZE_DESTROY(metadata_hash);
	}
	new_manifest_count = 0;
	offset = 0;
	ZEND_HASH_MAP_FOREACH_PTR(&phar->manifest, entry) {
		if (entry->cfp) {
			/* did we forget to get rid of cfp last time? */
			php_stream_close(entry->cfp);
			entry->cfp = 0;
		}
		if (entry->is_deleted || entry->is_mounted) {
			/* remove this from the new phar */
			continue;
		}
		if (entry->fp_refcount) {
			/* open file pointers refer to this fp, do not free the stream */
			switch (entry->fp_type) {
				case PHAR_FP:
					free_fp = false;
					break;
				case PHAR_UFP:
					free_ufp = false;
				default:
					break;
			}
		}
		/* after excluding deleted files, calculate manifest size in bytes and number of entries */
		++new_manifest_count;
		phar_add_virtual_dirs(phar, ZSTR_VAL(entry->filename), ZSTR_LEN(entry->filename));

		if (entry->is_dir) {
			/* we use this to calculate API version, 1.1.1 is used for phars with directories */
			has_dirs = true;
		}
		if (!Z_ISUNDEF(entry->metadata_tracker.val) && !entry->metadata_tracker.str) {
			ZEND_ASSERT(!entry->is_persistent);
			/* Assume serialization will succeed. TODO: Set error and throw if EG(exception) != NULL */
			smart_str buf = {0};
			PHP_VAR_SERIALIZE_INIT(metadata_hash);
			php_var_serialize(&buf, &entry->metadata_tracker.val, &metadata_hash);
			PHP_VAR_SERIALIZE_DESTROY(metadata_hash);
			entry->metadata_tracker.str = buf.s;
		}

		/* 32 bits for filename length, length of filename, manifest + metadata, and add 1 for trailing / if a directory */
		offset += 4 + ZSTR_LEN(entry->filename) + sizeof(entry_buffer) + (entry->metadata_tracker.str ? ZSTR_LEN(entry->metadata_tracker.str) : 0) + (entry->is_dir ? 1 : 0);

		/* compress and rehash as necessary */
		if ((oldfile && !entry->is_modified) || entry->is_dir) {
			if (entry->fp_type == PHAR_UFP) {
				/* reset so we can copy the compressed data over */
				entry->fp_type = PHAR_FP;
			}
			continue;
		}
		if (!phar_get_efp(entry, false)) {
			/* re-open internal file pointer just-in-time */
			newentry = phar_open_jit(phar, entry, error);
			if (!newentry) {
				/* major problem re-opening, so we ignore this file and the error */
				efree(*error);
				*error = NULL;
				continue;
			}
			entry = newentry;
		}
		file = phar_get_efp(entry, false);
		if (-1 == phar_seek_efp(entry, 0, SEEK_SET, 0, true)) {
			if (must_close_old_file) {
				php_stream_close(oldfile);
			}
			php_stream_close(newfile);
			spprintf(error, 0, "unable to seek to start of file \"%s\" while creating new phar \"%s\"", ZSTR_VAL(entry->filename), ZSTR_VAL(phar->fname));
			return EOF;
		}
		newcrc32 = php_crc32_bulk_init();
		php_crc32_stream_bulk_update(&newcrc32, file, entry->uncompressed_filesize);
		entry->crc32 = php_crc32_bulk_end(newcrc32);
		entry->is_crc_checked = 1;
		if (!(entry->flags & PHAR_ENT_COMPRESSION_MASK)) {
			/* not compressed */
			entry->compressed_filesize = entry->uncompressed_filesize;
			continue;
		}
		const char *compression_filter_name = phar_get_compress_filter_name(entry);
		ZEND_ASSERT(compression_filter_name && "Must have as this has a compression flag set");
		filter = php_stream_filter_create(compression_filter_name, NULL, false);
		if (!filter) {
			if (must_close_old_file) {
				php_stream_close(oldfile);
			}
			php_stream_close(newfile);
			spprintf(error, 0, "unable to %s compress file \"%s\" to new phar \"%s\"",
				entry->flags & PHAR_ENT_COMPRESSED_GZ ? "gzip" : "bzip2",
				ZSTR_VAL(entry->filename),
				ZSTR_VAL(phar->fname));
			return EOF;
		}

		/* create new file that holds the compressed versions */
		/* work around inability to specify freedom in write and strictness
		in read count */
		if (shared_cfp == NULL) {
			shared_cfp = php_stream_fopen_tmpfile();
		}
		entry->cfp = shared_cfp;
		if (!entry->cfp) {
			php_stream_filter_free(filter);
			*error = estrdup("unable to create temporary file");
			if (must_close_old_file) {
				php_stream_close(oldfile);
			}
			php_stream_close(newfile);
			goto cleanup;
		}
		/* for real phars, header_offset is unused; we misuse it here to store the offset in the temp file */
		ZEND_ASSERT(entry->header_offset == 0);
		entry->header_offset = php_stream_tell(entry->cfp);
		php_stream_flush(file);
		if (-1 == phar_seek_efp(entry, 0, SEEK_SET, 0, false)) {
			php_stream_filter_free(filter);
			if (must_close_old_file) {
				php_stream_close(oldfile);
			}
			php_stream_close(newfile);
			spprintf(error, 0, "unable to seek to start of file \"%s\" while creating new phar \"%s\"",
				ZSTR_VAL(entry->filename), ZSTR_VAL(phar->fname));
			goto cleanup;
		}
		php_stream_filter_append((&entry->cfp->writefilters), filter);
		if (SUCCESS != php_stream_copy_to_stream_ex(file, entry->cfp, entry->uncompressed_filesize, NULL)) {
			php_stream_filter_remove(filter, 1);
			if (must_close_old_file) {
				php_stream_close(oldfile);
			}
			php_stream_close(newfile);
			spprintf(error, 0, "unable to copy compressed file contents of file \"%s\" while creating new phar \"%s\"",
				ZSTR_VAL(entry->filename), ZSTR_VAL(phar->fname));
			goto cleanup;
		}
		php_stream_filter_flush(filter, 1);
		php_stream_flush(entry->cfp);
		php_stream_filter_remove(filter, 1);
		php_stream_seek(entry->cfp, 0, SEEK_END);
		entry->compressed_filesize = ((uint32_t) php_stream_tell(entry->cfp)) - entry->header_offset;
		/* generate crc on compressed file */
		entry->old_flags = entry->flags;
		entry->is_modified = 1;
		global_flags |= (entry->flags & PHAR_ENT_COMPRESSION_MASK);
	} ZEND_HASH_FOREACH_END();
	global_flags |= PHAR_HDR_SIGNATURE;

	/* write out manifest pre-header */
	/*  4: manifest length
	 *  4: manifest entry count
	 *  2: phar version
	 *  4: phar global flags
	 *  4: alias length
	 *  ?: the alias itself
	 *  4: phar metadata length
	 *  ?: phar metadata
	 */
	restore_alias_len = phar->alias_len;
	if (phar->is_temporary_alias) {
		phar->alias_len = 0;
	}

	manifest_len = offset + phar->alias_len + sizeof(manifest) + (main_metadata_str.s ? ZSTR_LEN(main_metadata_str.s) : 0);
	phar_set_32(manifest, manifest_len);
	/* Hack - see bug #65028, add padding byte to the end of the manifest */
	if(manifest[0] == '\r' || manifest[0] == '\n') {
		manifest_len++;
		phar_set_32(manifest, manifest_len);
		manifest_hack = true;
	}
	phar_set_32(manifest+4, new_manifest_count);
	if (has_dirs) {
		*(manifest + 8) = (unsigned char) (((PHAR_API_VERSION) >> 8) & 0xFF);
		*(manifest + 9) = (unsigned char) (((PHAR_API_VERSION) & 0xF0));
	} else {
		*(manifest + 8) = (unsigned char) (((PHAR_API_VERSION_NODIR) >> 8) & 0xFF);
		*(manifest + 9) = (unsigned char) (((PHAR_API_VERSION_NODIR) & 0xF0));
	}
	phar_set_32(manifest+10, global_flags);
	phar_set_32(manifest+14, phar->alias_len);

	/* write the manifest header */
	if (sizeof(manifest) != php_stream_write(newfile, manifest, sizeof(manifest))
	|| (size_t)phar->alias_len != php_stream_write(newfile, phar->alias, phar->alias_len)) {

		if (must_close_old_file) {
			php_stream_close(oldfile);
		}

		php_stream_close(newfile);
		phar->alias_len = restore_alias_len;

		spprintf(error, 0, "unable to write manifest header of new phar \"%s\"", ZSTR_VAL(phar->fname));

		goto cleanup;
	}

	phar->alias_len = restore_alias_len;

	phar_set_32(manifest, main_metadata_str.s ? ZSTR_LEN(main_metadata_str.s) : 0);
	if (4 != php_stream_write(newfile, manifest, 4) || ((main_metadata_str.s ? ZSTR_LEN(main_metadata_str.s) : 0)
	&& ZSTR_LEN(main_metadata_str.s) != php_stream_write(newfile, ZSTR_VAL(main_metadata_str.s), ZSTR_LEN(main_metadata_str.s)))) {
		smart_str_free(&main_metadata_str);

		if (must_close_old_file) {
			php_stream_close(oldfile);
		}

		php_stream_close(newfile);
		phar->alias_len = restore_alias_len;

		spprintf(error, 0, "unable to write manifest meta-data of new phar \"%s\"", ZSTR_VAL(phar->fname));

		goto cleanup;
	}
	smart_str_free(&main_metadata_str);

	/* re-calculate the manifest location to simplify later code */
	manifest_ftell = php_stream_tell(newfile);

	/* now write the manifest */
	ZEND_HASH_MAP_FOREACH_PTR(&phar->manifest, entry) {
		const zend_string *metadata_str;
		if (entry->is_deleted || entry->is_mounted) {
			/* remove this from the new phar if deleted, ignore if mounted */
			continue;
		}

		if (entry->is_dir) {
			/* add 1 for trailing slash */
			phar_set_32(entry_buffer, ZSTR_LEN(entry->filename) + 1);
		} else {
			phar_set_32(entry_buffer, ZSTR_LEN(entry->filename));
		}

		if (4 != php_stream_write(newfile, entry_buffer, 4)
		|| ZSTR_LEN(entry->filename) != php_stream_write(newfile, ZSTR_VAL(entry->filename), ZSTR_LEN(entry->filename))
		|| (entry->is_dir && 1 != php_stream_write(newfile, "/", 1))) {
			if (must_close_old_file) {
				php_stream_close(oldfile);
			}
			php_stream_close(newfile);
			if (entry->is_dir) {
				spprintf(error, 0, "unable to write filename of directory \"%s\" to manifest of new phar \"%s\"", ZSTR_VAL(entry->filename), ZSTR_VAL(phar->fname));
			} else {
				spprintf(error, 0, "unable to write filename of file \"%s\" to manifest of new phar \"%s\"", ZSTR_VAL(entry->filename), ZSTR_VAL(phar->fname));
			}
			goto cleanup;
		}

		/* set the manifest meta-data:
			4: uncompressed filesize
			4: creation timestamp
			4: compressed filesize
			4: crc32
			4: flags
			4: metadata-len
			+: metadata
		*/
		mytime = time(NULL);
		phar_set_32(entry_buffer, entry->uncompressed_filesize);
		phar_set_32(entry_buffer+4, mytime);
		phar_set_32(entry_buffer+8, entry->compressed_filesize);
		phar_set_32(entry_buffer+12, entry->crc32);
		phar_set_32(entry_buffer+16, entry->flags);
		metadata_str = entry->metadata_tracker.str;
		phar_set_32(entry_buffer+20, metadata_str ? ZSTR_LEN(metadata_str) : 0);

		if (sizeof(entry_buffer) != php_stream_write(newfile, entry_buffer, sizeof(entry_buffer))
		|| (metadata_str &&
		    ZSTR_LEN(metadata_str) != php_stream_write(newfile, ZSTR_VAL(metadata_str), ZSTR_LEN(metadata_str)))) {
			if (must_close_old_file) {
				php_stream_close(oldfile);
			}

			php_stream_close(newfile);

			spprintf(error, 0, "unable to write temporary manifest of file \"%s\" to manifest of new phar \"%s\"", ZSTR_VAL(entry->filename), ZSTR_VAL(phar->fname));

			goto cleanup;
		}
	} ZEND_HASH_FOREACH_END();
	/* Hack - see bug #65028, add padding byte to the end of the manifest */
	if (manifest_hack) {
		if(1 != php_stream_write(newfile, manifest, 1)) {
			if (must_close_old_file) {
				php_stream_close(oldfile);
			}

			php_stream_close(newfile);

			*error = estrdup("unable to write manifest padding byte");

			goto cleanup;
		}
	}

	/* now copy the actual file data to the new phar */
	offset = php_stream_tell(newfile);
	ZEND_HASH_MAP_FOREACH_PTR(&phar->manifest, entry) {
		if (entry->is_deleted || entry->is_dir || entry->is_mounted) {
			continue;
		}

		if (entry->cfp) {
			file = entry->cfp;
			php_stream_seek(file, entry->header_offset, SEEK_SET);
		} else {
			file = phar_get_efp(entry, false);
			if (-1 == phar_seek_efp(entry, 0, SEEK_SET, 0, false)) {
				if (must_close_old_file) {
					php_stream_close(oldfile);
				}
				php_stream_close(newfile);
				spprintf(error, 0, "unable to seek to start of file \"%s\" while creating new phar \"%s\"", ZSTR_VAL(entry->filename), ZSTR_VAL(phar->fname));
				goto cleanup;
			}
		}

		if (!file) {
			if (must_close_old_file) {
				php_stream_close(oldfile);
			}
			php_stream_close(newfile);
			spprintf(error, 0, "unable to seek to start of file \"%s\" while creating new phar \"%s\"", ZSTR_VAL(entry->filename), ZSTR_VAL(phar->fname));
			goto cleanup;
		}

		/* this will have changed for all files that have either changed compression or been modified */
		entry->offset = entry->offset_abs = offset;
		offset += entry->compressed_filesize;
		if (php_stream_copy_to_stream_ex(file, newfile, entry->compressed_filesize, &wrote) == FAILURE) {
			if (must_close_old_file) {
				php_stream_close(oldfile);
			}

			php_stream_close(newfile);

			spprintf(error, 0, "unable to write contents of file \"%s\" to new phar \"%s\"", ZSTR_VAL(entry->filename), ZSTR_VAL(phar->fname));

			goto cleanup;
		}

		entry->is_modified = 0;

		if (entry->cfp) {
			entry->cfp = NULL;
			entry->header_offset = 0;
		}

		if (entry->fp_type == PHAR_MOD) {
			/* this fp is in use by a phar_entry_data returned by phar_get_entry_data, it will be closed when the phar_entry_data is phar_entry_delref'ed */
			if (entry->fp_refcount == 0 && entry->fp != phar->fp && entry->fp != phar->ufp) {
				php_stream_close(entry->fp);
			}

			entry->fp = NULL;
			entry->fp_type = PHAR_FP;
		} else if (entry->fp_type == PHAR_UFP) {
			entry->fp_type = PHAR_FP;
		}
	} ZEND_HASH_FOREACH_END();

	if (shared_cfp != NULL) {
		php_stream_close(shared_cfp);
		shared_cfp = NULL;
	}

	/* append signature */
	if (global_flags & PHAR_HDR_SIGNATURE) {
		char sig_buf[4];

		php_stream_rewind(newfile);

		if (phar->signature) {
			efree(phar->signature);
			phar->signature = NULL;
		}

		switch(phar->sig_flags) {
			default: {
				char *signature_error = NULL;
				zend_string *signature = phar_create_signature(phar, newfile, &signature_error);
				if (!signature) {
					spprintf(error, 0, "phar error: unable to write signature: %s", signature_error);
					efree(signature_error);

					if (must_close_old_file) {
						php_stream_close(oldfile);
					}
					php_stream_close(newfile);
					return EOF;
				}

				php_stream_write(newfile, ZSTR_VAL(signature), ZSTR_LEN(signature));
				if (phar->sig_flags == PHAR_SIG_OPENSSL ||
					phar->sig_flags == PHAR_SIG_OPENSSL_SHA256 ||
					phar->sig_flags == PHAR_SIG_OPENSSL_SHA512) {
					phar_set_32(sig_buf, ZSTR_LEN(signature));
					php_stream_write(newfile, sig_buf, 4);
				}
				zend_string_release_ex(signature, false);
				break;
			}
		}
		phar_set_32(sig_buf, phar->sig_flags);
		php_stream_write(newfile, sig_buf, 4);
		php_stream_write(newfile, "GBMB", 4);
	}

	/* finally, close the temp file, rename the original phar,
	   move the temp to the old phar, unlink the old phar, and reload it into memory
	*/
	if (phar->fp && free_fp) {
		php_stream_close(phar->fp);
	}

	if (phar->ufp) {
		if (free_ufp) {
			php_stream_close(phar->ufp);
		}
		phar->ufp = NULL;
	}

	if (must_close_old_file) {
		php_stream_close(oldfile);
	}

	phar->halt_offset = halt_offset;
	phar->is_brandnew = 0;

	php_stream_rewind(newfile);

	if (phar->donotflush) {
		/* deferred flush */
		phar->fp = newfile;
	} else {
		phar->fp = php_stream_open_wrapper(ZSTR_VAL(phar->fname), "w+b", IGNORE_URL|STREAM_MUST_SEEK|REPORT_ERRORS, NULL);
		if (!phar->fp) {
			phar->fp = newfile;
			spprintf(error, 4096, "unable to open new phar \"%s\" for writing", ZSTR_VAL(phar->fname));
			return EOF;
		}

		if (phar->flags & PHAR_FILE_COMPRESSED_GZ) {
			/* to properly compress, we have to tell zlib to add a zlib header */
			zval filterparams;

			array_init(&filterparams);
			add_assoc_long(&filterparams, "window", MAX_WBITS+16);
			filter = php_stream_filter_create("zlib.deflate", &filterparams, php_stream_is_persistent(phar->fp));
			zend_array_destroy(Z_ARR(filterparams));

			if (!filter) {
				spprintf(error, 4096, "unable to compress all contents of phar \"%s\" using zlib, PHP versions older than 5.2.6 have a buggy zlib", ZSTR_VAL(phar->fname));
				return EOF;
			}

			php_stream_filter_append(&phar->fp->writefilters, filter);
			php_stream_copy_to_stream_ex(newfile, phar->fp, PHP_STREAM_COPY_ALL, NULL);
			php_stream_filter_flush(filter, 1);
			php_stream_filter_remove(filter, 1);
			php_stream_close(phar->fp);
			/* use the temp stream as our base */
			phar->fp = newfile;
		} else if (phar->flags & PHAR_FILE_COMPRESSED_BZ2) {
			filter = php_stream_filter_create("bzip2.compress", NULL, php_stream_is_persistent(phar->fp));
			php_stream_filter_append(&phar->fp->writefilters, filter);
			php_stream_copy_to_stream_ex(newfile, phar->fp, PHP_STREAM_COPY_ALL, NULL);
			php_stream_filter_flush(filter, 1);
			php_stream_filter_remove(filter, 1);
			php_stream_close(phar->fp);
			/* use the temp stream as our base */
			phar->fp = newfile;
		} else {
			php_stream_copy_to_stream_ex(newfile, phar->fp, PHP_STREAM_COPY_ALL, NULL);
			/* we could also reopen the file in "rb" mode but there is no need for that */
			php_stream_close(newfile);
		}
	}

	if (-1 == php_stream_seek(phar->fp, phar->halt_offset, SEEK_SET)) {
		spprintf(error, 0, "unable to seek to __HALT_COMPILER(); in new phar \"%s\"", ZSTR_VAL(phar->fname));
		return EOF;
	}

	return 0;

cleanup:
	if (shared_cfp != NULL) {
		php_stream_close(shared_cfp);
	}
	ZEND_HASH_MAP_FOREACH_PTR(&phar->manifest, entry) {
		if (entry->cfp) {
			entry->cfp = NULL;
			entry->header_offset = 0;
		}
	} ZEND_HASH_FOREACH_END();

	return EOF;
}
/* }}} */

#ifdef COMPILE_DL_PHAR
#ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
#endif
ZEND_GET_MODULE(phar)
#endif

static ssize_t phar_zend_stream_reader(void *handle, char *buf, size_t len) /* {{{ */
{
	return php_stream_read(phar_get_pharfp((phar_archive_data*)handle), buf, len);
}
/* }}} */

static size_t phar_zend_stream_fsizer(void *handle) /* {{{ */
{
	return ((phar_archive_data*)handle)->halt_offset + 32;
} /* }}} */

zend_op_array *(*phar_orig_compile_file)(zend_file_handle *file_handle, int type);

static bool phar_has_marker_in_filename(const zend_string *filename)
{
	const char *path = ZSTR_VAL(filename);
	const char *basename = zend_memrchr(path, '/', ZSTR_LEN(filename));
#ifdef PHP_WIN32
	const char *backslash = zend_memrchr(path, '\\', ZSTR_LEN(filename));
	if (backslash && (!basename || backslash > basename)) {
		basename = backslash;
	}
#endif
	const char *marker = basename ? basename + 1 : path;
	while ((marker = strstr(marker, ".phar"))) {
		marker += sizeof(".phar") - 1;
		if (*marker == '\0' || *marker == '.') {
			return true;
		}
	}
	return false;
}

static zend_string *phar_resolve_path(zend_string *filename)
{
	zend_string *ret = phar_find_in_include_path(filename);
	if (!ret) {
		ret = phar_save_resolve_path(filename);
	}
	return ret;
}

static zend_op_array *phar_compile_file(zend_file_handle *file_handle, int type) /* {{{ */
{
	zend_op_array *res;
	zend_string *name = NULL;
	bool failed;
	phar_archive_data *phar;

	if (!file_handle || !file_handle->filename) {
		return phar_orig_compile_file(file_handle, type);
	}
	if (phar_has_marker_in_filename(file_handle->filename) && !strstr(ZSTR_VAL(file_handle->filename), "://")) {
		if (SUCCESS == phar_open_from_filename(ZSTR_VAL(file_handle->filename), ZSTR_LEN(file_handle->filename), NULL, 0, &phar, NULL)) {
			if (phar->is_zip || phar->is_tar) {
				zend_file_handle f;

				/* zip or tar-based phar */
				name = zend_strpprintf(4096, "phar://%s/%s", ZSTR_VAL(file_handle->filename), ".phar/stub.php");
				zend_stream_init_filename_ex(&f, name);
				if (SUCCESS == zend_stream_open_function(&f)) {
					zend_string_release(f.filename);
					f.filename = file_handle->filename;
					if (f.opened_path) {
						zend_string_release(f.opened_path);
					}
					f.opened_path = file_handle->opened_path;

					switch (file_handle->type) {
						case ZEND_HANDLE_STREAM:
							if (file_handle->handle.stream.closer && file_handle->handle.stream.handle) {
								file_handle->handle.stream.closer(file_handle->handle.stream.handle);
							}
							file_handle->handle.stream.handle = NULL;
							break;
						default:
							break;
					}
					*file_handle = f;
				}
			} else if (phar->flags & PHAR_FILE_COMPRESSION_MASK) {
				/* compressed phar */
				file_handle->type = ZEND_HANDLE_STREAM;
				/* we do our own reading directly from the phar, don't change the next line */
				file_handle->handle.stream.handle  = phar;
				file_handle->handle.stream.reader  = phar_zend_stream_reader;
				file_handle->handle.stream.closer  = NULL;
				file_handle->handle.stream.fsizer  = phar_zend_stream_fsizer;
				file_handle->handle.stream.isatty  = 0;
				phar->is_persistent ?
					php_stream_rewind(PHAR_G(cached_fp)[phar->phar_pos].fp) :
					php_stream_rewind(phar->fp);
			}
		}
	}

	zend_try {
		failed = false;
		CG(zend_lineno) = 0;
		res = phar_orig_compile_file(file_handle, type);
	} zend_catch {
		failed = true;
		res = NULL;
	} zend_end_try();

	if (name) {
		zend_string_release(name);
	}

	if (failed) {
		zend_bailout();
	}

	return res;
}
/* }}} */

static void mime_type_dtor(zval *zv)
{
	free(Z_PTR_P(zv));
}

PHP_GINIT_FUNCTION(phar) /* {{{ */
{
#if defined(COMPILE_DL_PHAR) && defined(ZTS)
	ZEND_TSRMLS_CACHE_UPDATE();
#endif
	phar_mime_type mime;

	memset(phar_globals, 0, sizeof(zend_phar_globals));
	HT_INVALIDATE(&phar_globals->phar_persist_map);
	HT_INVALIDATE(&phar_globals->phar_fname_map);
	HT_INVALIDATE(&phar_globals->phar_alias_map);
	phar_globals->readonly = 1;

	zend_hash_init(&phar_globals->mime_types, 0, NULL, mime_type_dtor, 1);

#define PHAR_SET_MIME(mimetype, ret, fileext) \
		mime.mime = mimetype; \
		mime.len = sizeof((mimetype))+1; \
		mime.type = ret; \
		zend_hash_str_add_mem(&phar_globals->mime_types, fileext, sizeof(fileext)-1, (void *)&mime, sizeof(phar_mime_type)); \

	PHAR_SET_MIME("text/html", PHAR_MIME_PHPS, "phps")
	PHAR_SET_MIME("text/plain", PHAR_MIME_OTHER, "c")
	PHAR_SET_MIME("text/plain", PHAR_MIME_OTHER, "cc")
	PHAR_SET_MIME("text/plain", PHAR_MIME_OTHER, "cpp")
	PHAR_SET_MIME("text/plain", PHAR_MIME_OTHER, "c++")
	PHAR_SET_MIME("text/plain", PHAR_MIME_OTHER, "dtd")
	PHAR_SET_MIME("text/plain", PHAR_MIME_OTHER, "h")
	PHAR_SET_MIME("text/plain", PHAR_MIME_OTHER, "log")
	PHAR_SET_MIME("text/plain", PHAR_MIME_OTHER, "rng")
	PHAR_SET_MIME("text/plain", PHAR_MIME_OTHER, "txt")
	PHAR_SET_MIME("text/plain", PHAR_MIME_OTHER, "xsd")
	PHAR_SET_MIME("", PHAR_MIME_PHP, "php")
	PHAR_SET_MIME("", PHAR_MIME_PHP, "inc")
	PHAR_SET_MIME("video/avi", PHAR_MIME_OTHER, "avi")
	PHAR_SET_MIME("image/bmp", PHAR_MIME_OTHER, "bmp")
	PHAR_SET_MIME("text/css", PHAR_MIME_OTHER, "css")
	PHAR_SET_MIME("image/gif", PHAR_MIME_OTHER, "gif")
	PHAR_SET_MIME("text/html", PHAR_MIME_OTHER, "htm")
	PHAR_SET_MIME("text/html", PHAR_MIME_OTHER, "html")
	PHAR_SET_MIME("text/html", PHAR_MIME_OTHER, "htmls")
	PHAR_SET_MIME("image/x-ico", PHAR_MIME_OTHER, "ico")
	PHAR_SET_MIME("image/jpeg", PHAR_MIME_OTHER, "jpe")
	PHAR_SET_MIME("image/jpeg", PHAR_MIME_OTHER, "jpg")
	PHAR_SET_MIME("image/jpeg", PHAR_MIME_OTHER, "jpeg")
	PHAR_SET_MIME("application/x-javascript", PHAR_MIME_OTHER, "js")
	PHAR_SET_MIME("audio/midi", PHAR_MIME_OTHER, "midi")
	PHAR_SET_MIME("audio/midi", PHAR_MIME_OTHER, "mid")
	PHAR_SET_MIME("audio/mod", PHAR_MIME_OTHER, "mod")
	PHAR_SET_MIME("movie/quicktime", PHAR_MIME_OTHER, "mov")
	PHAR_SET_MIME("audio/mp3", PHAR_MIME_OTHER, "mp3")
	PHAR_SET_MIME("video/mpeg", PHAR_MIME_OTHER, "mpg")
	PHAR_SET_MIME("video/mpeg", PHAR_MIME_OTHER, "mpeg")
	PHAR_SET_MIME("application/pdf", PHAR_MIME_OTHER, "pdf")
	PHAR_SET_MIME("image/png", PHAR_MIME_OTHER, "png")
	PHAR_SET_MIME("application/shockwave-flash", PHAR_MIME_OTHER, "swf")
	PHAR_SET_MIME("image/tiff", PHAR_MIME_OTHER, "tif")
	PHAR_SET_MIME("image/tiff", PHAR_MIME_OTHER, "tiff")
	PHAR_SET_MIME("audio/wav", PHAR_MIME_OTHER, "wav")
	PHAR_SET_MIME("image/xbm", PHAR_MIME_OTHER, "xbm")
	PHAR_SET_MIME("text/xml", PHAR_MIME_OTHER, "xml")

	phar_restore_orig_functions();
}
/* }}} */

PHP_GSHUTDOWN_FUNCTION(phar) /* {{{ */
{
	zend_hash_destroy(&phar_globals->mime_types);
}
/* }}} */

PHP_MINIT_FUNCTION(phar) /* {{{ */
{
	REGISTER_INI_ENTRIES();

	phar_orig_compile_file = zend_compile_file;
	zend_compile_file = phar_compile_file;

	phar_save_resolve_path = zend_resolve_path;
	zend_resolve_path = phar_resolve_path;

	phar_object_init();

	phar_intercept_functions_init();
	phar_save_orig_functions();

	return php_register_url_stream_wrapper("phar", &php_stream_phar_wrapper);
}
/* }}} */

PHP_MSHUTDOWN_FUNCTION(phar) /* {{{ */
{
	php_unregister_url_stream_wrapper("phar");

	phar_intercept_functions_shutdown();

	if (zend_compile_file == phar_compile_file) {
		zend_compile_file = phar_orig_compile_file;
	}

	if (PHAR_G(manifest_cached)) {
		zend_hash_destroy(&(cached_phars));
		zend_hash_destroy(&(cached_alias));
	}

	UNREGISTER_INI_ENTRIES();
	return SUCCESS;
}
/* }}} */

void phar_request_initialize(void) /* {{{ */
{
	if (!PHAR_G(request_init))
	{
		PHAR_G(last_phar) = NULL;
		PHAR_G(last_alias) = NULL;
		PHAR_G(last_phar_name) = NULL;
		PHAR_G(has_bz2) = zend_hash_str_exists(&module_registry, "bz2", sizeof("bz2")-1);
		PHAR_G(has_zlib) = zend_hash_str_exists(&module_registry, "zlib", sizeof("zlib")-1);
		PHAR_G(request_init) = 1;
		PHAR_G(request_ends) = 0;
		PHAR_G(request_done) = 0;
		zend_hash_init(&(PHAR_G(phar_fname_map)), 5, zend_get_hash_value, destroy_phar_data,  0);
		zend_hash_init(&(PHAR_G(phar_persist_map)), 5, zend_get_hash_value, NULL,  0);
		zend_hash_init(&(PHAR_G(phar_alias_map)), 5, zend_get_hash_value, NULL, 0);

		if (PHAR_G(manifest_cached)) {
			phar_archive_data *pphar;
			phar_entry_fp *stuff = (phar_entry_fp *) ecalloc(zend_hash_num_elements(&cached_phars), sizeof(phar_entry_fp));

			ZEND_HASH_MAP_FOREACH_PTR(&cached_phars, pphar) {
				stuff[pphar->phar_pos].manifest = (phar_entry_fp_info *) ecalloc( zend_hash_num_elements(&(pphar->manifest)), sizeof(phar_entry_fp_info));
			} ZEND_HASH_FOREACH_END();

			PHAR_G(cached_fp) = stuff;
		}

		PHAR_G(phar_SERVER_mung_list) = 0;
		PHAR_G(cwd) = NULL;
		PHAR_G(cwd_len) = 0;
		PHAR_G(cwd_init) = 0;
	}
}
/* }}} */

PHP_RSHUTDOWN_FUNCTION(phar) /* {{{ */
{
	PHAR_G(request_ends) = 1;

	if (PHAR_G(request_init))
	{
		phar_release_functions();
		zend_hash_destroy(&(PHAR_G(phar_alias_map)));
		HT_INVALIDATE(&PHAR_G(phar_alias_map));
		zend_hash_destroy(&(PHAR_G(phar_fname_map)));
		HT_INVALIDATE(&PHAR_G(phar_fname_map));
		zend_hash_destroy(&(PHAR_G(phar_persist_map)));
		HT_INVALIDATE(&PHAR_G(phar_persist_map));
		PHAR_G(phar_SERVER_mung_list) = 0;

		if (PHAR_G(cached_fp)) {
			for (uint32_t i = 0; i < zend_hash_num_elements(&cached_phars); ++i) {
				if (PHAR_G(cached_fp)[i].fp) {
					php_stream_close(PHAR_G(cached_fp)[i].fp);
				}
				if (PHAR_G(cached_fp)[i].ufp) {
					php_stream_close(PHAR_G(cached_fp)[i].ufp);
				}
				efree(PHAR_G(cached_fp)[i].manifest);
			}
			efree(PHAR_G(cached_fp));
			PHAR_G(cached_fp) = 0;
		}

		PHAR_G(request_init) = 0;

		if (PHAR_G(cwd)) {
			efree(PHAR_G(cwd));
		}

		PHAR_G(cwd) = NULL;
		PHAR_G(cwd_len) = 0;
		PHAR_G(cwd_init) = 0;
	}

	PHAR_G(request_done) = 1;
	return SUCCESS;
}
/* }}} */

PHP_MINFO_FUNCTION(phar) /* {{{ */
{
	phar_request_initialize();
	php_info_print_table_start();
	php_info_print_table_row(2, "Phar: PHP Archive support", "enabled");
	php_info_print_table_row(2, "Phar API version", PHP_PHAR_API_VERSION);
	php_info_print_table_row(2, "Phar-based phar archives", "enabled");
	php_info_print_table_row(2, "Tar-based phar archives", "enabled");
	php_info_print_table_row(2, "ZIP-based phar archives", "enabled");

	if (PHAR_G(has_zlib)) {
		php_info_print_table_row(2, "gzip compression", "enabled");
	} else {
		php_info_print_table_row(2, "gzip compression", "disabled (install ext/zlib)");
	}

	if (PHAR_G(has_bz2)) {
		php_info_print_table_row(2, "bzip2 compression", "enabled");
	} else {
		php_info_print_table_row(2, "bzip2 compression", "disabled (install ext/bz2)");
	}
#ifdef PHAR_HAVE_OPENSSL
	php_info_print_table_row(2, "Native OpenSSL support", "enabled");
#else
	if (zend_hash_str_exists(&module_registry, "openssl", sizeof("openssl")-1)) {
		php_info_print_table_row(2, "OpenSSL support", "enabled");
	} else {
		php_info_print_table_row(2, "OpenSSL support", "disabled (install ext/openssl)");
	}
#endif
	php_info_print_table_end();

	php_info_print_box_start(0);
	PUTS("Phar based on pear/PHP_Archive, original concept by Davey Shafik.");
	PUTS(!sapi_module.phpinfo_as_text?"<br />":"\n");
	PUTS("Phar fully realized by Gregory Beaver and Marcus Boerger.");
	PUTS(!sapi_module.phpinfo_as_text?"<br />":"\n");
	PUTS("Portions of tar implementation Copyright (c) 2003-2009 Tim Kientzle.");
	php_info_print_box_end();

	DISPLAY_INI_ENTRIES();
}
/* }}} */

/* {{{ phar_module_entry */
static const zend_module_dep phar_deps[] = {
	ZEND_MOD_OPTIONAL("apc")
	ZEND_MOD_OPTIONAL("bz2")
	ZEND_MOD_OPTIONAL("openssl")
	ZEND_MOD_OPTIONAL("zlib")
	ZEND_MOD_OPTIONAL("standard")
	ZEND_MOD_REQUIRED("hash")
	ZEND_MOD_REQUIRED("spl")
	ZEND_MOD_END
};

zend_module_entry phar_module_entry = {
	STANDARD_MODULE_HEADER_EX, NULL,
	phar_deps,
	"Phar",
	NULL,
	PHP_MINIT(phar),
	PHP_MSHUTDOWN(phar),
	NULL,
	PHP_RSHUTDOWN(phar),
	PHP_MINFO(phar),
	PHP_PHAR_VERSION,
	PHP_MODULE_GLOBALS(phar),   /* globals descriptor */
	PHP_GINIT(phar),            /* globals ctor */
	PHP_GSHUTDOWN(phar),        /* globals dtor */
	NULL,                       /* post deactivate */
	STANDARD_MODULE_PROPERTIES_EX
};
/* }}} */
