/*
 * Copyright (C) 2016 The CyanogenMod Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <fcntl.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <strings.h>
#include <sys/file.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <utime.h>
#include <pthread.h>

#include <netinet/in6.h>
#include <arpa/inet.h>

#include "hostent.h"
#include "resolv_private.h"

#define MAX_ADDRLEN	(INET6_ADDRSTRLEN - (1 + 5))
#define MAX_HOSTLEN	MAXHOSTNAMELEN

#define ESTIMATED_LINELEN	32
#define HCFILE_ALLOC_SIZE	256

/* From sethostent.c */
#define ALIGNBYTES	(sizeof(uintptr_t) - 1)
#define ALIGN(p)	(((uintptr_t)(p) + ALIGNBYTES) &~ ALIGNBYTES)

/*
 * Host cache entry for hcfile.c_data.
 * Offsets are into hcfile.h_data.
 * Strings are *not* terminated by NULL, but by whitespace (isspace) or '#'.
 * Use hstr* functions with these.
 */
struct hcent
{
	uint32_t	addr;
	uint32_t	name;
};

/*
 * Overall host cache file state.
 */
struct hcfile
{
	int		h_fd;
	struct stat	h_st;
	char		*h_data;

	uint32_t	c_alloc;
	uint32_t	c_len;
	struct hcent	*c_data;
};
static struct hcfile hcfile;
static pthread_mutex_t hclock = PTHREAD_MUTEX_INITIALIZER;

static size_t hstrlen(const char *s)
{
	const char *p = s;
	while (*p && *p != '#' && !isspace(*p))
		++p;
	return p - s;
}

static int hstrcmp(const char *a, const char *b)
{
	size_t alen = hstrlen(a);
	size_t blen = hstrlen(b);
	int res = strncmp(a, b, MIN(alen, blen));
	if (res == 0)
		res = alen - blen;
	return res;
}

static char *hstrcpy(char *dest, const char *src)
{
	size_t len = hstrlen(src);
	memcpy(dest, src, len);
	dest[len] = '\0';
	return dest;
}

static char *hstrdup(const char *s)
{
	size_t len = hstrlen(s);
	char *dest = (char *)malloc(len + 1);
	if (!dest)
		return NULL;
	memcpy(dest, s, len);
	dest[len] = '\0';
	return dest;
}

static int cmp_hcent_name(const void *a, const void *b)
{
	struct hcent *ea = (struct hcent *)a;
	const char *na = hcfile.h_data + ea->name;
	struct hcent *eb = (struct hcent *)b;
	const char *nb = hcfile.h_data + eb->name;

	return hstrcmp(na, nb);
}

static struct hcent *_hcfindname(const char *name)
{
	size_t first, last, mid;
	struct hcent *cur = NULL;
	int cmp;

	if (hcfile.c_len == 0)
		return NULL;

	first = 0;
	last = hcfile.c_len - 1;
	mid = (first + last) / 2;
	while (first <= last) {
		cur = hcfile.c_data + mid;
		cmp = hstrcmp(hcfile.h_data + cur->name, name);
		if (cmp == 0)
			goto found;
		if (cmp < 0)
			first = mid + 1;
		else {
			if (mid > 0)
				last = mid - 1;
			else
				return NULL;
		}
		mid = (first + last) / 2;
	}
	return NULL;

found:
	while (cur > hcfile.c_data) {
		struct hcent *prev = cur - 1;
		cmp = cmp_hcent_name(cur, prev);
		if (cmp)
			break;
		cur = prev;
	}

	return cur;
}

/*
 * Find next name on line, if any.
 *
 * Assumes that line is terminated by LF.
 */
static const char *_hcnextname(const char *name)
{
	while (!isspace(*name)) {
		if (*name == '#')
			return NULL;
		++name;
	}
	while (isspace(*name)) {
		if (*name == '\n')
			return NULL;
		++name;
	}
	if (*name == '#')
		return NULL;
	return name;
}

