/*
   +----------------------------------------------------------------------+
   | PHP Version 4                                                        |
   +----------------------------------------------------------------------+
   | Copyright (c) 1997-2002 The PHP Group                                |
   +----------------------------------------------------------------------+
   | This source file is subject to version 2.02 of the PHP license,      |
   | that is bundled with this package in the file LICENSE, and is        |
   | available at through the world-wide-web at                           |
   | http://www.php.net/license/2_02.txt.                                 |
   | If you did not receive a copy of the PHP license and are unable to   |
   | obtain it through the world-wide-web, please send a note to          |
   | license@php.net so we can mail you a copy immediately.               |
   +----------------------------------------------------------------------+
   | Author: Sara Golemon <pollita@php.net>                               |
   +----------------------------------------------------------------------+
*/

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"

#if WITH_CVSCLIENT

#include "php_cvsclient.h"
#include "php_globals.h"
#include "php_network.h"
#include "ext/standard/url.h"
#include "ext/standard/info.h"
#include "ext/standard/php_string.h"
#include "ext/standard/php_smart_str.h"
#ifndef PHP_WIN32
#include <sys/socket.h>
#endif
#include <ctype.h>

/* ********************* */
/* CVS pserver interface */
/* ********************* */

/* {{{ php_cvsclient_do_connect */
static php_stream * php_cvsclient_do_connect(char *path, int options, php_stream_context *context, php_url **presource TSRMLS_DC)
{
	php_stream *stream = NULL;
	php_url *resource = NULL;

	resource = php_url_parse((char *) path);
	if (!resource || !resource->scheme || !resource->host) {
		goto connect_errexit;
	}

	if (strcasecmp("cvs", resource->scheme) && strcasecmp("cvs.diff", resource->scheme)) {
		goto connect_errexit;
	}

	/* use port 2401 if one wasn't specified */
	if (resource->port == 0) {
		resource->port = 2401;
	}

	stream = php_stream_sock_open_host(resource->host, resource->port, SOCK_STREAM, NULL, 0);
	if (stream == NULL) {
		goto connect_errexit;
	}

    php_stream_context_set(stream, context);
    php_stream_notify_info(context, PHP_STREAM_NOTIFY_CONNECT, NULL, 0);

	if (presource) {
		*presource = resource;
	} else {
		php_url_free(resource);
	}

	return stream;

 connect_errexit:
	if (resource) {
		php_url_free(resource);
	}
	if (stream) {
		php_stream_close(stream);
	}

	return NULL;
}
/* }}} */

/* {{{ php_cvsclient_encode
 */
static char * php_cvsclient_encode(const char *orig)
{
	unsigned char *enc;
	int i;

	enc = estrdup(orig);
	for(i=0; i<strlen(enc); i++) {
		if (enc[i] >= 32 && enc[i] <= 127) {
			enc[i] = cvs_encode[enc[i] - 32];
		}
	}

	return enc;
}
/* }}} */

/* {{{ php_cvsclient_authenticate
 */
static int php_cvsclient_authenticate(php_stream *stream, const char *cvsroot, const char *user, const char *pass TSRMLS_DC)
{
	char *encpw;
	char response[128];

	encpw = php_cvsclient_encode(pass);

	php_stream_printf(stream TSRMLS_CC,
		"BEGIN AUTH REQUEST\n"
		"%s\n"
		"%s\n"
		"A%s\n"
		"END AUTH REQUEST\n",
		cvsroot, user, encpw);
	
	efree(encpw);

	/* Who do you love? */
	if (php_stream_gets(stream, response, sizeof(response)-1) == NULL) {
		return FAILURE;
	}
	if (strncmp(response, "I LOVE YOU", strlen("I LOVE YOU"))) {
		return FAILURE;
	}
	return SUCCESS;
}
/* }}} */

/* {{{ php_cvsclient_negotiate */
static int php_cvsclient_negotiate(php_stream *stream, const char *cvsroot TSRMLS_DC)
{
	php_cvsclient_requests *verbs = cvsclient_requests;
	char response[4096];
	int requests = 0, i;

	php_stream_printf(stream TSRMLS_CC, 
		"Root %s\n"
		"Valid-responses %s\n"
		"valid-requests\n",
		cvsroot,
		"Valid-requests New-entry Updated Created Update-existing Merged Rcs-diff Patched Mode Mod-time Checksum "
		"Copy-file Removed Remove-entry Set-static-directory Clear-static-directory Set-sticky Clear-sticky Template Set-checkin-prog "
		"Set-update-prog Notified Module-expansion Wrapper-rcsOption ok error Checked-in M E F MT");

	if (php_stream_gets(stream, response, sizeof(response)-1) == NULL) {
		return 0;
	}

	for(i=0; i<strlen(response); i++) {
		response[i] = tolower(response[i]);
	}

	/* TODO: Improve this, we could (and probably do) get false positives */
	while (verbs->request && verbs->label) {
		if (strstr(response, verbs->label)) {
			requests |= verbs->request;
		}
		verbs++;
	}

	return requests;
}
/* }}} */

