/****************************************************************************
 * bfs                                                                      *
 * Copyright (C) 2020 Tavian Barnes <tavianator@tavianator.com>             *
 *                                                                          *
 * Permission to use, copy, modify, and/or distribute this software for any *
 * purpose with or without fee is hereby granted.                           *
 *                                                                          *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF         *
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR  *
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES   *
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN    *
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF  *
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.           *
 ****************************************************************************/

#include "pwcache.h"
#include "darray.h"
#include "trie.h"
#include <errno.h>
#include <grp.h>
#include <pwd.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>

struct bfs_users {
	/** The array of passwd entries. */
	struct passwd *entries;
	/** A map from usernames to entries. */
	struct trie by_name;
	/** A map from UIDs to entries. */
	struct trie by_uid;
};

struct bfs_users *bfs_users_parse(void) {
	int error;

	struct bfs_users *users = malloc(sizeof(*users));
	if (!users) {
		return NULL;
	}

	users->entries = NULL;
	trie_init(&users->by_name);
	trie_init(&users->by_uid);

	setpwent();

	while (true) {
		errno = 0;
		struct passwd *ent = getpwent();
		if (!ent) {
			if (errno) {
				error = errno;
				goto fail_end;
			} else {
				break;
			}
		}

		if (DARRAY_PUSH(&users->entries, ent) != 0) {
			error = errno;
			goto fail_end;
		}

		ent = users->entries + darray_length(users->entries) - 1;
		ent->pw_name = strdup(ent->pw_name);
		ent->pw_dir = strdup(ent->pw_dir);
		ent->pw_shell = strdup(ent->pw_shell);
		if (!ent->pw_name || !ent->pw_dir || !ent->pw_shell) {
			error = ENOMEM;
			goto fail_end;
		}
	}

	endpwent();

	for (size_t i = 0; i < darray_length(users->entries); ++i) {
		struct passwd *entry = users->entries + i;
		struct trie_leaf *leaf = trie_insert_str(&users->by_name, entry->pw_name);
		if (leaf) {
			if (!leaf->value) {
				leaf->value = entry;
			}
		} else {
			error = errno;
			goto fail_free;
		}

		leaf = trie_insert_mem(&users->by_uid, &entry->pw_uid, sizeof(entry->pw_uid));
		if (leaf) {
			if (!leaf->value) {
				leaf->value = entry;
			}
		} else {
			error = errno;
			goto fail_free;
		}
	}

	return users;

fail_end:
	endpwent();
fail_free:
	bfs_users_free(users);
	errno = error;
	return NULL;
}

const struct passwd *bfs_getpwnam(const struct bfs_users *users, const char *name) {
	const struct trie_leaf *leaf = trie_find_str(&users->by_name, name);
	if (leaf) {
		return leaf->value;
	} else {
		return NULL;
	}
}

const struct passwd *bfs_getpwuid(const struct bfs_users *users, uid_t uid) {
	const struct trie_leaf *leaf = trie_find_mem(&users->by_uid, &uid, sizeof(uid));
	if (leaf) {
		return leaf->value;
	} else {
		return NULL;
	}
}

void bfs_users_free(struct bfs_users *users) {
	if (users) {
		trie_destroy(&users->by_uid);
		trie_destroy(&users->by_name);

		for (size_t i = 0; i < darray_length(users->entries); ++i) {
			struct passwd *entry = users->entries + i;
			free(entry->pw_shell);
			free(entry->pw_dir);
			free(entry->pw_name);
		}
		darray_free(users->entries);

		free(users);
	}
}

struct bfs_groups {
	/** The array of group entries. */
	struct group *entries;
	/** A map from group names to entries. */
	struct trie by_name;
	/** A map from GIDs to entries. */
	struct trie by_gid;
};

/**
 * struct group::gr_mem isn't properly aligned on macOS, so do this to avoid
 * ASAN warnings.
 */
static char *next_gr_mem(void **gr_mem) {
	char *mem;
	memcpy(&mem, *gr_mem, sizeof(mem));
	*gr_mem = (char *)*gr_mem + sizeof(mem);
	return mem;
}

struct bfs_groups *bfs_groups_parse(void) {
	int error;

	struct bfs_groups *groups = malloc(sizeof(*groups));
	if (!groups) {
		return NULL;
	}

	groups->entries = NULL;
	trie_init(&groups->by_name);
	trie_init(&groups->by_gid);

	setgrent();

	while (true) {
		errno = 0;
		struct group *ent = getgrent();
		if (!ent) {
			if (errno) {
				error = errno;
				goto fail_end;
			} else {
				break;
			}
		}

		if (DARRAY_PUSH(&groups->entries, ent) != 0) {
			error = errno;
			goto fail_end;
		}
		ent = groups->entries + darray_length(groups->entries) - 1;

		void *members = ent->gr_mem;
		ent->gr_mem = NULL;

		ent->gr_name = strdup(ent->gr_name);
		if (!ent->gr_name) {
			error = errno;
			goto fail_end;
		}

		for (char *mem = next_gr_mem(&members); mem; mem = next_gr_mem(&members)) {
			char *dup = strdup(mem);
			if (!dup) {
				error = errno;
				goto fail_end;
			}

			if (DARRAY_PUSH(&ent->gr_mem, &dup) != 0) {
				error = errno;
				free(dup);
				goto fail_end;
			}
		}
	}

	endgrent();

	for (size_t i = 0; i < darray_length(groups->entries); ++i) {
		struct group *entry = groups->entries + i;
		struct trie_leaf *leaf = trie_insert_str(&groups->by_name, entry->gr_name);
		if (leaf) {
			if (!leaf->value) {
				leaf->value = entry;
			}
		} else {
			error = errno;
			goto fail_free;
		}

		leaf = trie_insert_mem(&groups->by_gid, &entry->gr_gid, sizeof(entry->gr_gid));
		if (leaf) {
			if (!leaf->value) {
				leaf->value = entry;
			}
		} else {
			error = errno;
			goto fail_free;
		}
	}

	return groups;

fail_end:
	endgrent();
fail_free:
	bfs_groups_free(groups);
	errno = error;
	return NULL;
}

const struct group *bfs_getgrnam(const struct bfs_groups *groups, const char *name) {
	const struct trie_leaf *leaf = trie_find_str(&groups->by_name, name);
	if (leaf) {
		return leaf->value;
	} else {
		return NULL;
	}
}

const struct group *bfs_getgrgid(const struct bfs_groups *groups, gid_t gid) {
	const struct trie_leaf *leaf = trie_find_mem(&groups->by_gid, &gid, sizeof(gid));
	if (leaf) {
		return leaf->value;
	} else {
		return NULL;
	}
}

void bfs_groups_free(struct bfs_groups *groups) {
	if (groups) {
		trie_destroy(&groups->by_gid);
		trie_destroy(&groups->by_name);

		for (size_t i = 0; i < darray_length(groups->entries); ++i) {
			struct group *entry = groups->entries + i;
			for (size_t j = 0; j < darray_length(entry->gr_mem); ++j) {
				free(entry->gr_mem[j]);
			}
			darray_free(entry->gr_mem);
			free(entry->gr_name);
		}
		darray_free(groups->entries);

		free(groups);
	}
}
