/*
 * File contexts backend for labeling system
 *
 * Author : Eamon Walsh <ewalsh@tycho.nsa.gov>
 * Author : Stephen Smalley <stephen.smalley.work@gmail.com>
 */

#include <assert.h>
#include <fcntl.h>
#include <stdarg.h>
#include <string.h>
#include <stdio.h>
#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>

#include "hashtab.h"
#include "callbacks.h"
#include "label_internal.h"
#include "label_file.h"

/* controls the shrink multiple of the hashtab length */
#define SHRINK_MULTIS 1

struct chkdups_key {
	char *regex;
	unsigned int mode;
};

/*
 * Internals, mostly moved over from matchpathcon.c
 */

/* return the length of the text that is the stem of a file name */
static int get_stem_from_file_name(const char *const buf)
{
	const char *tmp = strchr(buf + 1, '/');

	if (!tmp)
		return 0;
	return tmp - buf;
}

/* find the stem of a file name, returns the index into stem_arr (or -1 if
 * there is no match - IE for a file in the root directory or a regex that is
 * too complex for us). */
static int find_stem_from_file(struct saved_data *data, const char *key)
{
	int i;
	int stem_len = get_stem_from_file_name(key);

	if (!stem_len)
		return -1;
	for (i = 0; i < data->num_stems; i++) {
		if (stem_len == data->stem_arr[i].len
		    && !strncmp(key, data->stem_arr[i].buf, stem_len)) {
			return i;
		}
	}
	return -1;
}

/*
 * hash calculation and key comparison of hash table
 */

static unsigned int symhash(hashtab_t h, const_hashtab_key_t key)
{
	const struct chkdups_key *k = (const struct chkdups_key *)key;
	const char *p = NULL;
	size_t size;
	unsigned int val = 0;

	size = strlen(k->regex);
	for (p = k->regex; ((size_t) (p - k->regex)) < size; p++)
		val =
			((val << 4) | (val >> (8 * sizeof(unsigned int) - 4))) ^ (*p);
	return val % h->size;
}

static int symcmp(hashtab_t h
		  __attribute__ ((unused)), const_hashtab_key_t key1,
		  const_hashtab_key_t key2)
{
	const struct chkdups_key *a = (const struct chkdups_key *)key1;
	const struct chkdups_key *b = (const struct chkdups_key *)key2;

	return strcmp(a->regex, b->regex) || (a->mode && b->mode && a->mode != b->mode);
}

static int destroy_chkdups_key(hashtab_key_t key)
{
	free(key);

	return 0;
}

/*
 * Warn about duplicate specifications.
 */
static int nodups_specs(struct saved_data *data, const char *path)
{
	int rc = 0, ret = 0;
	unsigned int ii;
	struct spec *curr_spec, *spec_arr = data->spec_arr;
	struct chkdups_key *new = NULL;
	unsigned int hashtab_len = (data->nspec / SHRINK_MULTIS) ? data->nspec / SHRINK_MULTIS : 1;

	hashtab_t hash_table = hashtab_create(symhash, symcmp, hashtab_len);
	if (!hash_table) {
		rc = -1;
		COMPAT_LOG(SELINUX_ERROR, "%s: hashtab create failed.\n", path);
		return rc;
	}
	for (ii = 0; ii < data->nspec; ii++) {
		new = (struct chkdups_key *)malloc(sizeof(struct chkdups_key));
		if (!new) {
			rc = -1;
			hashtab_destroy_key(hash_table, destroy_chkdups_key);
			COMPAT_LOG(SELINUX_ERROR, "%s: hashtab key create failed.\n", path);
			return rc;
		}
		new->regex = spec_arr[ii].regex_str;
		new->mode = spec_arr[ii].mode;
		ret = hashtab_insert(hash_table, (hashtab_key_t)new, &spec_arr[ii]);
		if (ret == HASHTAB_SUCCESS)
			continue;
		if (ret == HASHTAB_PRESENT) {
			curr_spec =
				(struct spec *)hashtab_search(hash_table, (hashtab_key_t)new);
			rc = -1;
			errno = EINVAL;
			free(new);
			if (strcmp(spec_arr[ii].lr.ctx_raw, curr_spec->lr.ctx_raw)) {
				COMPAT_LOG
					(SELINUX_ERROR,
					 "%s: Multiple different specifications for %s  (%s and %s).\n",
					 path, curr_spec->regex_str,
					 spec_arr[ii].lr.ctx_raw,
					 curr_spec->lr.ctx_raw);
			} else {
				COMPAT_LOG
					(SELINUX_ERROR,
					 "%s: Multiple same specifications for %s.\n",
					 path, curr_spec->regex_str);
			}
		}
		if (ret == HASHTAB_OVERFLOW) {
			rc = -1;
			free(new);
			COMPAT_LOG
				(SELINUX_ERROR,
				"%s: hashtab happen memory error.\n",
				path);
			break;
		}
	}

	hashtab_destroy_key(hash_table, destroy_chkdups_key);

	return rc;
}