/* ********************** */
/* Wrapper Implementation */
/* ********************** */

static char *php_cvsclient_get_url_param(const char *query, const char *param TSRMLS_DC)
{
	int query_len, param_len;
	const char *p, *e;
	char *param_dup, *val;

	if (!query || !param) {
		return NULL;
	}

	query_len = strlen(query);
	param_len = strlen(param);

	if (!query_len || !param_len) {
		return NULL;
	}

	param_dup = emalloc(param_len + 3);
	memcpy(param_dup + 1, param, param_len);
	param_dup[0] = '&';
	param_dup[param_len+1] = '=';
	param_dup[param_len+2] = '\0';

	if (strncasecmp(query, param_dup + 1, param_len + 1) == 0) {
		/* param found at start of string */
		p = query + param_len + 1;
	} else {
		p = strstr(query, param_dup);
		if (p) {
			/* param found further in */
			p += param_len + 2;
		}
	}

	if (!p) {
		/* parameter not found */
		efree(param_dup);
		return NULL;
	}

	e = strchr(p, '&');
	if (!e) {
		/* last parameter */
		e = p + strlen(p);
	}

	val = estrndup(p, e - p);
	efree(param_dup);

	return val;
}

/* {{{ php_stream_url_wrap_cvsclient
 */
static php_stream * php_stream_url_wrap_cvsclient(php_stream_wrapper *wrapper, char *path, char *mode, int options, char **opened_path, php_stream_context *context STREAMS_DC TSRMLS_DC)
{
	php_stream *stream = NULL, *memstream = NULL;
	php_url *resource = NULL;
	char *module = NULL;
	char *cvsroot = NULL;
	char *filepath = NULL;
	char *filename, *p;
	char response[4096];
	int valid_requests, i;
	long filesize;
	zval *wrapperdata = NULL;
	zval **tmpzval;
	int req_options = 0;

	if (strpbrk(mode, "awx+")) {
		php_stream_wrapper_log_error(wrapper, options TSRMLS_CC, "CVS wrapper does not support writeable connections (yet).");
		return NULL;
	}

	/* Connect */
	stream = php_cvsclient_do_connect(path, options, context, &resource TSRMLS_CC);
	if (!stream || !resource->path) {
		goto wrap_errexit;
	}
	p = strchr(resource->path + 1, '/');
	if (p) {
		cvsroot = estrndup(resource->path, p - resource->path);
		filename = p;
		p = strchr(filename + 1, '/');
		if (p) {
			module = estrndup(filename, p - filename);
			filename = p;
			p = strrchr(filename + 1, '/');
			if (p) {
				filepath = estrndup(filename, p - filename);
				filename = p + 1;
			} else {
				filename++;
			}
		} else {
			goto wrap_errexit;
		}
	} else {
		goto wrap_errexit;
	}

	/* Authenticate */
	if (resource && resource->user && resource->pass &&
		php_cvsclient_authenticate(stream, cvsroot, resource->user, resource->pass TSRMLS_CC) == FAILURE) {
		goto wrap_errexit;
	}

	/* Negotiate */
	if ((valid_requests = php_cvsclient_negotiate(stream, cvsroot TSRMLS_CC)) == 0) {
		goto wrap_errexit;
	}

	/* Request */
	if (resource->query && strlen(resource->query)) {
		char *rev;

		rev = php_cvsclient_get_url_param(resource->query, "r" TSRMLS_CC);
		if (rev) {
			/* Revision tag provided in URL (overrides context option) */
			php_stream_printf(stream TSRMLS_CC, "Argument -r\nArgument %s\n", rev);
			efree(rev);
			req_options |= CVSCLIENT_HAVE_REVISION;			
		}
	}
	if (((req_options & CVSCLIENT_HAVE_REVISION) == 0) &&
		context &&
		php_stream_context_get_option(context, "cvs", "revision", &tmpzval) == SUCCESS) {
		SEPARATE_ZVAL(tmpzval);
		convert_to_string_ex(tmpzval);
		php_stream_printf(stream TSRMLS_CC, "Argument -r\nArgument %s\n", Z_STRVAL_PP(tmpzval));
		zval_ptr_dtor(tmpzval);
		req_options |= CVSCLIENT_HAVE_REVISION;
	}
	php_stream_printf(stream TSRMLS_CC, 
		"Argument %s\n"
		"Directory .\n"
		"%s%s%s\n"
		"update\n", 
		filename,
		cvsroot, module, filepath ? filepath : "");

	efree(cvsroot);
	cvsroot = NULL;
	efree(module);
	module = NULL;
	if (filepath) {
		efree(filepath);
		filepath = NULL;
	}

	/* Parse response */
	ALLOC_INIT_ZVAL(wrapperdata);
	array_init(wrapperdata);
	while (php_stream_gets(stream, response, sizeof(response)-1) != NULL) {
		if (strncasecmp(response, "error", 5) == 0) {
			goto wrap_errexit;
		}
		if (strncasecmp(response, "mod-time ", 9) == 0) {
			add_assoc_string(wrapperdata, "mod-time", response + 9, 1);
		}
		if ((strlen(response) > strlen(filename) + 4) && 
			response[0] == '/' && 
			strncmp(response + 1, filename, strlen(filename)) == 0 &&
			response[strlen(filename)+1] == '/') {
			p = response + strlen(filename) + 2;
			p = strchr(p, '/');
			if (p) {
				*p = '\0';
				p = response + strlen(filename) + 2;
				add_assoc_string(wrapperdata, "revision", p, 1);
			}
		}
		/* TODO: Parse other elements into wrapperdata */
		for(i=0; i<strlen(response); i++) {
			if (!isdigit(response[i]) && !iscntrl(response[i])) {
				goto header_loop;
			}
		}
		break;
 header_loop:
		;
	}
	filesize = atoi(response);
	add_assoc_long(wrapperdata, "filesize", filesize);

	memstream = php_stream_fopen_tmpfile();
	if (!memstream) {
		goto wrap_errexit;
	}
	while (filesize) {
		int read;

		read = php_stream_read(stream, response, (filesize > (sizeof(response) - 1)) ? sizeof(response)-1 : filesize);
		php_stream_write(memstream, response, read);

		filesize -= read;
		if (read <= 0) {
			php_error_docref(NULL TSRMLS_CC, E_WARNING, "Error reading remote file.");
			goto wrap_errexit;
		}
	}
	php_stream_rewind(memstream);
	php_stream_close(stream);

	memstream->wrapperdata = wrapperdata;

	php_url_free(resource);
	resource = NULL;

	return memstream;

 wrap_errexit:
	if (filepath) {
		efree(filepath);
	}
	if (module) {
		efree(module);
	}
	if (cvsroot) {
		efree(cvsroot);
	}
	if (wrapperdata) {
		zval_ptr_dtor(&wrapperdata);
	}
	if (resource) {
		php_url_free(resource);
	}
	if (stream) {
		php_stream_close(stream);
	}
	if (memstream) {
		php_stream_close(memstream);
	}
	return NULL;
}
/* }}} */

