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

#include <sys/ioctl.h>
#include <unistd.h>
#include <linux/fs.h>
#include <acl/libacl.h>
#include "platform.h"

#ifndef FS_PROJINHERIT_FL
#define FS_PROJINHERIT_FL 0x20000000
#endif

#define CHATTR_ALL_FL							\
(FS_NOATIME_FL | FS_SYNC_FL | FS_DIRSYNC_FL | FS_APPEND_FL | FS_COMPR_FL | \
FS_NODUMP_FL | FS_EXTENT_FL | FS_IMMUTABLE_FL | FS_JOURNAL_DATA_FL |	\
FS_SECRM_FL | FS_UNRM_FL | FS_NOTAIL_FL | FS_TOPDIR_FL | FS_NOCOW_FL | \
FS_PROJINHERIT_FL)

int fchmod_opath(int fd, mode_t mode) {
	char p[64];
	snprintf(p, sizeof(p), "/proc/self/fd/%d", fd);
	if (chmod(p, mode) < 0)
		return -errno;
	return 0;
}

int parse_attribute_from_arg(Item *item) {
	static const struct {
		char character;
		unsigned value;
	} attributes[] = {
		{'A', FS_NOATIME_FL}, {'S', FS_SYNC_FL},      {'D', FS_DIRSYNC_FL},
		{'a', FS_APPEND_FL},  {'c', FS_COMPR_FL},     {'d', FS_NODUMP_FL},
		{'e', FS_EXTENT_FL},  {'i', FS_IMMUTABLE_FL}, {'j', FS_JOURNAL_DATA_FL},
		{'s', FS_SECRM_FL},   {'u', FS_UNRM_FL},      {'t', FS_NOTAIL_FL},
		{'T', FS_TOPDIR_FL},  {'C', FS_NOCOW_FL},     {'P', FS_PROJINHERIT_FL},
	};

	enum { MODE_ADD, MODE_DEL, MODE_SET } mode = MODE_ADD;
	unsigned value = 0, mask = 0;
	const char *p = item->argument;

	if (p) {
		if (*p == '+') {
			mode = MODE_ADD;
			p++;
		} else if (*p == '-') {
			mode = MODE_DEL;
			p++;
		} else if (*p == '=') {
			mode = MODE_SET;
			p++;
		}
	}

	if ((!p || !*p) && mode != MODE_SET) {
		log_error("Setting file attribute on '%s' needs an attribute "
		"specification.",
		item->path);
		return -EINVAL;
	}

	for (; p && *p; p++) {
		bool found = false;
		for (size_t j = 0; j < ELEMENTSOF(attributes); j++) {
			if (*p == attributes[j].character) {
				unsigned v = attributes[j].value;
				if (mode == MODE_ADD || mode == MODE_SET)
					value |= v;
				mask |= v;
				found = true;
				break;
			}
		}
		if (!found) {
			log_error("Unknown file attribute '%c' on '%s'.", *p, item->path);
			return -EINVAL;
		}
	}

	if (mode == MODE_SET)
		mask |= CHATTR_ALL_FL;

	item->attribute_mask = mask;
	item->attribute_value = value;
	item->attribute_set = true;

	return 0;
}

int fd_set_attribute(Item *i, int fd, const char *path, const struct stat *st) {
	struct stat stbuf;

	if (!i->attribute_set || i->attribute_mask == 0)
		return 0;

	if (!st) {
		if (fstat(fd, &stbuf) < 0)
			return log_error_errno(-errno, "fstat(%s) failed: %s", path,
								   strerror(errno));
			st = &stbuf;
	}

	if (!S_ISREG(st->st_mode) && !S_ISDIR(st->st_mode)) {
		log_debug("Skipping chattr for '%s' (not a regular file or directory).",
				  path);
		return 0;
	}

	unsigned f = i->attribute_value & i->attribute_mask;

	if (!S_ISDIR(st->st_mode))
		f &= ~FS_DIRSYNC_FL;

	log_debug("%s file attributes 0x%08x on %s",
			  arg_dry_run ? "Would set" : "Setting", f & i->attribute_mask, path);

	if (!arg_dry_run) {
		char proc_path[64];
		snprintf(proc_path, sizeof(proc_path), "/proc/self/fd/%d", fd);
		_cleanup_close_ int reopened = open(proc_path, O_RDONLY | O_CLOEXEC);
		int real_fd = reopened >= 0 ? reopened : fd;

		unsigned current;
		if (ioctl(real_fd, FS_IOC_GETFLAGS, &current) < 0) {
			if (errno == ENOTTY || errno == ENOSYS || errno == EOPNOTSUPP) {
				log_debug("File attributes not supported on '%s', ignoring.", path);
				return 0;
			}
			return log_error_errno(-errno,
								   "Failed to get file attributes on '%s': %s", path,
						  strerror(errno));
		}

		unsigned desired = (current & ~i->attribute_mask) | (f & i->attribute_mask);

		if (desired != current) {
			if (ioctl(real_fd, FS_IOC_SETFLAGS, &desired) < 0) {
				if (errno == ENOTTY || errno == ENOSYS || errno == EOPNOTSUPP)
					log_debug("Setting file attributes not supported on '%s', ignoring.",
							  path);
					else
						log_warning("Failed to set file attributes on '%s': %s (ignoring)",
									path, strerror(errno));
			}
		}
	}

	return 0;
}

int path_set_attribute(Item *i, const char *path) {
	if (!i->attribute_set || i->attribute_mask == 0)
		return 0;

	_cleanup_close_ int fd =
	open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_PATH);
	if (fd < 0) {
		if (errno == ENOENT)
			return 0;
		return -errno;
	}
	return fd_set_attribute(i, fd, path, NULL);
}

int fd_reopen(int fd, int parent_fd, const char *basename,
              const char *resolved_path, int open_flags) {
	(void)parent_fd;
	(void)basename;
	(void)resolved_path;

	char procpath[64];
	snprintf(procpath, sizeof(procpath), "/proc/self/fd/%d", fd);

	int new_fd = open(procpath, open_flags);
	if (new_fd < 0)
		return -errno;
	return new_fd;
}

int acl_get_for_fd(int fd, int parent_fd, const char *basename,
               const char *resolved_path, acl_type_t type, acl_t *ret) {
	(void)parent_fd;
	(void)basename;
	(void)resolved_path;

	char procpath[64];
	snprintf(procpath, sizeof(procpath), "/proc/self/fd/%d", fd);

	acl_t a = acl_get_file(procpath, type);
	if (!a)
		return -errno;
	*ret = a;
	return 0;
}

int acl_set_for_fd(int fd, int parent_fd, const char *basename,
               const char *resolved_path, acl_type_t type, acl_t acl) {
	(void)parent_fd;
	(void)basename;
	(void)resolved_path;

	char procpath[64];
	snprintf(procpath, sizeof(procpath), "/proc/self/fd/%d", fd);

	if (acl_set_file(procpath, type, acl) < 0)
		return -errno;
	return 0;
}