static int process_text_file(FILE *fp, const char *prefix,
			     struct selabel_handle *rec, const char *path)
{
	int rc;
	size_t line_len;
	unsigned int lineno = 0;
	char *line_buf = NULL;

	while (getline(&line_buf, &line_len, fp) > 0) {
		rc = process_line(rec, path, prefix, line_buf, ++lineno);
		if (rc)
			goto out;
	}
	rc = 0;
out:
	free(line_buf);
	return rc;
}

static int load_mmap(FILE *fp, size_t len, struct selabel_handle *rec,
		     const char *path)
{
	struct saved_data *data = (struct saved_data *)rec->data;
	int rc;
	char *addr, *str_buf;
	int *stem_map;
	struct mmap_area *mmap_area;
	uint32_t i, magic, version;
	uint32_t entry_len, stem_map_len, regex_array_len;
	const char *reg_version;
	const char *reg_arch;
	char reg_arch_matches = 0;

	mmap_area = malloc(sizeof(*mmap_area));
	if (!mmap_area) {
		return -1;
	}

	addr = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fileno(fp), 0);
	if (addr == MAP_FAILED) {
		free(mmap_area);
		perror("mmap");
		return -1;
	}

	/* save where we mmap'd the file to cleanup on close() */
	mmap_area->addr = mmap_area->next_addr = addr;
	mmap_area->len = mmap_area->next_len = len;
	mmap_area->next = data->mmap_areas;
	data->mmap_areas = mmap_area;

	/* check if this looks like an fcontext file */
	rc = next_entry(&magic, mmap_area, sizeof(uint32_t));
	if (rc < 0 || magic != SELINUX_MAGIC_COMPILED_FCONTEXT)
		return -1;

	/* check if this version is higher than we understand */
	rc = next_entry(&version, mmap_area, sizeof(uint32_t));
	if (rc < 0 || version > SELINUX_COMPILED_FCONTEXT_MAX_VERS)
		return -1;

	reg_version = regex_version();
	if (!reg_version)
		return -1;

	reg_arch = regex_arch_string();
	if (!reg_arch)
		return -1;

	if (version >= SELINUX_COMPILED_FCONTEXT_PCRE_VERS) {

		len = strlen(reg_version);

		rc = next_entry(&entry_len, mmap_area, sizeof(uint32_t));
		if (rc < 0)
			return -1;

		/* Check version lengths */
		if (len != entry_len)
			return -1;

		/* Check if regex version mismatch */
		str_buf = malloc(entry_len + 1);
		if (!str_buf)
			return -1;

		rc = next_entry(str_buf, mmap_area, entry_len);
		if (rc < 0) {
			free(str_buf);
			return -1;
		}

		str_buf[entry_len] = '\0';
		if ((strcmp(str_buf, reg_version) != 0)) {
			COMPAT_LOG(SELINUX_ERROR,
				"Regex version mismatch, expected: %s actual: %s\n",
				reg_version, str_buf);
			free(str_buf);
			return -1;
		}
		free(str_buf);

		if (version >= SELINUX_COMPILED_FCONTEXT_REGEX_ARCH) {
			len = strlen(reg_arch);

			rc = next_entry(&entry_len, mmap_area,
					sizeof(uint32_t));
			if (rc < 0)
				return -1;

			/* Check arch string lengths */
			if (len != entry_len) {
				/*
				 * Skip the entry and conclude that we have
				 * a mismatch, which is not fatal.
				 */
				next_entry(NULL, mmap_area, entry_len);
				goto end_arch_check;
			}

			/* Check if arch string mismatch */
			str_buf = malloc(entry_len + 1);
			if (!str_buf)
				return -1;

			rc = next_entry(str_buf, mmap_area, entry_len);
			if (rc < 0) {
				free(str_buf);
				return -1;
			}

			str_buf[entry_len] = '\0';
			reg_arch_matches = strcmp(str_buf, reg_arch) == 0;
			free(str_buf);
		}
	}