/* {{{ php_stream_cvsclient_stat
 */
static int php_stream_cvsclient_stat(php_stream_wrapper *wrapper,
		php_stream *stream,
		php_stream_statbuf *ssb
		TSRMLS_DC)
{
	/* For now, we return with a failure code to prevent the underlying
	 * file's details from being used instead. */
	return -1;
}
/* }}} */

static php_stream_wrapper_ops cvsclient_stream_wops = {
	php_stream_url_wrap_cvsclient,
	NULL, /* stream_close */
	php_stream_cvsclient_stat,
	NULL, /* stat_url */
	NULL, /* opendir */
	"CVS"
#if PHP_MAJOR_VERSION >= 5
	,NULL /* unlink */
#endif
};

php_stream_wrapper php_stream_cvsclient_wrapper =  {
	&cvsclient_stream_wops,
	NULL,
	1 /* is_url */
};

/* {{{ php_stream_url_wrap_cvsclient_diff
 */
static php_stream * php_stream_url_wrap_cvsclient_diff(php_stream_wrapper *wrapper, char *path, char *mode, int options, char **opened_path, php_stream_context *context STREAMS_DC TSRMLS_DC)
{
	php_stream *stream = NULL, *memstream = NULL;
	php_url *resource = NULL;
	char *module = NULL;
	char *cvsroot = NULL;
	char *filepath = NULL;
	char *filename, *p;
	char response[4096];
	int valid_requests, unified = 0;
	zval *wrapperdata = NULL, *revision_list = NULL;
	zval **tmpzval;
	char *rev1 = NULL, *rev2 = NULL, *type = NULL;

	if (strpbrk(mode, "awx+")) {
		php_stream_wrapper_log_error(wrapper, options TSRMLS_CC, "Writing does not make sense for a CVS diff.");
		return NULL;
	}

	/* Connect */
	stream = php_cvsclient_do_connect(path, options, context, &resource TSRMLS_CC);
	if (!stream || !resource->path) {
		goto wrap_errexit;
	}
	p = strchr(resource->path + 1, '/');
	if (p) {
		cvsroot = estrndup(resource->path, p - resource->path);
		filename = p;
		p = strchr(filename + 1, '/');
		if (p) {
			module = estrndup(filename, p - filename);
			filename = p;
			p = strrchr(filename + 1, '/');
			if (p) {
				filepath = estrndup(filename, p - filename);
				filename = p + 1;
			} else {
				filename++;
			}
		} else {
			goto wrap_errexit;
		}
	} else {
		goto wrap_errexit;
	}

	/* Authenticate */
	if (resource && resource->user && resource->pass &&
		php_cvsclient_authenticate(stream, cvsroot, resource->user, resource->pass TSRMLS_CC) == FAILURE) {
		goto wrap_errexit;
	}

	/* Negotiate */
	if ((valid_requests = php_cvsclient_negotiate(stream, cvsroot TSRMLS_CC)) == 0) {
		goto wrap_errexit;
	}

	if (resource->query && strlen(resource->query)) {
		rev1 = php_cvsclient_get_url_param(resource->query, "r1" TSRMLS_CC);
		rev2 = php_cvsclient_get_url_param(resource->query, "r2" TSRMLS_CC);
		type = php_cvsclient_get_url_param(resource->query, "type" TSRMLS_CC);
	}
	if (!rev1 && context &&
		(php_stream_context_get_option(context, "cvs", "revision1", &tmpzval) == SUCCESS ||
		 php_stream_context_get_option(context, "cvs", "revision",  &tmpzval) == SUCCESS)) {
		SEPARATE_ZVAL(tmpzval);
		convert_to_string_ex(tmpzval);
		rev1 = estrndup(Z_STRVAL_PP(tmpzval), Z_STRLEN_PP(tmpzval));
		zval_ptr_dtor(tmpzval);
	}
	if (!rev2 && context &&
		php_stream_context_get_option(context, "cvs", "revision2", &tmpzval) == SUCCESS) {
		SEPARATE_ZVAL(tmpzval);
		convert_to_string_ex(tmpzval);
		rev2 = estrndup(Z_STRVAL_PP(tmpzval), Z_STRLEN_PP(tmpzval));
		zval_ptr_dtor(tmpzval);
	}
	if (!type && context &&
		php_stream_context_get_option(context, "cvs", "diff_type", &tmpzval) == SUCCESS) {
		SEPARATE_ZVAL(tmpzval);
		convert_to_string_ex(tmpzval);
		type = estrndup(Z_STRVAL_PP(tmpzval), Z_STRLEN_PP(tmpzval));
		zval_ptr_dtor(tmpzval);
	}

	if (!rev1) {
		/* Revision1 is required */
		goto wrap_errexit;
	}
	if (type && ((strcasecmp(type, "u") == 0) || (strcasecmp(type, "unified") == 0))) {
		unified = 1;
		php_stream_write_string(stream, "Argument -u\n");
	}
	php_stream_printf(stream TSRMLS_CC, 
		"Argument -r\n"
		"Argument %s\n"
		"Argument -r\n"
		"Argument %s\n", 
		rev1, rev2 ? rev2 : "HEAD");

	/* Request */
	php_stream_printf(stream TSRMLS_CC,
		"Directory .\n"
		"%s%s%s\n"
		"Entry /%s/%s///\n"
		"Unchanged %s\n"
		"Argument %s\n"
		"diff\n",
		cvsroot, module, filepath ? filepath : "",
		filename, rev2 ? rev2 : "HEAD",
		filename,
		filename);

	efree(rev1);
	rev1 = NULL;
	if (rev2) {
		efree(rev2);
		rev2 = NULL;
	}
	if (type) {
		efree(type);
		type = NULL;
	}

	efree(cvsroot);
	cvsroot = NULL;
	efree(module);
	module = NULL;
	if (filepath) {
		efree(filepath);
		filepath = NULL;
	}

	/* Parse response */
	ALLOC_INIT_ZVAL(wrapperdata);
	array_init(wrapperdata);
	while (php_stream_gets(stream, response, sizeof(response)-1) != NULL) {
		if (strncasecmp(response, "error", 5) == 0) {
			goto wrap_errexit;
		}
		if (strncasecmp(response, "M retrieving revision ", strlen("M retrieving revision ")) == 0) {
			zval *retval;

			if (!revision_list) {
				ALLOC_INIT_ZVAL(revision_list);
				array_init(revision_list);
				add_assoc_zval(wrapperdata, "revisions", revision_list);
			}

			ALLOC_INIT_ZVAL(retval);
			php_trim(response + strlen("M retrieving revision "), strlen(response) - strlen("M retrieving revision "), NULL, 0, retval, 2 TSRMLS_CC);

			add_next_index_zval(revision_list, retval);
		}
		/* TODO: Parse other elements into wrapperdata */
		if (strlen(response) < 4 || strncasecmp(response, "M diff", strlen("M diff"))) {
			goto header_loop;
		}
		break;
 header_loop:
		;
	}

	memstream = php_stream_fopen_tmpfile();
	if (!memstream) {
		goto wrap_errexit;
	}
	while (php_stream_gets(stream, response, sizeof(response)-1) != NULL &&
		strlen(response) >= strlen("M ") && strncmp(response, "M ", strlen("M ")) == 0) {
		if (unified && strlen(response) > strlen("M --- ") && strncmp(response, "M --- ", strlen("M --- ")) == 0) {
			zval *retval;

			ALLOC_INIT_ZVAL(retval);
			php_trim(response + strlen("M "), strlen(response) - (sizeof("M ") - 1), NULL, 0, retval, 2 TSRMLS_CC);

			add_assoc_zval(wrapperdata, "revision1", retval);
		} else if (unified && strlen(response) > strlen("M +++ ") && strncmp(response, "M +++ ", sizeof("M +++ ") - 1) == 0) {
			zval *retval;

			ALLOC_INIT_ZVAL(retval);
			php_trim(response + (sizeof("M ") - 1), strlen(response) - (sizeof("M ") - 1), NULL, 0, retval, 2 TSRMLS_CC);

			add_assoc_zval(wrapperdata, "revision2", retval);
		} else php_stream_write(memstream, response + (sizeof("M ") - 1), strlen(response) - (sizeof("M ") - 1));
	}

	php_stream_rewind(memstream);
	php_stream_close(stream);

	memstream->wrapperdata = wrapperdata;

	php_url_free(resource);

	return memstream;

 wrap_errexit:
	if (filepath) {
		efree(filepath);
	}
	if (module) {
		efree(module);
	}
	if (cvsroot) {
		efree(cvsroot);
	}
	if (wrapperdata) {
		/* This will catch revision_list as well */
		zval_ptr_dtor(&wrapperdata);
	}
	if (resource) {
		php_url_free(resource);
	}
	if (stream) {
		php_stream_close(stream);
	}
	if (memstream) {
		php_stream_close(memstream);
	}
	if (rev1) {
		efree(rev1);
	}
	if (rev2) {
		efree(rev2);
	}
	if (type) {
		efree(type);
	}

	return NULL;
}
/* }}} */

