/*
 * Copyright (C) the libgit2 contributors. All rights reserved.
 *
 * This file is part of libgit2, distributed under the GNU GPL v2 with
 * a Linking Exception. For full terms see the included COPYING file.
 */

#include "netops.h"

#include <ctype.h>
#include "git2/errors.h"

#include "posix.h"
#include "buffer.h"
#include "http_parser.h"
#include "global.h"

int gitno_recv(gitno_buffer *buf)
{
	return buf->recv(buf);
}

void gitno_buffer_setup_callback(
	gitno_buffer *buf,
	char *data,
	size_t len,
	int (*recv)(gitno_buffer *buf), void *cb_data)
{
	memset(data, 0x0, len);
	buf->data = data;
	buf->len = len;
	buf->offset = 0;
	buf->recv = recv;
	buf->cb_data = cb_data;
}

static int recv_stream(gitno_buffer *buf)
{
	git_stream *io = (git_stream *) buf->cb_data;
	size_t readlen = buf->len - buf->offset;
	ssize_t ret;

	readlen = min(readlen, INT_MAX);

	ret = git_stream_read(io, buf->data + buf->offset, (int)readlen);
	if (ret < 0)
		return -1;

	buf->offset += ret;
	return (int)ret;
}

void gitno_buffer_setup_fromstream(git_stream *st, gitno_buffer *buf, char *data, size_t len)
{
	memset(data, 0x0, len);
	buf->data = data;
	buf->len = len;
	buf->offset = 0;
	buf->recv = recv_stream;
	buf->cb_data = st;
}

/* Consume up to ptr and move the rest of the buffer to the beginning */
void gitno_consume(gitno_buffer *buf, const char *ptr)
{
	size_t consumed;

	assert(ptr - buf->data >= 0);
	assert(ptr - buf->data <= (int) buf->len);

	consumed = ptr - buf->data;

	memmove(buf->data, ptr, buf->offset - consumed);
	memset(buf->data + buf->offset, 0x0, buf->len - buf->offset);
	buf->offset -= consumed;
}

/* Consume const bytes and move the rest of the buffer to the beginning */
void gitno_consume_n(gitno_buffer *buf, size_t cons)
{
	memmove(buf->data, buf->data + cons, buf->len - buf->offset);
	memset(buf->data + cons, 0x0, buf->len - buf->offset);
	buf->offset -= cons;
}

/* Match host names according to RFC 2818 rules */
int gitno__match_host(const char *pattern, const char *host)
{
	for (;;) {
		char c = git__tolower(*pattern++);

		if (c == '\0')
			return *host ? -1 : 0;

		if (c == '*') {
			c = *pattern;
			/* '*' at the end matches everything left */
			if (c == '\0')
				return 0;

	/*
	 * We've found a pattern, so move towards the next matching
	 * char. The '.' is handled specially because wildcards aren't
	 * allowed to cross subdomains.
	 */

			while(*host) {
				char h = git__tolower(*host);
				if (c == h)
					return gitno__match_host(pattern, host++);
				if (h == '.')
					return gitno__match_host(pattern, host);
				host++;
			}
			return -1;
		}

		if (c != git__tolower(*host++))
			return -1;
	}

	return -1;
}

int gitno_connection_data_handle_redirect(
		git_net_url *url,
		const char *redirect_str,
		const char *service_suffix)
{
	git_net_url tmp = GIT_NET_URL_INIT;
	int error = 0;

	assert(url && redirect_str);

	if (redirect_str[0] == '/') {
		git__free(url->path);

		if ((url->path = git__strdup(redirect_str)) == NULL) {
			error = -1;
			goto done;
		}
	} else {
		git_net_url *original = url;

		if ((error = git_net_url_parse(&tmp, redirect_str)) < 0)
			goto done;

		/* Validate that this is a legal redirection */

		if (original->scheme &&
		    strcmp(original->scheme, tmp.scheme) != 0 &&
			strcmp(tmp.scheme, "https") != 0) {
			git_error_set(GIT_ERROR_NET, "cannot redirect from '%s' to '%s'",
				original->scheme, tmp.scheme);

			error = -1;
			goto done;
		}

		if (original->host &&
		    git__strcasecmp(original->host, tmp.host) != 0) {
			git_error_set(GIT_ERROR_NET, "cannot redirect from '%s' to '%s'",
				original->host, tmp.host);

			error = -1;
			goto done;
		}

		git_net_url_swap(url, &tmp);
	}

	/* Remove the service suffix if it was given to us */
	if (service_suffix) {
		/*
		 * Some servers strip the query parameters from the Location header
		 * when sending a redirect. Others leave it in place.
		 * Check for both, starting with the stripped case first,
		 * since it appears to be more common.
		 */
		const char *service_query = strchr(service_suffix, '?');
		size_t full_suffix_len = strlen(service_suffix);
		size_t suffix_len = service_query ?
			(size_t)(service_query - service_suffix) : full_suffix_len;
		size_t path_len = strlen(url->path);
		ssize_t truncate = -1;

		/* Check for a redirect without query parameters, like "/newloc/info/refs" */
		if (suffix_len && path_len >= suffix_len) {
			size_t suffix_offset = path_len - suffix_len;

			if (git__strncmp(url->path + suffix_offset, service_suffix, suffix_len) == 0 &&
			    (!service_query || git__strcmp(url->query, service_query + 1) == 0)) {
				truncate = suffix_offset;
			}
		}

		/*
		 * If we haven't already found where to truncate to remove the suffix,
		 * check for a redirect with query parameters,
		 * like "/newloc/info/refs?service=git-upload-pack"
		 */
		if (truncate == -1 && git__suffixcmp(url->path, service_suffix) == 0) {
			truncate = path_len - full_suffix_len;
		}

		if (truncate >= 0) {
			/* Ensure we leave a minimum of '/' as the path */
			if (truncate == 0)
				truncate++;
			url->path[truncate] = '\0';

			git__free(url->query);
			url->query = NULL;
		}
	}

done:
	git_net_url_dispose(&tmp);
	return error;
}