end_arch_check:

	/* allocate the stems_data array */
	rc = next_entry(&stem_map_len, mmap_area, sizeof(uint32_t));
	if (rc < 0)
		return -1;

	/*
	 * map indexed by the stem # in the mmap file and contains the stem
	 * number in the data stem_arr
	 */
	stem_map = calloc(stem_map_len, sizeof(*stem_map));
	if (!stem_map)
		return -1;

	for (i = 0; i < stem_map_len; i++) {
		char *buf;
		uint32_t stem_len;
		int newid;

		/* the length does not include the nul */
		rc = next_entry(&stem_len, mmap_area, sizeof(uint32_t));
		if (rc < 0 || !stem_len) {
			rc = -1;
			goto out;
		}

		/* Check for stem_len wrap around. */
		if (stem_len < UINT32_MAX) {
			buf = (char *)mmap_area->next_addr;
			/* Check if over-run before null check. */
			rc = next_entry(NULL, mmap_area, (stem_len + 1));
			if (rc < 0)
				goto out;

			if (buf[stem_len] != '\0') {
				rc = -1;
				goto out;
			}
		} else {
			rc = -1;
			goto out;
		}

		/* store the mapping between old and new */
		newid = find_stem(data, buf, stem_len);
		if (newid < 0) {
			newid = store_stem(data, buf, stem_len);
			if (newid < 0) {
				rc = newid;
				goto out;
			}
			data->stem_arr[newid].from_mmap = 1;
		}
		stem_map[i] = newid;
	}

	/* allocate the regex array */
	rc = next_entry(&regex_array_len, mmap_area, sizeof(uint32_t));
	if (rc < 0 || !regex_array_len) {
		rc = -1;
		goto out;
	}

	for (i = 0; i < regex_array_len; i++) {
		struct spec *spec;
		int32_t stem_id, meta_chars;
		uint32_t mode = 0, prefix_len = 0;

		rc = grow_specs(data);
		if (rc < 0)
			goto out;

		spec = &data->spec_arr[data->nspec];
		spec->from_mmap = 1;

		/* Process context */
		rc = next_entry(&entry_len, mmap_area, sizeof(uint32_t));
		if (rc < 0 || !entry_len) {
			rc = -1;
			goto out;
		}

		str_buf = malloc(entry_len);
		if (!str_buf) {
			rc = -1;
			goto out;
		}
		rc = next_entry(str_buf, mmap_area, entry_len);
		if (rc < 0) {
			free(str_buf);
			goto out;
		}

		if (str_buf[entry_len - 1] != '\0') {
			free(str_buf);
			rc = -1;
			goto out;
		}
		spec->lr.ctx_raw = str_buf;

		if (strcmp(spec->lr.ctx_raw, "<<none>>") && rec->validating) {
			if (selabel_validate(&spec->lr) < 0) {
				selinux_log(SELINUX_ERROR,
					    "%s: context %s is invalid\n",
					    path, spec->lr.ctx_raw);
				goto out;
			}
		}

		/* Process regex string */
		rc = next_entry(&entry_len, mmap_area, sizeof(uint32_t));
		if (rc < 0 || !entry_len) {
			rc = -1;
			goto out;
		}

		spec->regex_str = (char *)mmap_area->next_addr;
		rc = next_entry(NULL, mmap_area, entry_len);
		if (rc < 0)
			goto out;

		if (spec->regex_str[entry_len - 1] != '\0') {
			rc = -1;
			goto out;
		}

		/* Process mode */
		if (version >= SELINUX_COMPILED_FCONTEXT_MODE)
			rc = next_entry(&mode, mmap_area, sizeof(uint32_t));
		else
			rc = next_entry(&mode, mmap_area, sizeof(mode_t));
		if (rc < 0)
			goto out;

		spec->mode = mode;

		/* map the stem id from the mmap file to the data->stem_arr */
		rc = next_entry(&stem_id, mmap_area, sizeof(int32_t));
		if (rc < 0)
			goto out;

		if (stem_id < 0 || stem_id >= (int32_t)stem_map_len)
			spec->stem_id = -1;
		else
			spec->stem_id = stem_map[stem_id];

		/* retrieve the hasMetaChars bit */
		rc = next_entry(&meta_chars, mmap_area, sizeof(uint32_t));
		if (rc < 0)
			goto out;

		spec->hasMetaChars = meta_chars;
		/* and prefix length for use by selabel_lookup_best_match */
		if (version >= SELINUX_COMPILED_FCONTEXT_PREFIX_LEN) {
			rc = next_entry(&prefix_len, mmap_area,
					    sizeof(uint32_t));
			if (rc < 0)
				goto out;

			spec->prefix_len = prefix_len;
		}

		rc = regex_load_mmap(mmap_area, &spec->regex, reg_arch_matches,
				     &spec->regex_compiled);
		if (rc < 0)
			goto out;

		__pthread_mutex_init(&spec->regex_lock, NULL);
		data->nspec++;
	}

	rc = 0;
out:
	free(stem_map);

	return rc;
}

struct file_details {
	const char *suffix;
	struct stat sb;
};

static char *rolling_append(char *current, const char *suffix, size_t max)
{
	size_t size;
	size_t suffix_size;
	size_t current_size;

	if (!suffix)
		return current;

	current_size = strlen(current);
	suffix_size = strlen(suffix);

	size = current_size + suffix_size;
	if (size < current_size || size < suffix_size)
		return NULL;

	/* ensure space for the '.' and the '\0' characters. */
	if (size >= (SIZE_MAX - 2))
		return NULL;

	size += 2;

	if (size > max)
		return NULL;

	/* Append any given suffix */
	char *to = current + current_size;
	*to++ = '.';
	strcpy(to, suffix);

	return current;
}

