/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
 * This file is part of libmount from util-linux project.
 *
 * Copyright (C) 2022 Karel Zak <kzak@redhat.com>
 *
 * libmount is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 *
 * This is X-mount.owner=, X-mount.group= and X-mount.mode= implementation.
 *
 * Please, see the comment in libmount/src/hooks.c to understand how hooks work.
 */
#include <sched.h>

#include "mountP.h"
#include "pathnames.h"
#include "fileutils.h"

struct hook_data {
	uid_t owner;
	gid_t group;
	mode_t mode;
};

/* de-initiallize this module */
static int hookset_deinit(struct libmnt_context *cxt, const struct libmnt_hookset *hs)
{
	void *data;

	DBG(HOOK, ul_debugobj(hs, "deinit '%s'", hs->name));

	/* remove all our hooks and free hook data */
	while (mnt_context_remove_hook(cxt, hs, 0, &data) == 0) {
		free(data);
		data = NULL;
	}

	return 0;
}

static int hook_post(
			struct libmnt_context *cxt,
			const struct libmnt_hookset *hs __attribute__((__unused__)),
			void *data)
{
	struct hook_data *hd = (struct hook_data *) data;
	const char *target;
	int rc = 0, fd;

	assert(cxt);

	if (!hd || !cxt->fs)
		return 0;

	target = mnt_fs_get_target(cxt->fs);
	if (!target)
		return 0;

	/* fd_target is pinned in restricted mode (see prepare_target()),
	 * for root open it here to keep chmod/chown fd-based too */
	if (mnt_context_target_fd_required(cxt))
		fd = mnt_context_get_target_fd(cxt);
	else
		fd = open(target, O_PATH | O_CLOEXEC);

	if (fd < 0)
		return -MNT_ERR_CHMOD;

	if (hd->owner != (uid_t) -1 || hd->group != (uid_t) -1) {
		DBG(CXT, ul_debugobj(cxt, " fchownat(%s, %u, %u)", target, hd->owner, hd->group));
		if (fchownat(fd, "", hd->owner, hd->group, AT_EMPTY_PATH) == -1)
			rc = -MNT_ERR_CHOWN;
	}

	if (!rc && hd->mode != (mode_t) -1) {
		char buf[sizeof(_PATH_PROC_FDDIR) + 1 + sizeof(stringify_value(INT_MAX))];

		snprintf(buf, sizeof(buf), _PATH_PROC_FDDIR "/%d", fd);
		DBG(CXT, ul_debugobj(cxt, " chmod(%s, %04o)", buf, hd->mode));
		if (chmod(buf, hd->mode) == -1)
			rc = -MNT_ERR_CHMOD;
	}

	if (!mnt_context_target_fd_required(cxt))
		close(fd);
	return rc;
}

static inline struct hook_data *new_hook_data(void)
{
	struct hook_data *hd = calloc(1, sizeof(*hd));

	if (!hd)
		return NULL;

	hd->owner = (uid_t) -1;
	hd->group = (gid_t) -1;
	hd->mode = (mode_t) -1;
	return hd;
}

static int hook_prepare_options(
			struct libmnt_context *cxt,
			const struct libmnt_hookset *hs,
			void *data __attribute__((__unused__)))
{
	struct hook_data *hd = NULL;
	struct libmnt_optlist *ol;
	struct libmnt_opt *opt;
	int rc = 0;

	assert(cxt);
	assert(cxt->map_userspace);

	ol = mnt_context_get_optlist(cxt);
	if (!ol)
		return -ENOMEM;

	opt = mnt_optlist_get_named(ol, "X-mount.owner", cxt->map_userspace);
	if (opt) {
		const char *value = mnt_opt_get_value(opt);
		if (!value)
			goto fail;
		if (!hd) {
			hd = new_hook_data();
			if (!hd)
				goto fail;
		}
		if (mnt_parse_uid(value, strlen(value), &hd->owner))
			goto fail;
	}

	opt = mnt_optlist_get_named(ol, "X-mount.group", cxt->map_userspace);
	if (opt) {
		const char *value = mnt_opt_get_value(opt);
		if (!value)
			goto fail;
		if (!hd) {
			hd = new_hook_data();
			if (!hd)
				goto fail;
		}
		if (mnt_parse_gid(value, strlen(value), &hd->group))
			goto fail;
	}

	opt = mnt_optlist_get_named(ol, "X-mount.mode", cxt->map_userspace);
	if (opt) {
		const char *value = mnt_opt_get_value(opt);
		if (!value)
			goto fail;
		if (!hd) {
			hd = new_hook_data();
			if (!hd)
				goto fail;
		}
		if (mnt_parse_mode(value, strlen(value), &hd->mode))
			goto fail;
	}

	if (hd) {
		DBG(CXT, ul_debugobj(cxt, " wanted ownership %d:%d, mode %04o",
					hd->owner, hd->group, hd->mode));
		rc = mnt_context_append_hook(cxt, hs,
				MNT_STAGE_POST,
				hd, hook_post);
		if (rc < 0)
			goto fail;
	}
	return 0;
fail:
	if (rc == 0)
		rc = -MNT_ERR_MOUNTOPT;
	free(hd);
	return rc;
}


const struct libmnt_hookset hookset_owner =
{
	.name = "__owner",

	.firststage = MNT_STAGE_PREP_OPTIONS,
	.firstcall = hook_prepare_options,

	.deinit = hookset_deinit
};