static php_stream_wrapper_ops cvsclient_stream_diff_wops = {
	php_stream_url_wrap_cvsclient_diff,
	NULL, /* stream_close */
	NULL,
	NULL, /* stat_url */
	NULL, /* opendir */
	"CVSDIFF"
#if PHP_MAJOR_VERSION >= 5
	,NULL /* unlink */
#endif
};

php_stream_wrapper php_stream_cvsclient_diff_wrapper =  {
	&cvsclient_stream_diff_wops,
	NULL,
	1 /* is_url */
};

/* ****************** */
/* Userland Functions */
/* ****************** */

/* {{{ proto resource cvsclient_connect(string server, string cvsroot[, int port])
   Connect to CVS pserver */
PHP_FUNCTION(cvsclient_connect)
{
	php_cvsclient_resource *cvsclient;
	php_stream *stream;
	char *server, *cvsroot;
	long server_len, cvsroot_len, port = PHP_CVSCLIENT_DEFAULT_PORT;

	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss|l", &server, &server_len, &cvsroot, &cvsroot_len, &port) == FAILURE) {
		RETURN_FALSE;
	}

	stream = php_stream_sock_open_host(server, port, SOCK_STREAM, NULL, 0);
	if (!stream) {
		php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to connect to CVS pserver cvs://%s:%ld", server, port);
		RETURN_FALSE;
	}

	cvsclient = emalloc(sizeof(php_cvsclient_resource));
	cvsclient->server = stream;
	cvsclient->cvsroot = estrndup(cvsroot, cvsroot_len);

	ZEND_REGISTER_RESOURCE(return_value, cvsclient, le_cvsclient);
}
/* }}} */