static int fcontext_is_binary(FILE *fp)
{
	uint32_t magic;
	int rc;

	size_t len = fread(&magic, sizeof(magic), 1, fp);

	rc = fseek(fp, 0L, SEEK_SET);
	if (rc == -1)
		return -1;

	return (len && (magic == SELINUX_MAGIC_COMPILED_FCONTEXT));
}

#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))

static FILE *open_file(const char *path, const char *suffix,
	       char *save_path, size_t len, struct stat *sb, bool open_oldest)
{
	unsigned int i;
	int rc;
	char stack_path[len];
	struct file_details *found = NULL;

	/*
	 * Rolling append of suffix. Try to open with path.suffix then the
	 * next as path.suffix.suffix and so forth.
	 */
	struct file_details fdetails[2] = {
			{ .suffix = suffix },
			{ .suffix = "bin" }
	};

	rc = snprintf(stack_path, sizeof(stack_path), "%s", path);
	if (rc >= (int) sizeof(stack_path)) {
		errno = ENAMETOOLONG;
		return NULL;
	}

	for (i = 0; i < ARRAY_SIZE(fdetails); i++) {

		/* This handles the case if suffix is null */
		path = rolling_append(stack_path, fdetails[i].suffix,
				      sizeof(stack_path));
		if (!path)
			return NULL;

		rc = stat(path, &fdetails[i].sb);
		if (rc)
			continue;

		/* first file thing found, just take it */
		if (!found) {
			strcpy(save_path, path);
			found = &fdetails[i];
			continue;
		}

		/*
		 * Keep picking the newest file found. Where "newest"
		 * includes equality. This provides a precedence on
		 * secondary suffixes even when the timestamp is the
		 * same. Ie choose file_contexts.bin over file_contexts
		 * even if the time stamp is the same. Invert this logic
		 * on open_oldest set to true. The idea is that if the
		 * newest file failed to process, we can attempt to
		 * process the oldest. The logic here is subtle and depends
		 * on the array ordering in fdetails for the case when time
		 * stamps are the same.
		 */
		if (open_oldest ^
			(fdetails[i].sb.st_mtime >= found->sb.st_mtime)) {
			found = &fdetails[i];
			strcpy(save_path, path);
		}
	}

	if (!found) {
		errno = ENOENT;
		return NULL;
	}

	memcpy(sb, &found->sb, sizeof(*sb));
	return fopen(save_path, "re");
}

static int process_file(const char *path, const char *suffix,
			  struct selabel_handle *rec,
			  const char *prefix, struct selabel_digest *digest)
{
	int rc;
	unsigned int i;
	struct stat sb;
	FILE *fp = NULL;
	char found_path[PATH_MAX];

	/*
	 * On the first pass open the newest modified file. If it fails to
	 * process, then the second pass shall open the oldest file. If both
	 * passes fail, then it's a fatal error.
	 */
	for (i = 0; i < 2; i++) {
		fp = open_file(path, suffix, found_path, sizeof(found_path),
			&sb, i > 0);
		if (fp == NULL)
			return -1;

		rc = fcontext_is_binary(fp);
		if (rc < 0) {
			(void) fclose(fp);
			return -1;
		}

		rc = rc ?
				load_mmap(fp, sb.st_size, rec, found_path) :
				process_text_file(fp, prefix, rec, found_path);
		if (!rc)
			rc = digest_add_specfile(digest, fp, NULL, sb.st_size,
				found_path);

		fclose(fp);

		if (!rc)
			return 0;
	}
	return -1;
}

static void selabel_subs_fini(struct selabel_sub *ptr)
{
	struct selabel_sub *next;

	while (ptr) {
		next = ptr->next;
		free(ptr->src);
		free(ptr->dst);
		free(ptr);
		ptr = next;
	}
}

static char *selabel_sub(struct selabel_sub *ptr, const char *src)
{
	char *dst = NULL;
	int len;

	while (ptr) {
		if (strncmp(src, ptr->src, ptr->slen) == 0 ) {
			if (src[ptr->slen] == '/' ||
			    src[ptr->slen] == 0) {
				if ((src[ptr->slen] == '/') &&
				    (strcmp(ptr->dst, "/") == 0))
					len = ptr->slen + 1;
				else
					len = ptr->slen;
				if (asprintf(&dst, "%s%s", ptr->dst, &src[len]) < 0)
					return NULL;
				return dst;
			}
		}
		ptr = ptr->next;
	}
	return NULL;
}

#if !defined(BUILD_HOST) && !defined(ANDROID)
static int selabel_subs_init(const char *path, struct selabel_digest *digest,
		       struct selabel_sub **out_subs)
{
	char buf[1024];
	FILE *cfg = fopen(path, "re");
	struct selabel_sub *list = NULL, *sub = NULL;
	struct stat sb;
	int status = -1;

