remote-ext.c   [plain text]


#include "builtin.h"
#include "transport.h"
#include "run-command.h"

/*
 * URL syntax:
 *	'command [arg1 [arg2 [...]]]'	Invoke command with given arguments.
 *	Special characters:
 *	'% ': Literal space in argument.
 *	'%%': Literal percent sign.
 *	'%S': Name of service (git-upload-pack/git-upload-archive/
 *		git-receive-pack.
 *	'%s': Same as \s, but with possible git- prefix stripped.
 *	'%G': Only allowed as first 'character' of argument. Do not pass this
 *		Argument to command, instead send this as name of repository
 *		in in-line git://-style request (also activates sending this
 *		style of request).
 *	'%V': Only allowed as first 'character' of argument. Used in
 *		conjunction with '%G': Do not pass this argument to command,
 *		instead send this as vhost in git://-style request (note: does
 *		not activate sending git:// style request).
 */

static char *git_req;
static char *git_req_vhost;

static char *strip_escapes(const char *str, const char *service,
	const char **next)
{
	size_t rpos = 0;
	int escape = 0;
	char special = 0;
	size_t psoff = 0;
	struct strbuf ret = STRBUF_INIT;

	/* Calculate prefix length for \s and lengths for \s and \S */
	if (!strncmp(service, "git-", 4))
		psoff = 4;

	/* Pass the service to command. */
	setenv("GIT_EXT_SERVICE", service, 1);
	setenv("GIT_EXT_SERVICE_NOPREFIX", service + psoff, 1);

	/* Scan the length of argument. */
	while (str[rpos] && (escape || str[rpos] != ' ')) {
		if (escape) {
			switch (str[rpos]) {
			case ' ':
			case '%':
			case 's':
			case 'S':
				break;
			case 'G':
			case 'V':
				special = str[rpos];
				if (rpos == 1)
					break;
				/* Fall-through to error. */
			default:
				die("Bad remote-ext placeholder '%%%c'.",
					str[rpos]);
			}
			escape = 0;
		} else
			escape = (str[rpos] == '%');
		rpos++;
	}
	if (escape && !str[rpos])
		die("remote-ext command has incomplete placeholder");
	*next = str + rpos;
	if (**next == ' ')
		++*next;	/* Skip over space */

	/*
	 * Do the actual placeholder substitution. The string will be short
	 * enough not to overflow integers.
	 */
	rpos = special ? 2 : 0;		/* Skip first 2 bytes in specials. */
	escape = 0;
	while (str[rpos] && (escape || str[rpos] != ' ')) {
		if (escape) {
			switch (str[rpos]) {
			case ' ':
			case '%':
				strbuf_addch(&ret, str[rpos]);
				break;
			case 's':
				strbuf_addstr(&ret, service + psoff);
				break;
			case 'S':
				strbuf_addstr(&ret, service);
				break;
			}
			escape = 0;
		} else
			switch (str[rpos]) {
			case '%':
				escape = 1;
				break;
			default:
				strbuf_addch(&ret, str[rpos]);
				break;
			}
		rpos++;
	}
	switch (special) {
	case 'G':
		git_req = strbuf_detach(&ret, NULL);
		return NULL;
	case 'V':
		git_req_vhost = strbuf_detach(&ret, NULL);
		return NULL;
	default:
		return strbuf_detach(&ret, NULL);
	}
}

/* Should be enough... */
#define MAXARGUMENTS 256

static const char **parse_argv(const char *arg, const char *service)
{
	int arguments = 0;
	int i;
	const char **ret;
	char *temparray[MAXARGUMENTS + 1];

	while (*arg) {
		char *expanded;
		if (arguments == MAXARGUMENTS)
			die("remote-ext command has too many arguments");
		expanded = strip_escapes(arg, service, &arg);
		if (expanded)
			temparray[arguments++] = expanded;
	}

	ret = xmalloc((arguments + 1) * sizeof(char *));
	for (i = 0; i < arguments; i++)
		ret[i] = temparray[i];
	ret[arguments] = NULL;
	return ret;
}

static void send_git_request(int stdin_fd, const char *serv, const char *repo,
	const char *vhost)
{
	size_t bufferspace;
	size_t wpos = 0;
	char *buffer;

	/*
	 * Request needs 12 bytes extra if there is vhost (xxxx \0host=\0) and
	 * 6 bytes extra (xxxx \0) if there is no vhost.
	 */
	if (vhost)
		bufferspace = strlen(serv) + strlen(repo) + strlen(vhost) + 12;
	else
		bufferspace = strlen(serv) + strlen(repo) + 6;

	if (bufferspace > 0xFFFF)
		die("Request too large to send");
	buffer = xmalloc(bufferspace);

	/* Make the packet. */
	wpos = sprintf(buffer, "%04x%s %s%c", (unsigned)bufferspace,
		serv, repo, 0);

	/* Add vhost if any. */
	if (vhost)
		sprintf(buffer + wpos, "host=%s%c", vhost, 0);

	/* Send the request */
	if (write_in_full(stdin_fd, buffer, bufferspace) < 0)
		die_errno("Failed to send request");

	free(buffer);
}

static int run_child(const char *arg, const char *service)
{
	int r;
	struct child_process child;

	memset(&child, 0, sizeof(child));
	child.in = -1;
	child.out = -1;
	child.err = 0;
	child.argv = parse_argv(arg, service);

	if (start_command(&child) < 0)
		die("Can't run specified command");

	if (git_req)
		send_git_request(child.in, service, git_req, git_req_vhost);

	r = bidirectional_transfer_loop(child.out, child.in);
	if (!r)
		r = finish_command(&child);
	else
		finish_command(&child);
	return r;
}

#define MAXCOMMAND 4096

static int command_loop(const char *child)
{
	char buffer[MAXCOMMAND];

	while (1) {
		size_t i;
		if (!fgets(buffer, MAXCOMMAND - 1, stdin)) {
			if (ferror(stdin))
				die("Comammand input error");
			exit(0);
		}
		/* Strip end of line characters. */
		i = strlen(buffer);
		while (i > 0 && isspace(buffer[i - 1]))
			buffer[--i] = 0;

		if (!strcmp(buffer, "capabilities")) {
			printf("*connect\n\n");
			fflush(stdout);
		} else if (!strncmp(buffer, "connect ", 8)) {
			printf("\n");
			fflush(stdout);
			return run_child(child, buffer + 8);
		} else {
			fprintf(stderr, "Bad command");
			return 1;
		}
	}
}

int cmd_remote_ext(int argc, const char **argv, const char *prefix)
{
	if (argc != 3)
		die("Expected two arguments");

	return command_loop(argv[2]);
}