/*
 * Property Service contexts backend for labeling Android
 * property keys
 */

#include <stdarg.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "callbacks.h"
#include "label_internal.h"

/* A property security context specification. */
typedef struct spec {
	struct selabel_lookup_rec lr;	/* holds contexts for lookup result */
	char *property_key;		/* property key string */
} spec_t;

/* Our stored configuration */
struct saved_data {
	/*
	 * The array of specifications is sorted for longest
	 * prefix match
	 */
	spec_t *spec_arr;
	unsigned int nspec;	/* total number of specifications */
};

static int cmp(const void *A, const void *B)
{
	const struct spec *sp1 = A, *sp2 = B;

	if (strncmp(sp1->property_key, "*", 1) == 0)
		return 1;
	if (strncmp(sp2->property_key, "*", 1) == 0)
		return -1;

	size_t L1 = strlen(sp1->property_key);
	size_t L2 = strlen(sp2->property_key);

	return (L1 < L2) - (L1 > L2);
}

/*
 * Warn about duplicate specifications. Return error on different specifications.
 * TODO: Remove duplicate specifications. Move duplicate check to after sort
 * to improve performance.
 */
static int nodups_specs(struct saved_data *data)
{
	int rc = 0;
	unsigned int ii, jj;
	struct spec *curr_spec, *spec_arr = data->spec_arr;

	for (ii = 0; ii < data->nspec; ii++) {
		curr_spec = &spec_arr[ii];
		for (jj = ii + 1; jj < data->nspec; jj++) {
			if (!strcmp(spec_arr[jj].property_key,
					    curr_spec->property_key)) {
				if (strcmp(spec_arr[jj].lr.ctx_raw,
						    curr_spec->lr.ctx_raw)) {
					rc = -1;
					errno = EINVAL;
					selinux_log
						(SELINUX_ERROR,
						 "Multiple different specifications for %s  (%s and %s).\n",
						 curr_spec->property_key,
						 spec_arr[jj].lr.ctx_raw,
						 curr_spec->lr.ctx_raw);
				} else {
					selinux_log
						(SELINUX_WARNING,
						 "Multiple same specifications for %s.\n",
						 curr_spec->property_key);
				}
			}
		}
	}
	return rc;
}

static int process_line(struct selabel_handle *rec,
			const char *path, char *line_buf,
			int pass, unsigned lineno)
{
	int items;
	char *prop = NULL, *context = NULL;
	struct saved_data *data = (struct saved_data *)rec->data;
	spec_t *spec_arr = data->spec_arr;
	unsigned int nspec = data->nspec;
	const char *errbuf = NULL;

	items = read_spec_entries(line_buf, &errbuf, 2, &prop, &context);
	if (items < 0) {
		items = errno;
		selinux_log(SELINUX_ERROR,
			"%s:  line %u error due to: %s\n", path,
			lineno, errbuf ?: strerror(errno));
		errno = items;
		return -1;
	}

	if (items == 0)
		return items;

	if (items != 2) {
		selinux_log(SELINUX_ERROR,
			    "%s:  line %u is missing fields\n", path,
			    lineno);
		free(prop);
		errno = EINVAL;
		return -1;
	}

	if (pass == 0) {
		free(prop);
		free(context);
	} else if (pass == 1) {
		/* On the second pass, process and store the specification in spec. */
		spec_arr[nspec].property_key = prop;
		spec_arr[nspec].lr.ctx_raw = context;

		if (rec->validating) {
			if (selabel_validate(rec, &spec_arr[nspec].lr) < 0) {
				selinux_log(SELINUX_ERROR,
					    "%s:  line %u has invalid context %s\n",
					    path, lineno, spec_arr[nspec].lr.ctx_raw);
				errno = EINVAL;
				return -1;
			}
		}

		data->nspec = ++nspec;
	}

	return 0;
}

static int process_file(struct selabel_handle *rec, const char *path)
{
	struct saved_data *data = (struct saved_data *)rec->data;
	char line_buf[BUFSIZ];
	unsigned int lineno, maxnspec, pass;
	struct stat sb;
	FILE *fp;
	int status = -1;
	unsigned int nspec;
	spec_t *spec_arr;

	/* Open the specification file. */
	if ((fp = fopen(path, "re")) == NULL)
		return -1;

	if (fstat(fileno(fp), &sb) < 0)
		goto finish;

	errno = EINVAL;

	if (!S_ISREG(sb.st_mode))
		goto finish;

	/*
	 * Two passes per specification file. First is to get the size.
	 * After the first pass, the spec array is malloced / realloced to
	 * the appropriate size. Second pass is to populate the spec array.
	 */
	maxnspec = UINT_MAX / sizeof(spec_t);
	for (pass = 0; pass < 2; pass++) {
		nspec = 0;
		lineno = 0;

		while (fgets(line_buf, sizeof(line_buf) - 1, fp) &&
			nspec < maxnspec) {
			if (process_line(rec, path, line_buf, pass, ++lineno))
				goto finish;
			nspec++;
		}

		if (pass == 0) {
			if (nspec == 0) {
				status = 0;
				goto finish;
			}

			/* grow spec array if required */
			spec_arr = realloc(data->spec_arr,
					(data->nspec + nspec) * sizeof(spec_t));
			if (spec_arr == NULL)
				goto finish;

			memset(&spec_arr[data->nspec], 0, nspec * sizeof(spec_t));
			data->spec_arr = spec_arr;
			maxnspec = nspec;
			rewind(fp);
		}
	}

	status = digest_add_specfile(rec->digest, fp, NULL, sb.st_size, path);

finish:
	fclose(fp);
	return status;
}