	*out_subs = NULL;
	if (!cfg) {
		/* If the file does not exist, it is not fatal */
		return (errno == ENOENT) ? 0 : -1;
	}

	if (fstat(fileno(cfg), &sb) < 0)
		goto out;

	while (fgets_unlocked(buf, sizeof(buf) - 1, cfg)) {
		char *ptr = NULL;
		char *src = buf;
		char *dst = NULL;

		while (*src && isspace((unsigned char)*src))
			src++;
		if (src[0] == '#') continue;
		ptr = src;
		while (*ptr && ! isspace((unsigned char)*ptr))
			ptr++;
		*ptr++ = '\0';
		if (! *src) continue;

		dst = ptr;
		while (*dst && isspace((unsigned char)*dst))
			dst++;
		ptr = dst;
		while (*ptr && ! isspace((unsigned char)*ptr))
			ptr++;
		*ptr = '\0';
		if (! *dst)
			continue;

		sub = calloc(1, sizeof(*sub));
		if (! sub)
			goto err;

		sub->src = strdup(src);
		if (! sub->src)
			goto err;

		sub->dst = strdup(dst);
		if (! sub->dst)
			goto err;

		sub->slen = strlen(src);
		sub->next = list;
		list = sub;
		sub = NULL;
	}

	if (digest_add_specfile(digest, cfg, NULL, sb.st_size, path) < 0)
		goto err;

	*out_subs = list;
	status = 0;

out:
	fclose(cfg);
	return status;
err:
	if (sub)
		free(sub->src);
	free(sub);
	while (list) {
		sub = list->next;
		free(list->src);
		free(list->dst);
		free(list);
		list = sub;
	}
	goto out;
}
#endif

static char *selabel_sub_key(struct saved_data *data, const char *key)
{
	char *ptr = NULL;
	char *dptr = NULL;

	ptr = selabel_sub(data->subs, key);
	if (ptr) {
		dptr = selabel_sub(data->dist_subs, ptr);
		if (dptr) {
			free(ptr);
			ptr = dptr;
		}
	} else {
		ptr = selabel_sub(data->dist_subs, key);
	}
	if (ptr)
		return ptr;

	return NULL;
}

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;
	const char *path = NULL;
	const char *prefix = NULL;
	int status = -1, baseonly = 0;

	/* Process arguments */
	while (n--)
		switch(opts[n].type) {
		case SELABEL_OPT_PATH:
			path = opts[n].value;
			break;
		case SELABEL_OPT_SUBSET:
			prefix = opts[n].value;
			break;
		case SELABEL_OPT_BASEONLY:
			baseonly = !!opts[n].value;
			break;
		}

#if !defined(BUILD_HOST) && !defined(ANDROID)
	char subs_file[PATH_MAX + 1];
	/* Process local and distribution substitution files */
	if (!path) {
		status = selabel_subs_init(
			selinux_file_context_subs_dist_path(),
			rec->digest, &data->dist_subs);
		if (status)
			goto finish;
		status = selabel_subs_init(selinux_file_context_subs_path(),
			rec->digest, &data->subs);
		if (status)
			goto finish;
		path = selinux_file_context_path();
	} else {
		snprintf(subs_file, sizeof(subs_file), "%s.subs_dist", path);
		status = selabel_subs_init(subs_file, rec->digest,
					   &data->dist_subs);
		if (status)
			goto finish;
		snprintf(subs_file, sizeof(subs_file), "%s.subs", path);
		status = selabel_subs_init(subs_file, rec->digest,
					   &data->subs);
		if (status)
			goto finish;
	}

#endif

	if (!path)
		goto finish;

	rec->spec_file = strdup(path);

	/*
	 * The do detailed validation of the input and fill the spec array
	 */
	status = process_file(path, NULL, rec, prefix, rec->digest);
	if (status)
		goto finish;

	if (rec->validating) {
		status = nodups_specs(data, path);
		if (status)
			goto finish;
	}

	if (!baseonly) {
		status = process_file(path, "homedirs", rec, prefix,
							    rec->digest);
		if (status && errno != ENOENT)
			goto finish;

		status = process_file(path, "local", rec, prefix,
							    rec->digest);
		if (status && errno != ENOENT)
			goto finish;
	}

	digest_gen_hash(rec->digest);

	status = sort_specs(data);

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 mmap_area *area, *last_area;
	struct spec *spec;
	struct stem *stem;
	unsigned int i;

	selabel_subs_fini(data->subs);
	selabel_subs_fini(data->dist_subs);

	for (i = 0; i < data->nspec; i++) {
		spec = &data->spec_arr[i];
		free(spec->lr.ctx_trans);
		free(spec->lr.ctx_raw);
		regex_data_free(spec->regex);
		__pthread_mutex_destroy(&spec->regex_lock);
		if (spec->from_mmap)
			continue;
		free(spec->regex_str);
		free(spec->type_str);
	}

	for (i = 0; i < (unsigned int)data->num_stems; i++) {
		stem = &data->stem_arr[i];
		if (stem->from_mmap)
			continue;
		free(stem->buf);
	}

	if (data->spec_arr)
		free(data->spec_arr);
	if (data->stem_arr)
		free(data->stem_arr);

	area = data->mmap_areas;
	while (area) {
		munmap(area->addr, area->len);
		last_area = area;
		area = area->next;
		free(last_area);
	}
	free(data);
}