/* {{{ proto bool cvsclient_login(resource cvsclient, string username, string password)
   Authenticate to the CVS pserver */
PHP_FUNCTION(cvsclient_login)
{
	zval *zcvsclient;
	php_cvsclient_resource *cvsclient;
	char *username, *password;
	long username_len, password_len;

	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rss", &zcvsclient, &username, &username_len, &password, &password_len) == FAILURE) {
		RETURN_FALSE;
	}

	ZEND_FETCH_RESOURCE(cvsclient, php_cvsclient_resource*, &zcvsclient, -1, CVSCLIENT_RES_NAME, le_cvsclient);

	/* Authenticate */
	if (php_cvsclient_authenticate(cvsclient->server, cvsclient->cvsroot, username, password TSRMLS_CC) == FAILURE) {
		php_error_docref(NULL TSRMLS_CC, E_WARNING, "CVS pserver authentication failure.");
		RETURN_FALSE;
	}

	/* Not really part of the authentication process, but the protocol expects it next. */
	cvsclient->requests = php_cvsclient_negotiate(cvsclient->server, cvsclient->cvsroot TSRMLS_CC);

	RETURN_TRUE;
}
/* }}} */

/* {{{ proto mixed cvsclient_retrieve(resource cvsclient, string module, string path[, string saveto[, string revision]])
   Retrieve specified <revision> (can also be tag or branch) of filename referred to by <path> in <module> */
