// SPDX-License-Identifier: GPL-3.0-or-later

#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#include "chase.h"
#include "platform/platform.h"
#include "util.h"

static bool root_is_real(const char *root) {
	if (!root || !*root)
		return true;
	if (root[0] == '/' && root[1] == '\0')
		return true;
	return false;
}


static void done_append(char **done, const char *name) {
	if (!*done || !**done) {
		free(*done);
		*done = xasprintf("/%s", name);
		return;
	}
	char *n = xasprintf("%s/%s", *done, name);
	free(*done);
	*done = n;
}

static void done_pop(char *done) {
	if (!done || !*done)
		return;

	char *slash = strrchr(done, '/');
	if (slash)
		*slash = '\0';
	else
		done[0] = '\0';
}


static char *build_result(const char *root_abs, const char *rel) {
	const char *base = root_abs ? root_abs : "/";

	if (!rel || !*rel)
		return xstrdup(base);

	char *joined = path_join(base, rel);
	path_simplify(joined);
	return joined;
}

static char *build_full_path(const char *root_abs, const char *done, const char *name) {
	const char *base = root_abs ? root_abs : "/";
	const char *middle = done ? done : "";

	char *ret;
	if (asprintf(&ret, "%s/%s/%s", base, middle, name) < 0)
		return NULL;
	return path_simplify(ret);
}