// Finds all the matches of |key| in the given context. Returns the result in
// the allocated array and updates the match count. If match_count is NULL,
// stops early once the 1st match is found.
static struct spec **lookup_all(struct selabel_handle *rec,
                                      const char *key,
                                      int type,
                                      bool partial,
                                      size_t *match_count)
{
	struct saved_data *data = (struct saved_data *)rec->data;
	struct spec *spec_arr = data->spec_arr;
	int i, rc, file_stem;
	size_t len;
	mode_t mode = (mode_t)type;
	char *clean_key = NULL;
	const char *prev_slash, *next_slash;
	unsigned int sofar = 0;
	char *sub = NULL;

	struct spec **result = NULL;
	if (match_count) {
		*match_count = 0;
		result = calloc(data->nspec, sizeof(struct spec*));
	} else {
		result = calloc(1, sizeof(struct spec*));
	}
	if (!result) {
		selinux_log(SELINUX_ERROR, "Failed to allocate %zu bytes of data\n",
			    data->nspec * sizeof(struct spec*));
		goto finish;
	}

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

	/* Remove duplicate slashes */
	if ((next_slash = strstr(key, "//"))) {
		clean_key = (char *) malloc(strlen(key) + 1);
		if (!clean_key)
			goto finish;
		prev_slash = key;
		while (next_slash) {
			memcpy(clean_key + sofar, prev_slash, next_slash - prev_slash);
			sofar += next_slash - prev_slash;
			prev_slash = next_slash + 1;
			next_slash = strstr(prev_slash, "//");
		}
		strcpy(clean_key + sofar, prev_slash);
		key = clean_key;
	}

	/* remove trailing slash */
	len = strlen(key);
	if (len == 0) {
		errno = EINVAL;
		goto finish;
	}

	if (len > 1 && key[len - 1] == '/') {
		/* reuse clean_key from above if available */
		if (!clean_key) {
			clean_key = (char *) malloc(len);
			if (!clean_key)
				goto finish;

			memcpy(clean_key, key, len - 1);
		}

		clean_key[len - 1] = '\0';
		key = clean_key;
	}

	sub = selabel_sub_key(data, key);
	if (sub)
		key = sub;

	file_stem = find_stem_from_file(data, key);
	mode &= S_IFMT;

	/*
	 * Check for matching specifications in reverse order, so that
	 * the last matching specification is used.
	 */
	for (i = data->nspec - 1; i >= 0; i--) {
		struct spec *spec = &spec_arr[i];
		/* if the spec in question matches no stem or has the same
		 * stem as the file AND if the spec in question has no mode
		 * specified or if the mode matches the file mode then we do
		 * a regex check        */
		bool stem_matches = spec->stem_id == -1 || spec->stem_id == file_stem;
		// Don't check the stem if we want to find partial matches.
                // Otherwise the case "/abc/efg/(/.*)?" will be considered
                //a miss for "/abc".
		if ((partial || stem_matches) &&
				(!mode || !spec->mode || mode == spec->mode)) {
			if (compile_regex(spec, NULL) < 0)
				goto finish;
			rc = regex_match(spec->regex, key, partial);
			if (rc == REGEX_MATCH || (partial && rc == REGEX_MATCH_PARTIAL)) {
				if (rc == REGEX_MATCH) {
#ifdef __ATOMIC_RELAXED
					__atomic_store_n(&spec->any_matches,
							 true, __ATOMIC_RELAXED);
#else
#error "Please use a compiler that supports __atomic builtins"
#endif
				}

				if (strcmp(spec_arr[i].lr.ctx_raw, "<<none>>") == 0) {
					errno = ENOENT;
					goto finish;
				}

				if (match_count) {
					result[*match_count] = spec;
					*match_count += 1;
					// Continue to find all the matches.
					continue;
				}
				result[0] = spec;
				break;
			}

			if (rc == REGEX_NO_MATCH)
				continue;

			errno = ENOENT;
			/* else it's an error */
			goto finish;
		}
	}
	if (!result[0])
		errno = ENOENT;

finish:
	free(clean_key);
	free(sub);
	if (result && !result[0]) {
		free(result);
		result = NULL;
	}
	return result;
}

static struct spec *lookup_common(struct selabel_handle *rec,
                                  const char *key,
                                  int type,
                                  bool partial) {
	struct spec **matches = lookup_all(rec, key, type, partial, NULL);
	if (!matches) {
		return NULL;
	}
	struct spec *result = matches[0];
	free(matches);
	return result;
}