PHP_FUNCTION(cvsclient_retrieve)
{
	zval *zcvsclient;
	php_cvsclient_resource *cvsclient;
	char *p, *module, *path, *saveto = NULL, *revision = NULL, *file, response[4096];
	long module_len, path_len, saveto_len = 0, revision_len = 0, filesize = 0, count, i;

	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rss|ss", &zcvsclient, &module, &module_len, &path, &path_len, &saveto, &saveto_len, &revision, &revision_len) == FAILURE) {
		RETURN_FALSE;
	}

	ZEND_FETCH_RESOURCE(cvsclient, php_cvsclient_resource*, &zcvsclient, -1, CVSCLIENT_RES_NAME, le_cvsclient);

	if (path[0] == '/') {
		path++;
	}

	p = strrchr(path, '/');
	if (revision) {
		php_stream_printf(cvsclient->server TSRMLS_CC, "Argument -r\nArgument %s\n", revision);
	}
	if (p) {
		/* path is non-root */
		char save;

		save = path[path_len - (p - path)];
		path[path_len - (p - path)] = '\0';
		php_stream_printf(cvsclient->server TSRMLS_CC, 
			"Argument %s\n"
			"Directory .\n"
			"%s/%s/%s\n", 
			p + 1,
			cvsclient->cvsroot, module, path);
		path[path_len - (p - path)] = save;
	} else {
		/* file is at root of cvs module */
		php_stream_printf(cvsclient->server TSRMLS_CC, 
			"Argument %s\n"
			"Directory .\n"
			"%s/%s\n", 
			path,
			cvsclient->cvsroot, module);
	}
	php_stream_write_string(cvsclient->server, "update\n");
	
	while (filesize == 0 && php_stream_gets(cvsclient->server, response, sizeof(response)-1) != NULL) {
		/* Parse response and look for data */
		if (strncasecmp(response, "error", 5) == 0) {
			php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unrecoverable error occured (%s)", response);
			zend_list_delete(Z_LVAL_P(zcvsclient));
			RETURN_FALSE;
		}
		filesize = 1;
		for(i=0; filesize && i<strlen(response); i++) {
			if (!isdigit(response[i]) && !iscntrl(response[i])) {
				filesize = 0;
			}
		}
	}

	if (!filesize) {
		php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to find document length.");
		RETURN_FALSE;
	}

	filesize = atoi(response);
	if (filesize <= 0) {
		php_error_docref(NULL TSRMLS_CC, E_WARNING, "Invalid filesize (%ld)", filesize);
		RETURN_FALSE;
	}

	if (saveto && (saveto_len > 1 || (saveto_len == 1 && saveto[0] == '-'))) {
		/* Save to a "local" file */
		php_stream *outfile;

		if (!(outfile = php_stream_open_wrapper(saveto, "wb", ENFORCE_SAFE_MODE | REPORT_ERRORS, NULL))) {
			RETURN_FALSE;
		}
		while (filesize > 0) {
			count = php_stream_read(cvsclient->server, response, filesize > (sizeof(response) - 1) ? sizeof(response) - 1 : filesize);
			php_stream_write(outfile, response, count);
			filesize -= count;
			if (count <= 0) {
				php_error_docref(NULL TSRMLS_CC, E_WARNING, "Error reading remote file.");
				RETURN_FALSE;
			}
		}
		RETURN_TRUE;
	} else {
		/* Return as a string */
		p = file = emalloc(filesize);
		while (filesize > 0) {
			count = php_stream_read(cvsclient->server, p, filesize);
			filesize -= count;
			p += count;
			if (count <= 0) {
				php_error_docref(NULL TSRMLS_CC, E_WARNING, "Error reading remote file.");
				efree(file);
				RETURN_FALSE;
			}
		}
		RETURN_STRINGL(file, p - file, 0);
	}
}
/* }}} */

static char *php_cvsclient_parse_log(const char *log, const char *param TSRMLS_DC)
{
	int log_len, param_len;
	const char *p, *e;
	char *param_dup, *val;

	if (!log || !param) {
		return NULL;
	}

	log_len = strlen(log);
	param_len = strlen(param);

	if (!log_len || !param_len) {
		return NULL;
	}

	param_dup = emalloc(param_len + 3);
	memcpy(param_dup, param, param_len);
	param_dup[param_len] = ':';
	param_dup[param_len+1] = ' ';
	param_dup[param_len+2] = '\0';

	if (strncasecmp(log, param_dup + 1, param_len + 1) == 0) {
		/* param found at start of string */
		p = log + param_len + 1;
	} else {
		p = strstr(log, param_dup);
		if (p) {
			/* param found further in */
			p += param_len + 2;
		}
	}

	if (!p) {
		/* parameter not found */
		efree(param_dup);
		return NULL;
	}

	e = strchr(p, ';');
	if (!e) {
		/* last parameter */
		e = p + strlen(p);
	}

	val = estrndup(p, e - p);
	efree(param_dup);

	return val;
}

