/*
 * Copyright (C) 2016 Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "mount-opt.h"

#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>

#include "fault-injection.h"
#include "privs.h"
#include "string-utils.h"
#include "utils.h"

const char *sc_mount_opt2str(char *buf, size_t buf_size, unsigned long flags)
{
	unsigned long used = 0;
	sc_string_init(buf, buf_size);

#define F(FLAG, TEXT) do {                                         \
    if (flags & (FLAG)) {                                          \
      sc_string_append(buf, buf_size, #TEXT ","); flags ^= (FLAG); \
    }                                                              \
  } while (0)

	F(MS_RDONLY, ro);
	F(MS_NOSUID, nosuid);
	F(MS_NODEV, nodev);
	F(MS_NOEXEC, noexec);
	F(MS_SYNCHRONOUS, sync);
	F(MS_REMOUNT, remount);
	F(MS_MANDLOCK, mand);
	F(MS_DIRSYNC, dirsync);
	F(MS_NOATIME, noatime);
	F(MS_NODIRATIME, nodiratime);
	if (flags & MS_BIND) {
		if (flags & MS_REC) {
			sc_string_append(buf, buf_size, "rbind,");
			used |= MS_REC;
		} else {
			sc_string_append(buf, buf_size, "bind,");
		}
		flags ^= MS_BIND;
	}
	F(MS_MOVE, move);
	// The MS_REC flag handled separately by affected flags (MS_BIND,
	// MS_PRIVATE, MS_SLAVE, MS_SHARED)
	// XXX: kernel has MS_VERBOSE, glibc has MS_SILENT, both use the same constant
	F(MS_SILENT, silent);
	F(MS_POSIXACL, acl);
	F(MS_UNBINDABLE, unbindable);
	if (flags & MS_PRIVATE) {
		if (flags & MS_REC) {
			sc_string_append(buf, buf_size, "rprivate,");
			used |= MS_REC;
		} else {
			sc_string_append(buf, buf_size, "private,");
		}
		flags ^= MS_PRIVATE;
	}
	if (flags & MS_SLAVE) {
		if (flags & MS_REC) {
			sc_string_append(buf, buf_size, "rslave,");
			used |= MS_REC;
		} else {
			sc_string_append(buf, buf_size, "slave,");
		}
		flags ^= MS_SLAVE;
	}
	if (flags & MS_SHARED) {
		if (flags & MS_REC) {
			sc_string_append(buf, buf_size, "rshared,");
			used |= MS_REC;
		} else {
			sc_string_append(buf, buf_size, "shared,");
		}
		flags ^= MS_SHARED;
	}
	flags ^= used;		// this is just for MS_REC
	F(MS_RELATIME, relatime);
	F(MS_KERNMOUNT, kernmount);
	F(MS_I_VERSION, iversion);
	F(MS_STRICTATIME, strictatime);
#ifndef MS_LAZYTIME
#define MS_LAZYTIME (1<<25)
#endif
	F(MS_LAZYTIME, lazytime);
#ifndef MS_NOSEC
#define MS_NOSEC (1 << 28)
#endif
	F(MS_NOSEC, nosec);
#ifndef MS_BORN
#define MS_BORN (1 << 29)
#endif
	F(MS_BORN, born);
	F(MS_ACTIVE, active);
	F(MS_NOUSER, nouser);
#undef F
	// Render any flags that are unaccounted for.
	if (flags) {
		char of[128] = { 0 };
		sc_must_snprintf(of, sizeof of, "%#lx", flags);
		sc_string_append(buf, buf_size, of);
	}
	// Chop the excess comma from the end.
	size_t len = strnlen(buf, buf_size);
	if (len > 0 && buf[len - 1] == ',') {
		buf[len - 1] = 0;
	}
	return buf;
}

const char *sc_mount_cmd(char *buf, size_t buf_size, const char *source, const char
			 *target, const char *fs_type, unsigned long mountflags, const
			 void *data)
{
	sc_string_init(buf, buf_size);
	sc_string_append(buf, buf_size, "mount");

	// Add filesysystem type if it's there and doesn't have the special value "none"
	if (fs_type != NULL && strncmp(fs_type, "none", 5) != 0) {
		sc_string_append(buf, buf_size, " -t ");
		sc_string_append(buf, buf_size, fs_type);
	}
	// Check for some special, dedicated options, that aren't represented with
	// the generic mount option argument (mount -o ...), by collecting those
	// options that we will display as command line arguments in
	// used_special_flags. This is used below to filter out these arguments
	// from mount_flags when calling sc_mount_opt2str().
	int used_special_flags = 0;

	// Bind-ounts (bind)
	if (mountflags & MS_BIND) {
		if (mountflags & MS_REC) {
			sc_string_append(buf, buf_size, " --rbind");
			used_special_flags |= MS_REC;
		} else {
			sc_string_append(buf, buf_size, " --bind");
		}
		used_special_flags |= MS_BIND;
	}
	// Moving mount point location (move)
	if (mountflags & MS_MOVE) {
		sc_string_append(buf, buf_size, " --move");
		used_special_flags |= MS_MOVE;
	}
	// Shared subtree operations (shared, slave, private, unbindable).
	if (MS_SHARED & mountflags) {
		if (mountflags & MS_REC) {
			sc_string_append(buf, buf_size, " --make-rshared");
			used_special_flags |= MS_REC;
		} else {
			sc_string_append(buf, buf_size, " --make-shared");
		}
		used_special_flags |= MS_SHARED;
	}

	if (MS_SLAVE & mountflags) {
		if (mountflags & MS_REC) {
			sc_string_append(buf, buf_size, " --make-rslave");
			used_special_flags |= MS_REC;
		} else {
			sc_string_append(buf, buf_size, " --make-slave");
		}
		used_special_flags |= MS_SLAVE;
	}

	if (MS_PRIVATE & mountflags) {
		if (mountflags & MS_REC) {
			sc_string_append(buf, buf_size, " --make-rprivate");
			used_special_flags |= MS_REC;
		} else {
			sc_string_append(buf, buf_size, " --make-private");
		}
		used_special_flags |= MS_PRIVATE;
	}

	if (MS_UNBINDABLE & mountflags) {
		if (mountflags & MS_REC) {
			sc_string_append(buf, buf_size, " --make-runbindable");
			used_special_flags |= MS_REC;
		} else {
			sc_string_append(buf, buf_size, " --make-unbindable");
		}
		used_special_flags |= MS_UNBINDABLE;
	}
	// If regular option syntax exists then use it.
	if (mountflags & ~used_special_flags) {
		char opts_buf[1000] = { 0 };
		sc_mount_opt2str(opts_buf, sizeof opts_buf, mountflags &
				 ~used_special_flags);
		sc_string_append(buf, buf_size, " -o ");
		sc_string_append(buf, buf_size, opts_buf);
	}
	// Add source and target locations
	if (source != NULL && strncmp(source, "none", 5) != 0) {
		sc_string_append(buf, buf_size, " ");
		sc_string_append(buf, buf_size, source);
	}
	if (target != NULL && strncmp(target, "none", 5) != 0) {
		sc_string_append(buf, buf_size, " ");
		sc_string_append(buf, buf_size, target);
	}

	return buf;
}

const char *sc_umount_cmd(char *buf, size_t buf_size, const char *target,
			  int flags)
{
	sc_string_init(buf, buf_size);
	sc_string_append(buf, buf_size, "umount");

	if (flags & MNT_FORCE) {
		sc_string_append(buf, buf_size, " --force");
	}

	if (flags & MNT_DETACH) {
		sc_string_append(buf, buf_size, " --lazy");
	}
	if (flags & MNT_EXPIRE) {
		// NOTE: there's no real command line option for MNT_EXPIRE
		sc_string_append(buf, buf_size, " --expire");
	}
	if (flags & UMOUNT_NOFOLLOW) {
		// NOTE: there's no real command line option for UMOUNT_NOFOLLOW
		sc_string_append(buf, buf_size, " --no-follow");
	}
	if (target != NULL) {
		sc_string_append(buf, buf_size, " ");
		sc_string_append(buf, buf_size, target);
	}

	return buf;
}

#ifndef SNAP_CONFINE_DEBUG_BUILD
static const char *use_debug_build =
    "(disabled) use debug build to see details";
#endif

static bool sc_do_mount_ex(const char *source, const char *target,
			   const char *fs_type,
			   unsigned long mountflags, const void *data,
			   bool optional)
{
	char buf[10000] = { 0 };
	const char *mount_cmd = NULL;

	if (sc_is_debug_enabled()) {
#ifdef SNAP_CONFINE_DEBUG_BUILD
		mount_cmd = sc_mount_cmd(buf, sizeof(buf), source,
					 target, fs_type, mountflags, data);
#else
		mount_cmd = use_debug_build;
#endif
		debug("performing operation: %s", mount_cmd);
	}
	if (sc_faulty("mount", NULL)
	    || mount(source, target, fs_type, mountflags, data) < 0) {
		int saved_errno = errno;
		if (optional && saved_errno == ENOENT) {
			// The special-cased value that is allowed to fail.
			return false;
		}
		// Drop privileges so that we can compute our nice error message
		// without risking an attack on one of the string functions there.
		sc_privs_drop();

		// Compute the equivalent mount command.
		mount_cmd = sc_mount_cmd(buf, sizeof(buf), source,
					 target, fs_type, mountflags, data);
		// Restore errno and die.
		errno = saved_errno;
		die("cannot perform operation: %s", mount_cmd);
	}
	return true;
}

void sc_do_mount(const char *source, const char *target,
		 const char *fs_type, unsigned long mountflags,
		 const void *data)
{
	(void)sc_do_mount_ex(source, target, fs_type, mountflags, data, false);
}

bool sc_do_optional_mount(const char *source, const char *target,
			  const char *fs_type, unsigned long mountflags,
			  const void *data)
{
	return sc_do_mount_ex(source, target, fs_type, mountflags, data, true);
}

void sc_do_umount(const char *target, int flags)
{
	char buf[10000] = { 0 };
	const char *umount_cmd = NULL;

	if (sc_is_debug_enabled()) {
#ifdef SNAP_CONFINE_DEBUG_BUILD
		umount_cmd = sc_umount_cmd(buf, sizeof(buf), target, flags);
#else
		umount_cmd = use_debug_build;
#endif
		debug("performing operation: %s", umount_cmd);
	}
	if (sc_faulty("umount", NULL) || umount2(target, flags) < 0) {
		// Save errno as ensure can clobber it.
		int saved_errno = errno;

		// Drop privileges so that we can compute our nice error message
		// without risking an attack on one of the string functions there.
		sc_privs_drop();

		// Compute the equivalent umount command.
		umount_cmd = sc_umount_cmd(buf, sizeof(buf), target, flags);
		// Restore errno and die.
		errno = saved_errno;
		die("cannot perform operation: %s", umount_cmd);
	}
}