/*
 * Returns true if the digest of all partial matched contexts is the same as
 * the one saved by setxattr, otherwise returns false. The length of the SHA1
 * digest will always be returned. The caller must free any returned digests.
 */
static bool get_digests_all_partial_matches(struct selabel_handle *rec,
					    const char *pathname,
					    uint8_t **calculated_digest,
					    uint8_t **xattr_digest,
					    size_t *digest_len)
{
	uint8_t read_digest[SHA1_HASH_SIZE];
	ssize_t read_size = getxattr(pathname, RESTORECON_PARTIAL_MATCH_DIGEST,
				     read_digest, SHA1_HASH_SIZE
#ifdef __APPLE__
				     , 0, 0
#endif /* __APPLE __ */
				    );
	uint8_t hash_digest[SHA1_HASH_SIZE];
	bool status = selabel_hash_all_partial_matches(rec, pathname,
						       hash_digest);

	*xattr_digest = NULL;
	*calculated_digest = NULL;
	*digest_len = SHA1_HASH_SIZE;

	if (read_size == SHA1_HASH_SIZE) {
		*xattr_digest = calloc(1, SHA1_HASH_SIZE + 1);
		if (!*xattr_digest)
			goto oom;

		memcpy(*xattr_digest, read_digest, SHA1_HASH_SIZE);
	}

	if (status) {
		*calculated_digest = calloc(1, SHA1_HASH_SIZE + 1);
		if (!*calculated_digest)
			goto oom;

		memcpy(*calculated_digest, hash_digest, SHA1_HASH_SIZE);
	}

	if (status && read_size == SHA1_HASH_SIZE &&
	    memcmp(read_digest, hash_digest, SHA1_HASH_SIZE) == 0)
		return true;

	return false;

oom:
	selinux_log(SELINUX_ERROR, "SELinux: %s: Out of memory\n", __func__);
	return false;
}

static bool hash_all_partial_matches(struct selabel_handle *rec, const char *key, uint8_t *digest)
{
	assert(digest);

	size_t total_matches;
	struct spec **matches = lookup_all(rec, key, 0, true, &total_matches);
	if (!matches) {
		return false;
	}

	Sha1Context context;
	Sha1Initialise(&context);
	size_t i;
	for (i = 0; i < total_matches; i++) {
		char* regex_str = matches[i]->regex_str;
		mode_t mode = matches[i]->mode;
		char* ctx_raw = matches[i]->lr.ctx_raw;

		Sha1Update(&context, regex_str, strlen(regex_str) + 1);
		Sha1Update(&context, &mode, sizeof(mode_t));
		Sha1Update(&context, ctx_raw, strlen(ctx_raw) + 1);
	}

	SHA1_HASH sha1_hash;
	Sha1Finalise(&context, &sha1_hash);
	memcpy(digest, sha1_hash.bytes, SHA1_HASH_SIZE);

	free(matches);
	return true;
}

static struct selabel_lookup_rec *lookup(struct selabel_handle *rec,
					 const char *key, int type)
{
	struct spec *spec;

	spec = lookup_common(rec, key, type, false);
	if (spec)
		return &spec->lr;
	return NULL;
}

static bool partial_match(struct selabel_handle *rec, const char *key)
{
	return lookup_common(rec, key, 0, true) ? true : false;
}

static struct selabel_lookup_rec *lookup_best_match(struct selabel_handle *rec,
						    const char *key,
						    const char **aliases,
						    int type)
{
	size_t n, i;
	int best = -1;
	struct spec **specs;
	size_t prefix_len = 0;
	struct selabel_lookup_rec *lr = NULL;

	if (!aliases || !aliases[0])
		return lookup(rec, key, type);

	for (n = 0; aliases[n]; n++)
		;

	specs = calloc(n+1, sizeof(struct spec *));
	if (!specs)
		return NULL;
	specs[0] = lookup_common(rec, key, type, false);
	if (specs[0]) {
		if (!specs[0]->hasMetaChars) {
			/* exact match on key */
			lr = &specs[0]->lr;
			goto out;
		}
		best = 0;
		prefix_len = specs[0]->prefix_len;
	}
	for (i = 1; i <= n; i++) {
		specs[i] = lookup_common(rec, aliases[i-1], type, false);
		if (specs[i]) {
			if (!specs[i]->hasMetaChars) {
				/* exact match on alias */
				lr = &specs[i]->lr;
				goto out;
			}
			if (specs[i]->prefix_len > prefix_len) {
				best = i;
				prefix_len = specs[i]->prefix_len;
			}
		}
	}

	if (best >= 0) {
		/* longest fixed prefix match on key or alias */
		lr = &specs[best]->lr;
	} else {
		errno = ENOENT;
	}

out:
	free(specs);
	return lr;
}