static int _hcfilemmap(void)
{
	struct stat st;
	int h_fd;
	char *h_addr;
	const char *p, *pend;
	uint32_t c_alloc;

	h_fd = open(_PATH_HOSTS, O_RDONLY);
	if (h_fd < 0)
		return -1;
	if (flock(h_fd, LOCK_EX) != 0) {
		close(h_fd);
		return -1;
	}

	if (hcfile.h_data) {
		memset(&st, 0, sizeof(st));
		if (fstat(h_fd, &st) == 0) {
			if (st.st_size == hcfile.h_st.st_size &&
			    st.st_mtime == hcfile.h_st.st_mtime) {
				flock(h_fd, LOCK_UN);
				close(h_fd);
				return 0;
			}
		}
		free(hcfile.c_data);
		munmap(hcfile.h_data, hcfile.h_st.st_size);
		close(hcfile.h_fd);
		memset(&hcfile, 0, sizeof(struct hcfile));
	}

	if (fstat(h_fd, &st) != 0) {
		flock(h_fd, LOCK_UN);
		close(h_fd);
		return -1;
	}
	h_addr = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, h_fd, 0);
	if (h_addr == MAP_FAILED) {
		flock(h_fd, LOCK_UN);
		close(h_fd);
		return -1;
	}

	hcfile.h_fd = h_fd;
	hcfile.h_st = st;
	hcfile.h_data = h_addr;

	c_alloc = 0;
	/*
	 * Do an initial allocation if the file is "large".  Estimate
	 * 32 bytes per line and define "large" as more than half of
	 * the alloc growth size (256 entries).
	 */
	if (st.st_size >= ESTIMATED_LINELEN * HCFILE_ALLOC_SIZE / 2) {
		c_alloc = st.st_size / ESTIMATED_LINELEN;
		hcfile.c_data = malloc(c_alloc * sizeof(struct hcent));
		if (!hcfile.c_data) {
			goto oom;
		}
	}

	p = (const char *)h_addr;
	pend = p + st.st_size;
	while (p < pend) {
		const char *eol, *addr, *name;
		size_t len;
		addr = p;
		eol = memchr(p, '\n', pend - p);
		if (!eol)
			break;
		p = eol + 1;
		if (*addr == '#' || *addr == '\n')
			continue;
		len = hstrlen(addr);
		if (len > MAX_ADDRLEN)
			continue;
		name = addr + len;
		while (name < eol && isspace(*name))
			++name;
		while (name < eol) {
			len = hstrlen(name);
			if (len == 0)
				break;
			if (len < MAX_HOSTLEN) {
				struct hcent *ent;
				if (c_alloc <= hcfile.c_len) {
					struct hcent *c_data;
					c_alloc += HCFILE_ALLOC_SIZE;
					c_data = realloc(hcfile.c_data, c_alloc * sizeof(struct hcent));
					if (!c_data) {
						goto oom;
					}
					hcfile.c_data = c_data;
				}
				ent = hcfile.c_data + hcfile.c_len;
				ent->addr = addr - h_addr;
				ent->name = name - h_addr;
				++hcfile.c_len;
			}
			name += len;
			while (name < eol && isspace(*name))
				++name;
		}
	}

	qsort(hcfile.c_data, hcfile.c_len,
	    sizeof(struct hcent), cmp_hcent_name);

	flock(h_fd, LOCK_UN);

	return 0;

oom:
	free(hcfile.c_data);
	munmap(hcfile.h_data, hcfile.h_st.st_size);
	flock(hcfile.h_fd, LOCK_UN);
	close(hcfile.h_fd);
	memset(&hcfile, 0, sizeof(struct hcfile));
	return -1;
}

/*
 * Caching version of getaddrinfo.
 *
 * If we find the requested host name in the cache, use getaddrinfo to
 * populate the result for each address we find.
 *
 * Note glibc and bionic differ in the handling of ai_canonname.  POSIX
 * says that ai_canonname is only populated in the first result entry.
 * glibc does this.  bionic populates ai_canonname in all result entries.
 * We choose the POSIX/glibc way here.
 */
int hc_getaddrinfo(const char *host, const char *service,
		   const struct addrinfo *hints,
		   struct addrinfo **result)
{
	int ret = 0;
	struct hcent *ent, *cur;
	struct addrinfo *ai;
	struct addrinfo rhints;
	struct addrinfo *last;
	int canonname = 0;
	int cmp;

	if (getenv("ANDROID_HOSTS_CACHE_DISABLE") != NULL)
		return EAI_SYSTEM;

	/* Avoid needless work and recursion */
	if (hints && (hints->ai_flags & AI_NUMERICHOST))
		return EAI_SYSTEM;
	if (!host)
		return EAI_SYSTEM;

	pthread_mutex_lock(&hclock);

	if (_hcfilemmap() != 0) {
		ret = EAI_SYSTEM;
		goto out;
	}
	ent = _hcfindname(host);
	if (!ent) {
		ret = EAI_NONAME;
		goto out;
	}

	if (hints) {
		canonname = (hints->ai_flags & AI_CANONNAME);
		memcpy(&rhints, hints, sizeof(rhints));
		rhints.ai_flags &= ~AI_CANONNAME;
	}
	else {
		memset(&rhints, 0, sizeof(rhints));
	}
	rhints.ai_flags |= AI_NUMERICHOST;