/* {{{ proto string cvsclient_log(resource cvsclient, string module, string filepath[, string revision])
	Retrieve log message for a particular revision */
PHP_FUNCTION(cvsclient_log)
{
	zval *zcvsclient, *currentlog = NULL;
	php_cvsclient_resource *cvsclient;
	char *p, *module, *path, *revision = NULL, response[4096];
	long module_len, path_len, revision_len = 0, filesize = 0, search_mode;
	smart_str logentry = {0};

	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rss|s", &zcvsclient, &module, &module_len, &path, &path_len, &revision, &revision_len) == FAILURE) {
		RETURN_FALSE;
	}

	ZEND_FETCH_RESOURCE(cvsclient, php_cvsclient_resource*, &zcvsclient, -1, CVSCLIENT_RES_NAME, le_cvsclient);

	if (path[0] == '/') {
		path++;
	}

	p = strrchr(path, '/');
	if (revision) {
		php_stream_printf(cvsclient->server TSRMLS_CC, "Argument -r\nArgument %s\n", revision);
	}

	if (p) {
		/* path is non-root */
		char save;

		save = path[path_len - (p - path)];
		path[path_len - (p - path)] = '\0';
		php_stream_printf(cvsclient->server TSRMLS_CC, 
			"Argument %s\n"
			"Directory .\n"
			"%s/%s/%s\n",
			p + 1,
			cvsclient->cvsroot, module, path);
		path[path_len - (p - path)] = save;
	} else {
		/* file is at root of cvs module */
		php_stream_printf(cvsclient->server TSRMLS_CC, 
			"Argument %s\n"
			"Directory .\n"
			"%s/%s\n", 
			path,
			cvsclient->cvsroot, module);
	}
	php_stream_write_string(cvsclient->server, "log\n");

	array_init(return_value);
	revision = NULL;
	search_mode = CVSCLIENT_DIVIDER;
	while (filesize == 0 && php_stream_gets(cvsclient->server, response, sizeof(response)-1) != NULL) {
		/* Parse response and look for data */
		if (strncasecmp(response, "error", 5) == 0) {
			break;
		}
		if ((search_mode == CVSCLIENT_DIVIDER || search_mode == CVSCLIENT_BODY)&&
			strlen(response) >= sizeof(PHP_CVSCLIENT_REV_DIVIDER) &&
			strncmp(response, PHP_CVSCLIENT_REV_DIVIDER, sizeof(PHP_CVSCLIENT_REV_DIVIDER) - 1) == 0 &&
			response[sizeof(PHP_CVSCLIENT_REV_DIVIDER)-1] != '-') {
			/* New log entry */
			if (currentlog) {
				if (logentry.len) {
					smart_str_0(&logentry);
					add_assoc_stringl(currentlog, "log", logentry.c, logentry.len, 0);
					logentry.c = NULL;
					logentry.len = 0;
				}
				if (revision) {
					add_assoc_zval(return_value, revision, currentlog);
					revision = NULL;
				} else {
					add_next_index_zval(return_value, currentlog);
				}
				currentlog = NULL;
			}
			search_mode = CVSCLIENT_REVISION;
		}
		if (search_mode == CVSCLIENT_BODY &&
			strncmp(response, PHP_CVSCLIENT_REV_DIVIDER2, sizeof(PHP_CVSCLIENT_REV_DIVIDER2) - 1) == 0) {
			break;
		} 


		if (search_mode == CVSCLIENT_BODY) {
			smart_str_appendl(&logentry, response + sizeof("M ") - 1, strlen(response) - sizeof("M ") +1);
		} 
		if (search_mode == CVSCLIENT_SIGNATURE &&
			strlen(response) > sizeof("M date: ") &&
			strncasecmp(response, "M date: ", sizeof("M date: ") - 1) == 0) {
			char *date, *author, *state, *lines;

			p = response + 2;

			if (!currentlog) {
				/* Probably not needed */
				ALLOC_INIT_ZVAL(currentlog);
				array_init(currentlog);
			}

			date = php_cvsclient_parse_log(p, "date" TSRMLS_CC);
			author = php_cvsclient_parse_log(p, "author" TSRMLS_CC);
			state = php_cvsclient_parse_log(p, "state" TSRMLS_CC);
			lines = php_cvsclient_parse_log(p, "lines" TSRMLS_CC);

			if (date) {
				zval *z;

				ALLOC_INIT_ZVAL(z);
				php_trim(date, strlen(date), NULL, 0, z, 2 TSRMLS_CC);
				add_assoc_zval(currentlog, "date", z);
				efree(date);
			}
			if (author) {
				zval *z;

				ALLOC_INIT_ZVAL(z);
				php_trim(author, strlen(author), NULL, 0, z, 2 TSRMLS_CC);
				add_assoc_zval(currentlog, "author", z);
				efree(author);
			}
			if (state) {
				zval *z;

				ALLOC_INIT_ZVAL(z);
				php_trim(state, strlen(state), NULL, 0, z, 2 TSRMLS_CC);
				add_assoc_zval(currentlog, "state", z);
				efree(state);
			}
			if (lines) {
				zval *z;

				ALLOC_INIT_ZVAL(z);
				php_trim(lines, strlen(lines), NULL, 0, z, 2 TSRMLS_CC);
				add_assoc_zval(currentlog, "lines", z);
				efree(lines);
			}
			search_mode = CVSCLIENT_BODY;
		}
		if (search_mode == CVSCLIENT_REVISION &&
			strlen(response) > sizeof("M revision ") &&
			strncmp(response, "M revision ", sizeof("M revision ") - 1) == 0) {
			zval *revval;

			ALLOC_INIT_ZVAL(revval);
			php_trim(response + sizeof("M revision ") - 1, strlen(response) - sizeof("M revision ") + 1, NULL, 0, revval, 2 TSRMLS_CC);
			revision = Z_STRVAL_P(revval); /* Used for hash key */
			
			if (!currentlog) {
				ALLOC_INIT_ZVAL(currentlog);
				array_init(currentlog);
			}

			add_assoc_zval(currentlog, "revision", revval);
			search_mode = CVSCLIENT_SIGNATURE;
		}
	}
	if (currentlog) {
		if (logentry.c) {
			smart_str_0(&logentry);
			add_assoc_stringl(currentlog, "log", logentry.c, logentry.len, 0);
		}
		if (revision) {
			add_assoc_zval(return_value, revision, currentlog);
		} else {
			add_next_index_zval(return_value, currentlog);
		}
	} else {
		if (logentry.c) {
			efree(logentry.c);
		}
	}

}
/* }}} */