static enum selabel_cmp_result incomp(const struct spec *spec1, const struct spec *spec2, const char *reason, int i, int j)
{
	selinux_log(SELINUX_INFO,
		    "selabel_cmp: mismatched %s on entry %d: (%s, %x, %s) vs entry %d: (%s, %x, %s)\n",
		    reason,
		    i, spec1->regex_str, spec1->mode, spec1->lr.ctx_raw,
		    j, spec2->regex_str, spec2->mode, spec2->lr.ctx_raw);
	return SELABEL_INCOMPARABLE;
}

static enum selabel_cmp_result cmp(const struct selabel_handle *h1,
				   const struct selabel_handle *h2)
{
	const struct saved_data *data1 = (const struct saved_data *)h1->data;
	const struct saved_data *data2 = (const struct saved_data *)h2->data;
	unsigned int i, nspec1 = data1->nspec, j, nspec2 = data2->nspec;
	const struct spec *spec_arr1 = data1->spec_arr, *spec_arr2 = data2->spec_arr;
	const struct stem *stem_arr1 = data1->stem_arr, *stem_arr2 = data2->stem_arr;
	bool skipped1 = false, skipped2 = false;

	i = 0;
	j = 0;
	while (i < nspec1 && j < nspec2) {
		const struct spec *spec1 = &spec_arr1[i];
		const struct spec *spec2 = &spec_arr2[j];

		/*
		 * Because sort_specs() moves exact pathnames to the
		 * end, we might need to skip over additional regex
		 * entries that only exist in one of the configurations.
		 */
		if (!spec1->hasMetaChars && spec2->hasMetaChars) {
			j++;
			skipped2 = true;
			continue;
		}

		if (spec1->hasMetaChars && !spec2->hasMetaChars) {
			i++;
			skipped1 = true;
			continue;
		}

		if (spec1->regex && spec2->regex) {
			if (regex_cmp(spec1->regex, spec2->regex) == SELABEL_INCOMPARABLE){
				return incomp(spec1, spec2, "regex", i, j);
			}
		} else {
			if (strcmp(spec1->regex_str, spec2->regex_str))
				return incomp(spec1, spec2, "regex_str", i, j);
		}

		if (spec1->mode != spec2->mode)
			return incomp(spec1, spec2, "mode", i, j);

		if (spec1->stem_id == -1 && spec2->stem_id != -1)
			return incomp(spec1, spec2, "stem_id", i, j);
		if (spec2->stem_id == -1 && spec1->stem_id != -1)
			return incomp(spec1, spec2, "stem_id", i, j);
		if (spec1->stem_id != -1 && spec2->stem_id != -1) {
			const struct stem *stem1 = &stem_arr1[spec1->stem_id];
			const struct stem *stem2 = &stem_arr2[spec2->stem_id];
			if (stem1->len != stem2->len ||
			    strncmp(stem1->buf, stem2->buf, stem1->len))
				return incomp(spec1, spec2, "stem", i, j);
		}

		if (strcmp(spec1->lr.ctx_raw, spec2->lr.ctx_raw))
			return incomp(spec1, spec2, "ctx_raw", i, j);

		i++;
		j++;
	}

	if ((skipped1 || i < nspec1) && !skipped2)
		return SELABEL_SUPERSET;
	if ((skipped2 || j < nspec2) && !skipped1)
		return SELABEL_SUBSET;
	if (skipped1 && skipped2)
		return SELABEL_INCOMPARABLE;
	return SELABEL_EQUAL;
}


static void stats(struct selabel_handle *rec)
{
	struct saved_data *data = (struct saved_data *)rec->data;
	unsigned int i, nspec = data->nspec;
	struct spec *spec_arr = data->spec_arr;
	bool any_matches;

	for (i = 0; i < nspec; i++) {
#ifdef __ATOMIC_RELAXED
		any_matches = __atomic_load_n(&spec_arr[i].any_matches, __ATOMIC_RELAXED);
#else
#error "Please use a compiler that supports __atomic builtins"
#endif
		if (!any_matches) {
			if (spec_arr[i].type_str) {
				COMPAT_LOG(SELINUX_WARNING,
				    "Warning!  No matches for (%s, %s, %s)\n",
				    spec_arr[i].regex_str,
				    spec_arr[i].type_str,
				    spec_arr[i].lr.ctx_raw);
			} else {
				COMPAT_LOG(SELINUX_WARNING,
				    "Warning!  No matches for (%s, %s)\n",
				    spec_arr[i].regex_str,
				    spec_arr[i].lr.ctx_raw);
			}
		}
	}
}

int selabel_file_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 = &lookup;
	rec->func_partial_match = &partial_match;
	rec->func_get_digests_all_partial_matches =
					&get_digests_all_partial_matches;
	rec->func_hash_all_partial_matches = &hash_all_partial_matches;
	rec->func_lookup_best_match = &lookup_best_match;
	rec->func_cmp = &cmp;

	return init(rec, opts, nopts);
}