int chase_full(const char *path, const char *root, ChaseFlags flags,
               char **ret_path, int *ret_fd,
               int *ret_parent_fd, char **ret_basename) {
	if (!path || !*path)
		return -EINVAL;

	bool have_root = !root_is_real(root);

	_cleanup_free_ char *root_abs = NULL;
	if (have_root) {
		root_abs = xstrdup(root);
		path_simplify(root_abs);

		if (streq(root_abs, "/")) {
			free(root_abs);
			root_abs = NULL;
			have_root = false;
		} else if (!path_is_absolute(root_abs)) {
			return -EINVAL;
		}
	}

	_cleanup_free_ char *last_name = NULL;
	_cleanup_free_ char *done = xstrdup("");
	_cleanup_free_ char *todo = NULL;

	if (have_root && !(flags & CHASE_PREFIX_ROOT)) {
		const char *inside = path_startswith(path, root_abs);
		if (!inside) {
			if (flags & CHASE_WARN)
				log_warning("Path '%s' lies outside of root '%s'.", path, root_abs);
			return -EXDEV;
		}
		todo = xstrdup(inside);
	} else {
		todo = xstrdup(path);
	}

	
	const char *p = todo;
	while (*p == '/')
		p++;

	unsigned symlinks_followed = 0;

	while (*p) {
		const char *end = p;
		while (*end && *end != '/')
			end++;

		size_t namelen = (size_t)(end - p);
		if (namelen == 0) {
			p = end;
			continue;
		}
		if (namelen > NAME_MAX)
			return -ENAMETOOLONG;

		char name[NAME_MAX + 1];
		memcpy(name, p, namelen);
		name[namelen] = '\0';

		const char *next = end;
		while (*next == '/')
			next++;
		bool is_last = (*next == '\0');

		if (streq(name, ".")) {
			p = next;
			continue;
		}

		if (streq(name, "..")) {
			if (!*done) {
				p = next;
				continue;
			}
			done_pop(done);

			if (have_root && *done && !path_startswith(done, root_abs))
				return -EXDEV;

			free(last_name);
			last_name = NULL;
			p = next;
			continue;
		}

		struct stat st;
		_cleanup_free_ char *fullpath = build_full_path(root_abs, done, name);
		if (!fullpath)
			return -errno;
		if (lstat(fullpath, &st) != 0) {
			if (errno == ENOENT && (flags & CHASE_NONEXISTENT)) {
				_cleanup_free_ char *tail = path_join(name, next);
				char *new_done;
				if (!*done)
					new_done = xasprintf("/%s", tail);
				else
					new_done = xasprintf("%s/%s", done, tail);
				free(done);
				done = new_done;

				_cleanup_free_ char *full_path = build_result(root_abs, done);
				done_pop(done);
				_cleanup_free_ char *parent_path = build_result(root_abs, done);
			if (ret_path)
				*ret_path = xstrdup(full_path);
			if (ret_fd) {
				*ret_fd = -1;
			}
			if (ret_parent_fd) {
				*ret_parent_fd = open(parent_path, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
				if (*ret_parent_fd < 0)
					return -errno;
			}
			if (ret_basename)
				*ret_basename = xstrdup(name);
			return 0;
			}
			return -errno;
		}

		if (S_ISLNK(st.st_mode) && !(is_last && (flags & CHASE_NOFOLLOW))) {
			if (++symlinks_followed > CHASE_MAX) {
				if (flags & CHASE_WARN)
					log_warning(
								"Too many symbolic links encountered while resolving '%s'.",
								path);
				return -ELOOP;
			}

			_cleanup_free_ char *target = NULL;
			ssize_t tlen = readlink_dynamic(fullpath, &target);
			if (tlen < 0)
				return -tlen;
			if (tlen == 0)
				return -EINVAL;

			char *new_todo;
			if (target[0] == '/') {
				done[0] = '\0';

				const char *tp = target;
				while (*tp == '/')
					tp++;
				new_todo = path_join(tp, next);
			} else {
				new_todo = path_join(target, next);
			}
			free(target);
			free(todo);
			todo = new_todo;
			p = todo;
			while (*p == '/')
				p++;
			continue;
		}

		if (S_ISDIR(st.st_mode)) {
			free(last_name);
			last_name = xstrdup(name);

			done_append(&done, name);
			p = next;
			continue;
		}

		if (!is_last)
			return -ENOTDIR;

		done_append(&done, name);

		_cleanup_free_ char *full_path = build_result(root_abs, done);
		done_pop(done);
		_cleanup_free_ char *parent_path = build_result(root_abs, done);
		if (ret_path)
			*ret_path = xstrdup(full_path);
		if (ret_fd) {
			*ret_fd = open(full_path, O_RDONLY | O_CLOEXEC | (flags & CHASE_NOFOLLOW ? O_NOFOLLOW : 0));
			if (*ret_fd < 0)
				return -errno;
		}
		if (ret_parent_fd) {
			*ret_parent_fd = open(parent_path, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
			if (*ret_parent_fd < 0)
				return -errno;
		}
		if (ret_basename)
			*ret_basename = xstrdup(name);
		return 0;
	}

	_cleanup_free_ char *full_path = build_result(root_abs, done);
	done_pop(done);
	_cleanup_free_ char *parent_path = build_result(root_abs, done);
	if (ret_path)
		*ret_path = xstrdup(full_path);
	if (ret_fd) {
		*ret_fd = open(full_path, O_RDONLY | O_CLOEXEC | (flags & CHASE_NOFOLLOW ? O_NOFOLLOW : 0));
		if (*ret_fd < 0)
			return -errno;
	}
	if (ret_parent_fd) {
		*ret_parent_fd = open(parent_path, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
		if (*ret_parent_fd < 0)
			return -errno;
	}
	if (ret_basename)
		*ret_basename = last_name ? xstrdup(last_name) : xstrdup(".");
	return 0;
}

int chase(const char *path, const char *root, ChaseFlags flags,
          char **ret_path, int *ret_fd) {
	return chase_full(path, root, flags, ret_path, ret_fd, NULL, NULL);
}

int chase_and_open(const char *path, const char *root, ChaseFlags flags,
                   int open_flags, char **ret_path) {
	_cleanup_close_ int path_fd = -1;
	_cleanup_close_ int parent_fd = -1;
	_cleanup_free_ char *p = NULL;
	_cleanup_free_ char *base = NULL;

	int r = chase_full(path, root, flags, &p, &path_fd, &parent_fd, &base);
	if (r < 0)
		return r;

	if (path_fd < 0 && !base) {
		if (ret_path) {
			*ret_path = p;
			p = NULL;
		}
		return -ENOENT;
	}

	int fd = fd_reopen(path_fd, parent_fd, base, p, open_flags);
	if (fd < 0)
		return fd;

	if (ret_path) {
		*ret_path = p;
		p = NULL;
	}
	return fd;
}