	last = NULL;
	cur = ent;
	do {
		char addrstr[MAX_ADDRLEN];
		struct addrinfo *res;

		hstrcpy(addrstr, hcfile.h_data + cur->addr);

		if (getaddrinfo(addrstr, service, &rhints, &res) == 0) {
			if (!last)
				(*result)->ai_next = res;
			else
				last->ai_next = res;
			last = res;
			while (last->ai_next)
				last = last->ai_next;
		}

		if(cur + 1 >= hcfile.c_data + hcfile.c_len)
			break;
		cmp = cmp_hcent_name(cur, cur + 1);
		cur = cur + 1;
	}
	while (!cmp);

	if (last == NULL) {
		/* This check is equivalent to (*result)->ai_next == NULL */
		ret = EAI_NODATA;
		goto out;
	}

	if (canonname) {
		ai = (*result)->ai_next;
		free(ai->ai_canonname);
		ai->ai_canonname = hstrdup(hcfile.h_data + ent->name);
	}

out:
	pthread_mutex_unlock(&hclock);
	return ret;
}

/*
 * Caching version of gethtbyname.
 *
 * Note glibc and bionic differ in the handling of aliases.  glibc returns
 * all aliases for all entries, regardless of whether they match h_addrtype.
 * bionic returns only the aliases for the first hosts entry.  We return all
 * aliases for all IPv4 entries.
 *
 * Additionally, if an alias is IPv6 and the primary name for an alias also
 * has an IPv4 entry, glibc will return the IPv4 address(es), but bionic
 * will not.  Neither do we.
 */
int hc_gethtbyname(const char *host, int af, struct getnamaddr *info)
{
	int ret = NETDB_SUCCESS;
	struct hcent *ent, *cur;
	int cmp;
	size_t addrlen;
	unsigned int naliases = 0;
	char *aliases[MAXALIASES];
	unsigned int naddrs = 0;
	char *addr_ptrs[MAXADDRS];
	unsigned int n;

	if (getenv("ANDROID_HOSTS_CACHE_DISABLE") != NULL)
		return NETDB_INTERNAL;

	switch (af) {
	case AF_INET:  addrlen = NS_INADDRSZ;  break;
	case AF_INET6: addrlen = NS_IN6ADDRSZ; break;
	default:
		return NETDB_INTERNAL;
	}

	pthread_mutex_lock(&hclock);

	if (_hcfilemmap() != 0) {
		ret = NETDB_INTERNAL;
		goto out;
	}

	ent = _hcfindname(host);
	if (!ent) {
		ret = HOST_NOT_FOUND;
		goto out;
	}

	cur = ent;
	do {
		char addr[16];
		char addrstr[MAX_ADDRLEN];
		char namestr[MAX_HOSTLEN];
		const char *name;

		hstrcpy(addrstr, hcfile.h_data + cur->addr);
		if (inet_pton(af, addrstr, &addr) == 1) {
			char *aligned;
			/* First match is considered the official hostname */
			if (naddrs == 0) {
				hstrcpy(namestr, hcfile.h_data + cur->name);
				HENT_SCOPY(info->hp->h_name, namestr, info->buf, info->buflen);
			}
			for (name = hcfile.h_data + cur->name; name; name = _hcnextname(name)) {
				if (!hstrcmp(name, host))
					continue;
				hstrcpy(namestr, name);
				HENT_SCOPY(aliases[naliases], namestr, info->buf, info->buflen);
				++naliases;
				if (naliases >= MAXALIASES)
					goto nospc;
			}
			aligned = (char *)ALIGN(info->buf);
			if (info->buf != aligned) {
				if ((ptrdiff_t)info->buflen < (aligned - info->buf))
					goto nospc;
				info->buflen -= (aligned - info->buf);
				info->buf = aligned;
			}
			HENT_COPY(addr_ptrs[naddrs], addr, addrlen, info->buf, info->buflen);
			++naddrs;
			if (naddrs >= MAXADDRS)
				goto nospc;
		}

		if(cur + 1 >= hcfile.c_data + hcfile.c_len)
			break;
		cmp = cmp_hcent_name(cur, cur + 1);
		cur = cur + 1;
	}
	while (!cmp);

	if (naddrs == 0) {
		ret = HOST_NOT_FOUND;
		goto out;
	}

	addr_ptrs[naddrs++] = NULL;
	aliases[naliases++] = NULL;

	/* hp->h_name already populated */
	HENT_ARRAY(info->hp->h_aliases, naliases, info->buf, info->buflen);
	for (n = 0; n < naliases; ++n) {
		info->hp->h_aliases[n] = aliases[n];
	}
	info->hp->h_addrtype = af;
	info->hp->h_length = addrlen;
	HENT_ARRAY(info->hp->h_addr_list, naddrs, info->buf, info->buflen);
	for (n = 0; n < naddrs; ++n) {
		info->hp->h_addr_list[n] = addr_ptrs[n];
	}

out:
	pthread_mutex_unlock(&hclock);
	*info->he = ret;
	return ret;

nospc:
	ret = NETDB_INTERNAL;
	goto out;
}