static void closef(struct selabel_handle *rec);

static int init(struct selabel_handle *rec, const struct selinux_opt *opts,
		unsigned n)
{
	struct saved_data *data = (struct saved_data *)rec->data;
	char **paths = NULL;
	size_t num_paths = 0;
	int status = -1;
	size_t i;

	/* Process arguments */
	i = n;
	while (i--) {
		switch (opts[i].type) {
		case SELABEL_OPT_PATH:
			num_paths++;
			break;
		}
	}

	if (!num_paths)
		return -1;

	paths = calloc(num_paths, sizeof(*paths));
	if (!paths)
		return -1;

	rec->spec_files = paths;
	rec->spec_files_len = num_paths;

	i = n;
	while (i--) {
		switch(opts[i].type) {
		case SELABEL_OPT_PATH:
			*paths = strdup(opts[i].value);
			if (*paths == NULL)
				goto finish;
			paths++;
		}
	}

	for (i = 0; i < num_paths; i++) {
		status = process_file(rec, rec->spec_files[i]);
		if (status)
			goto finish;
	}

	/* warn about duplicates after all files have been processed. */
	status = nodups_specs(data);
	if (status)
		goto finish;

	qsort(data->spec_arr, data->nspec, sizeof(struct spec), cmp);

	digest_gen_hash(rec->digest);

finish:
	if (status)
		closef(rec);

	return status;
}

/*
 * Backend interface routines
 */
static void closef(struct selabel_handle *rec)
{
	struct saved_data *data = (struct saved_data *)rec->data;
	struct spec *spec;
	unsigned int i;

	if (data->spec_arr) {
		for (i = 0; i < data->nspec; i++) {
			spec = &data->spec_arr[i];
			free(spec->property_key);
			free(spec->lr.ctx_raw);
			free(spec->lr.ctx_trans);
		}

		free(data->spec_arr);
	}

	free(data);
}

static struct selabel_lookup_rec *property_lookup(struct selabel_handle *rec,
					 const char *key,
					 int __attribute__((unused)) type)
{
	struct saved_data *data = (struct saved_data *)rec->data;
	spec_t *spec_arr = data->spec_arr;
	unsigned int i;
	struct selabel_lookup_rec *ret = NULL;

	if (!data->nspec) {
		errno = ENOENT;
		goto finish;
	}

	for (i = 0; i < data->nspec; i++) {
		if (strncmp(spec_arr[i].property_key, key,
			    strlen(spec_arr[i].property_key)) == 0) {
			break;
		}
		if (strncmp(spec_arr[i].property_key, "*", 1) == 0)
			break;
	}

	if (i >= data->nspec) {
		/* No matching specification. */
		errno = ENOENT;
		goto finish;
	}

	ret = &spec_arr[i].lr;

finish:
	return ret;
}

static struct selabel_lookup_rec *service_lookup(struct selabel_handle *rec,
		const char *key, int __attribute__((unused)) type)
{
	struct saved_data *data = (struct saved_data *)rec->data;
	spec_t *spec_arr = data->spec_arr;
	unsigned int i;
	struct selabel_lookup_rec *ret = NULL;

	if (!data->nspec) {
		errno = ENOENT;
		goto finish;
	}

	for (i = 0; i < data->nspec; i++) {
		if (strcmp(spec_arr[i].property_key, key) == 0)
			break;
		if (strcmp(spec_arr[i].property_key, "*") == 0)
			break;
	}

	if (i >= data->nspec) {
		/* No matching specification. */
		errno = ENOENT;
		goto finish;
	}

	ret = &spec_arr[i].lr;

finish:
	return ret;
}

static void stats(struct selabel_handle __attribute__((unused)) *rec)
{
	selinux_log(SELINUX_WARNING, "'stats' functionality not implemented.\n");
}

int selabel_property_init(struct selabel_handle *rec,
			  const struct selinux_opt *opts,
			  unsigned nopts)
{
	struct saved_data *data;

	data = (struct saved_data *)calloc(1, sizeof(*data));
	if (!data)
		return -1;

	rec->data = data;
	rec->func_close = &closef;
	rec->func_stats = &stats;
	rec->func_lookup = &property_lookup;

	return init(rec, opts, nopts);
}

int selabel_service_init(struct selabel_handle *rec,
		const struct selinux_opt *opts, unsigned nopts)
{
	struct saved_data *data;

	data = (struct saved_data *)calloc(1, sizeof(*data));
	if (!data)
		return -1;

	rec->data = data;
	rec->func_close = &closef;
	rec->func_stats = &stats;
	rec->func_lookup = &service_lookup;

	return init(rec, opts, nopts);
}