/* ******************* */
/* Module Housekeeping */
/* ******************* */

function_entry cvsclient_functions[] = {
	PHP_FE(cvsclient_connect, 											NULL)
	PHP_FE(cvsclient_login, 											NULL)
	PHP_FE(cvsclient_retrieve,											NULL)
	PHP_FE(cvsclient_log,												NULL)
	/* TODO: Resource based single-session access and committment */
	{NULL, NULL, NULL}
};

zend_module_entry cvsclient_module_entry = {
	STANDARD_MODULE_HEADER,
	"cvsclient",
	cvsclient_functions,
	PHP_MINIT(cvsclient),
	PHP_MSHUTDOWN(cvsclient),
	NULL, /* RINIT */
	NULL, /* RSHUTDOWN */
	PHP_MINFO(cvsclient),
	NO_VERSION_YET,
	STANDARD_MODULE_PROPERTIES
};

#ifdef COMPILE_DL_CVSCLIENT
ZEND_GET_MODULE(cvsclient)
#endif

static void cvsclient_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC)
{
	php_cvsclient_resource *cvsclient = (php_cvsclient_resource *)rsrc->ptr;

	if (cvsclient) {
		if (cvsclient->server) {
			php_stream_close(cvsclient->server);
			cvsclient->server = NULL;
		}
		if (cvsclient->cvsroot) {
			efree(cvsclient->cvsroot);
			cvsclient->cvsroot = NULL;
		}
		efree(cvsclient);
		cvsclient = NULL;
		rsrc->ptr = NULL;
	}
}


PHP_MINIT_FUNCTION(cvsclient)
{
	/* TODO: Readonly support for cvs.log, cvs.history, etc... */
	le_cvsclient = zend_register_list_destructors_ex(cvsclient_dtor, NULL, CVSCLIENT_RES_NAME, module_number);

	if (php_register_url_stream_wrapper("cvs", &php_stream_cvsclient_wrapper TSRMLS_CC) == FAILURE) {
		return FAILURE;
	}
	if (php_register_url_stream_wrapper("cvs.diff", &php_stream_cvsclient_diff_wrapper TSRMLS_CC) == FAILURE) {
		return FAILURE;
	}

	return SUCCESS;
}

PHP_MSHUTDOWN_FUNCTION(cvsclient)
{
	if (php_unregister_url_stream_wrapper("cvs" TSRMLS_CC) == FAILURE) {
		return FAILURE;
	}
	if (php_unregister_url_stream_wrapper("cvs.diff" TSRMLS_CC) == FAILURE) {
		return FAILURE;
	}

	return SUCCESS;
}


PHP_MINFO_FUNCTION(cvsclient)
{
	php_info_print_table_start();
	php_info_print_table_row(2, "CVS Client", "enabled");
	php_info_print_table_row(2, "Wrapper", "cvs://, cvs.diff://");
	php_info_print_table_row(2, "Resource", CVSCLIENT_RES_NAME);
	php_info_print_table_end();
}

#endif

/*
 * Local variables:
 * tab-width: 4
 * c-basic-offset: 4
 * End:
 * vim600: sw=4 ts=4 fdm=marker
 * vim<600: sw=4 ts=4
 */


syntax highlighted by Code2HTML, v. 0.9.1