/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
 * This file is part of libmount from util-linux project.
 *
 * Copyright (C) 2011-2018 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.
 */

/**
 * SECTION: update
 * @title: Tables update
 * @short_description: userspace mount information management
 *
 * The struct libmnt_update provides an abstraction to manage mount options in
 * userspace independently of system configuration. The userspace mount options
 * (e.g. user=) are stored in the /run/mount/utab file.
 *
 * It's recommended to use high-level struct libmnt_context API.
 */
#include <sys/file.h>
#include <fcntl.h>
#include <signal.h>

#include "mountP.h"
#include "mangle.h"
#include "pathnames.h"
#include "strutils.h"

struct libmnt_update {
	char		*target;
	struct libmnt_fs *fs;
	char		*filename;
	unsigned long	mountflags;
	int		ready;

	struct libmnt_table *mountinfo;
};

static int set_fs_root(struct libmnt_update *upd, struct libmnt_fs *fs, unsigned long mountflags);
static int utab_new_entry(struct libmnt_update *upd, struct libmnt_fs *fs, unsigned long mountflags);

/**
 * mnt_new_update:
 *
 * Returns: newly allocated update handler
 */
struct libmnt_update *mnt_new_update(void)
{
	struct libmnt_update *upd;

	upd = calloc(1, sizeof(*upd));
	if (!upd)
		return NULL;

	DBG(UPDATE, ul_debugobj(upd, "allocate"));
	return upd;
}

/**
 * mnt_free_update:
 * @upd: update
 *
 * Deallocates struct libmnt_update handler.
 */
void mnt_free_update(struct libmnt_update *upd)
{
	if (!upd)
		return;

	DBG(UPDATE, ul_debugobj(upd, "free"));

	mnt_unref_fs(upd->fs);
	mnt_unref_table(upd->mountinfo);
	free(upd->target);
	free(upd->filename);
	free(upd);
}

/*
 * Returns 0 on success, <0 in case of error.
 */
int mnt_update_set_filename(struct libmnt_update *upd, const char *filename)
{
	const char *path = NULL;
	int rw = 0;

	if (!upd)
		return -EINVAL;

	/* filename explicitly defined */
	if (filename) {
		char *p = strdup(filename);
		if (!p)
			return -ENOMEM;

		free(upd->filename);
		upd->filename = p;
	}

	if (upd->filename)
		return 0;

	/* detect tab filename -- /run/mount/utab
	 */
	path = NULL;
	mnt_has_regular_utab(&path, &rw);
	if (!rw)
		return -EACCES;
	upd->filename = strdup(path);
	if (!upd->filename)
		return -ENOMEM;

	return 0;
}

/**
 * mnt_update_get_filename:
 * @upd: update
 *
 * This function returns the file name of the up-dated file.
 *
 * Returns: pointer to filename that will be updated or NULL in case of error.
 */
const char *mnt_update_get_filename(struct libmnt_update *upd)
{
	return upd ? upd->filename : NULL;
}

/**
 * mnt_update_is_ready:
 * @upd: update handler
 *
 * Returns: 1 if entry described by @upd is successfully prepared and will be
 * written to the utab file.
 */
int mnt_update_is_ready(struct libmnt_update *upd)
{
	return upd ? upd->ready : FALSE;
}

/**
 * mnt_update_set_fs:
 * @upd: update handler
 * @mountflags: MS_* flags
 * @target: umount target, must be NULL for mount
 * @fs: mount filesystem description, must be NULL for umount
 *
 * Returns: <0 in case on error, 0 on success, 1 if update is unnecessary.
 */
int mnt_update_set_fs(struct libmnt_update *upd, unsigned long mountflags,
		      const char *target, struct libmnt_fs *fs)
{
	int rc;

	if (!upd)
		return -EINVAL;
	if ((mountflags & MS_MOVE) && (!fs || !mnt_fs_get_srcpath(fs)))
		return -EINVAL;
	if (target && fs)
		return -EINVAL;

	DBG(UPDATE, ul_debugobj(upd,
			"resetting FS [target=%s, flags=0x%08lx]",
			target, mountflags));
	if (fs) {
		DBG(UPDATE, ul_debugobj(upd, "FS template:"));
		DBG(UPDATE, mnt_fs_print_debug(fs, stderr));
	}

	mnt_unref_fs(upd->fs);
	free(upd->target);
	upd->ready = FALSE;
	upd->fs = NULL;
	upd->target = NULL;
	upd->mountflags = 0;

	if (mountflags & MS_PROPAGATION)
		return 1;

	upd->mountflags = mountflags;

	rc = mnt_update_set_filename(upd, NULL);
	if (rc) {
		DBG(UPDATE, ul_debugobj(upd, "no writable file available [rc=%d]", rc));
		return rc;	/* error or no file available (rc = 1) */
	}
	if (target) {
		upd->target = strdup(target);
		if (!upd->target)
			return -ENOMEM;

	} else if (fs) {
		if (!(mountflags & MS_MOVE)) {
			rc = utab_new_entry(upd, fs, mountflags);
			if (rc)
			       return rc;
		} else {
			upd->fs = mnt_copy_mtab_fs(fs);
			if (!upd->fs)
				return -ENOMEM;
		}
	}

	DBG(UPDATE, ul_debugobj(upd, "ready"));
	upd->ready = TRUE;
	return 0;
}

/**
 * mnt_update_get_fs:
 * @upd: update
 *
 * Returns: update filesystem entry or NULL
 */
struct libmnt_fs *mnt_update_get_fs(struct libmnt_update *upd)
{
	return upd ? upd->fs : NULL;
}

/**
 * mnt_update_get_mflags:
 * @upd: update
 *
 * Returns: mount flags as was set by mnt_update_set_fs()
 */
unsigned long mnt_update_get_mflags(struct libmnt_update *upd)
{
	return upd ? upd->mountflags : 0;
}

/**
 * mnt_update_force_rdonly:
 * @upd: update
 * @rdonly: is read-only?
 *
 * Returns: 0 on success and negative number in case of error.
 */
int mnt_update_force_rdonly(struct libmnt_update *upd, int rdonly)
{
	int rc = 0;

	if (!upd || !upd->fs)
		return -EINVAL;

	if (rdonly && (upd->mountflags & MS_RDONLY))
		return 0;
	if (!rdonly && !(upd->mountflags & MS_RDONLY))
		return 0;

	if (rdonly)
		upd->mountflags &= ~MS_RDONLY;
	else
		upd->mountflags |= MS_RDONLY;

	return rc;
}


/*
 * Allocates an utab entry (upd->fs) for mount/remount. This function should be
 * called *before* mount(2) syscall. The @fs is used as a read-only template.
 *
 * Returns: 0 on success, negative number on error, 1 if utab's update is
 *          unnecessary.
 */
static int utab_new_entry(struct libmnt_update *upd, struct libmnt_fs *fs,
			  unsigned long mountflags)
{
	int rc = 0;
	const char *o, *a;
	char *u = NULL;

	assert(fs);
	assert(upd);
	assert(upd->fs == NULL);
	assert(!(mountflags & MS_MOVE));

	DBG(UPDATE, ul_debug("prepare utab entry"));

	o = mnt_fs_get_user_options(fs);
	a = mnt_fs_get_attributes(fs);
	upd->fs = NULL;

	if (o) {
		/* remove non-mtab options */
		rc = mnt_optstr_get_options(o, &u,
				mnt_get_builtin_optmap(MNT_USERSPACE_MAP),
				MNT_NOMTAB);
		if (rc)
			goto err;
	}

	if (!u && !a) {
		DBG(UPDATE, ul_debug("utab entry unnecessary (no options)"));
		return 1;
	}

	/* allocate the entry */
	upd->fs = mnt_copy_fs(NULL, fs);
	if (!upd->fs) {
		rc = -ENOMEM;
		goto err;
	}

	rc = mnt_fs_set_options(upd->fs, u);
	if (rc)
		goto err;
	rc = mnt_fs_set_attributes(upd->fs, a);
	if (rc)
		goto err;

	if (!(mountflags & MS_REMOUNT)) {
		rc = set_fs_root(upd, fs, mountflags);
		if (rc)
			goto err;
	}

	free(u);
	DBG(UPDATE, ul_debug("utab entry OK"));
	return 0;
err:
	free(u);
	mnt_unref_fs(upd->fs);
	upd->fs = NULL;
	return rc;
}

/*
 * Sets fs-root and fs-type to @upd->fs according to the @fs template and
 * @mountfalgs. For MS_BIND mountflag it reads information about the source
 * filesystem from /proc/self/mountinfo.
 */
static int set_fs_root(struct libmnt_update *upd, struct libmnt_fs *fs,
		       unsigned long mountflags)
{
	struct libmnt_fs *src_fs;
	char *fsroot = NULL;
	const char *src, *fstype;
	int rc = 0;

	DBG(UPDATE, ul_debug("setting FS root"));

	assert(upd);
	assert(upd->fs);
	assert(fs);

	fstype = mnt_fs_get_fstype(fs);

	if (mountflags & MS_BIND) {
		if (!upd->mountinfo)
			upd->mountinfo = mnt_new_table_from_file(_PATH_PROC_MOUNTINFO);
		src = mnt_fs_get_srcpath(fs);
		if (src) {
			 rc = mnt_fs_set_bindsrc(upd->fs, src);
			 if (rc)
				 goto err;
		}

	} else if (fstype && (strcmp(fstype, "btrfs") == 0 || strcmp(fstype, "auto") == 0)) {
		if (!upd->mountinfo)
			upd->mountinfo = mnt_new_table_from_file(_PATH_PROC_MOUNTINFO);
	}

	src_fs = mnt_table_get_fs_root(upd->mountinfo, fs,
					mountflags, &fsroot);
	if (src_fs) {
		src = mnt_fs_get_srcpath(src_fs);
		rc = mnt_fs_set_source(upd->fs, src);
		if (rc)
			goto err;

		mnt_fs_set_fstype(upd->fs, mnt_fs_get_fstype(src_fs));
	}

	upd->fs->root = fsroot;
	return 0;
err:
	free(fsroot);
	return rc;
}

/* mtab and fstab update -- returns zero on success
 */
static int fprintf_mtab_fs(FILE *f, struct libmnt_fs *fs)
{
	const char *o, *src, *fstype, *comm;
	char *m1, *m2, *m3, *m4;
	int rc;

	assert(fs);
	assert(f);

	comm = mnt_fs_get_comment(fs);
	src = mnt_fs_get_source(fs);
	fstype = mnt_fs_get_fstype(fs);
	o = mnt_fs_get_options(fs);

	m1 = src ? mangle(src) : "none";
	m2 = mangle(mnt_fs_get_target(fs));
	m3 = fstype ? mangle(fstype) : "none";
	m4 = o ? mangle(o) : "rw";

	if (m1 && m2 && m3 && m4) {
		if (comm)
			fputs(comm, f);
		rc = fprintf(f, "%s %s %s %s %d %d\n",
				m1, m2, m3, m4,
				mnt_fs_get_freq(fs),
				mnt_fs_get_passno(fs));
		if (rc > 0)
			rc = 0;
	} else
		rc = -ENOMEM;

	if (src)
		free(m1);
	free(m2);
	if (fstype)
		free(m3);
	if (o)
		free(m4);

	return rc;
}

static int fprintf_utab_fs(FILE *f, struct libmnt_fs *fs)
{
	char *p;
	int rc = 0;

	if (!fs || !f)
		return -EINVAL;

	if (mnt_fs_get_id(fs) > 0) {
		rc = fprintf(f, "ID=%d ", mnt_fs_get_id(fs));
	}
	if (rc >= 0) {
		p = mangle(mnt_fs_get_source(fs));
		if (p) {
			rc = fprintf(f, "SRC=%s ", p);
			free(p);
		}
	}
	if (rc >= 0) {
		p = mangle(mnt_fs_get_target(fs));
		if (p) {
			rc = fprintf(f, "TARGET=%s ", p);
			free(p);
		}
	}
	if (rc >= 0) {
		p = mangle(mnt_fs_get_root(fs));
		if (p) {
			rc = fprintf(f, "ROOT=%s ", p);
			free(p);
		}
	}
	if (rc >= 0) {
		p = mangle(mnt_fs_get_bindsrc(fs));
		if (p) {
			rc = fprintf(f, "BINDSRC=%s ", p);
			free(p);
		}
	}
	if (rc >= 0) {
		p = mangle(mnt_fs_get_attributes(fs));
		if (p) {
			rc = fprintf(f, "ATTRS=%s ", p);
			free(p);
		}
	}
	if (rc >= 0) {
		p = mangle(mnt_fs_get_user_options(fs));
		if (p) {
			rc = fprintf(f, "OPTS=%s", p);
			free(p);
		}
	}
	if (rc >= 0)
		rc = fprintf(f, "\n");

	if (rc > 0)
		rc = 0;	/* success */
	return rc;
}

static int update_table(struct libmnt_update *upd, struct libmnt_table *tb)
{
	FILE *f;
	int rc, fd;
	char *uq = NULL;

	if (!tb || !upd->filename)
		return -EINVAL;

	DBG(UPDATE, ul_debugobj(upd, "%s: updating", upd->filename));

	fd = mnt_open_uniq_filename(upd->filename, &uq);
	if (fd < 0)
		return fd;	/* error */

	f = fdopen(fd, "w" UL_CLOEXECSTR);
	if (f) {
		struct stat st;
		struct libmnt_iter itr;
		struct libmnt_fs *fs;

		mnt_reset_iter(&itr, MNT_ITER_FORWARD);

		if (tb->comms && mnt_table_get_intro_comment(tb))
			fputs(mnt_table_get_intro_comment(tb), f);

		while(mnt_table_next_fs(tb, &itr, &fs) == 0) {
			rc = fprintf_utab_fs(f, fs);
			if (rc) {
				DBG(UPDATE, ul_debugobj(upd,
					"%s: write entry failed: %m", uq));
				goto leave;
			}
		}
		if (tb->comms && mnt_table_get_trailing_comment(tb))
			fputs(mnt_table_get_trailing_comment(tb), f);

		if (fflush(f) != 0) {
			rc = -errno;
			DBG(UPDATE, ul_debugobj(upd, "%s: fflush failed: %m", uq));
			goto leave;
		}

		rc = fchmod(fd, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH) ? -errno : 0;

		if (!rc && stat(upd->filename, &st) == 0)
			/* Copy uid/gid from the present file before renaming. */
			rc = fchown(fd, st.st_uid, st.st_gid) ? -errno : 0;

		fclose(f);
		f = NULL;

		if (!rc)
			rc = rename(uq, upd->filename) ? -errno : 0;
	} else {
		rc = -errno;
		close(fd);
	}

leave:
	if (f)
		fclose(f);

	unlink(uq);	/* be paranoid */
	free(uq);
	DBG(UPDATE, ul_debugobj(upd, "%s: done [rc=%d]", upd->filename, rc));
	return rc;
}

/**
 * mnt_table_write_file
 * @tb: parsed file (e.g. fstab)
 * @file: target
 *
 * This function writes @tb to @file.
 *
 * Returns: 0 on success, negative number on error.
 */
int mnt_table_write_file(struct libmnt_table *tb, FILE *file)
{
	int rc = 0;
	struct libmnt_iter itr;
	struct libmnt_fs *fs;

	if (tb->comms && mnt_table_get_intro_comment(tb))
		fputs(mnt_table_get_intro_comment(tb), file);

	mnt_reset_iter(&itr, MNT_ITER_FORWARD);
	while(mnt_table_next_fs(tb, &itr, &fs) == 0) {
		rc = fprintf_mtab_fs(file, fs);
		if (rc)
			return rc;
	}
	if (tb->comms && mnt_table_get_trailing_comment(tb))
		fputs(mnt_table_get_trailing_comment(tb), file);

	if (fflush(file) != 0)
		rc = -errno;

	DBG(TAB, ul_debugobj(tb, "write file done [rc=%d]", rc));
	return rc;
}

/**
 * mnt_table_replace_file
 * @tb: parsed file (e.g. fstab)
 * @filename: target
 *
 * This function replaces @file by the new content from @tb.
 *
 * Returns: 0 on success, negative number on error.
 */
int mnt_table_replace_file(struct libmnt_table *tb, const char *filename)
{
	int fd, rc = 0;
	FILE *f;
	char *uq = NULL;

	DBG(TAB, ul_debugobj(tb, "%s: replacing", filename));

	fd = mnt_open_uniq_filename(filename, &uq);
	if (fd < 0)
		return fd;	/* error */

	f = fdopen(fd, "w" UL_CLOEXECSTR);
	if (f) {
		struct stat st;

		mnt_table_write_file(tb, f);

		if (fflush(f) != 0) {
			rc = -errno;
			DBG(UPDATE, ul_debug("%s: fflush failed: %m", uq));
			goto leave;
		}

		rc = fchmod(fd, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH) ? -errno : 0;

		if (!rc && stat(filename, &st) == 0)
			/* Copy uid/gid from the present file before renaming. */
			rc = fchown(fd, st.st_uid, st.st_gid) ? -errno : 0;

		fclose(f);
		f = NULL;

		if (!rc)
			rc = rename(uq, filename) ? -errno : 0;
	} else {
		rc = -errno;
		close(fd);
	}

leave:
	if (f)
		fclose(f);
	unlink(uq);
	free(uq);

	DBG(TAB, ul_debugobj(tb, "replace done [rc=%d]", rc));
	return rc;
}

static int add_file_entry(struct libmnt_table *tb, struct libmnt_update *upd)
{
	struct libmnt_fs *fs;

	assert(upd);

	fs = mnt_copy_fs(NULL, upd->fs);
	if (!fs)
		return -ENOMEM;

	mnt_table_add_fs(tb, fs);
	mnt_unref_fs(fs);

	return update_table(upd, tb);
}

static int update_add_entry(struct libmnt_update *upd, struct libmnt_lock *lc)
{
	struct libmnt_table *tb;
	int rc = 0;

	assert(upd);
	assert(upd->fs);

	DBG(UPDATE, ul_debugobj(upd, "%s: add entry", upd->filename));

	if (lc)
		rc = mnt_lock_file(lc);
	if (rc)
		return -MNT_ERR_LOCK;

	tb = __mnt_new_table_from_file(upd->filename, MNT_FMT_UTAB, 1);
	if (tb)
		rc = add_file_entry(tb, upd);
	if (lc)
		mnt_unlock_file(lc);

	mnt_unref_table(tb);
	return rc;
}

static int update_remove_entry(struct libmnt_update *upd, struct libmnt_lock *lc)
{
	struct libmnt_table *tb;
	int rc = 0;

	assert(upd);
	assert(upd->target);

	DBG(UPDATE, ul_debugobj(upd, "%s: remove entry", upd->filename));

	if (lc)
		rc = mnt_lock_file(lc);
	if (rc)
		return -MNT_ERR_LOCK;

	tb = __mnt_new_table_from_file(upd->filename, MNT_FMT_UTAB, 1);
	if (tb) {
		struct libmnt_fs *rem = mnt_table_find_target(tb, upd->target, MNT_ITER_BACKWARD);
		if (rem) {
			mnt_table_remove_fs(tb, rem);
			rc = update_table(upd, tb);
		}
	}
	if (lc)
		mnt_unlock_file(lc);

	mnt_unref_table(tb);
	return rc;
}

static int update_modify_target(struct libmnt_update *upd, struct libmnt_lock *lc)
{
	struct libmnt_table *tb = NULL;
	int rc = 0;

	assert(upd);
	DBG(UPDATE, ul_debugobj(upd, "%s: modify target", upd->filename));

	if (lc)
		rc = mnt_lock_file(lc);
	if (rc)
		return -MNT_ERR_LOCK;

	tb = __mnt_new_table_from_file(upd->filename, MNT_FMT_UTAB, 1);
	if (tb) {
		const char *upd_source = mnt_fs_get_srcpath(upd->fs);
		const char *upd_target = mnt_fs_get_target(upd->fs);
		struct libmnt_iter itr;
		struct libmnt_fs *fs;
		char *cn_target = mnt_resolve_path(upd_target, NULL);

		if (!cn_target) {
			rc = -ENOMEM;
			goto done;
		}

		mnt_reset_iter(&itr, MNT_ITER_BACKWARD);
		while (mnt_table_next_fs(tb, &itr, &fs) == 0) {
			char *p;
			const char *e;

			e = startswith(mnt_fs_get_target(fs), upd_source);
			if (!e || (*e && *e != '/'))
				continue;
			if (*e == '/')
				e++;		/* remove extra '/' */

			/* no subdirectory, replace entire path */
			if (!*e)
				rc = mnt_fs_set_target(fs, cn_target);

			/* update start of the path, keep subdirectory */
			else if (asprintf(&p, "%s/%s", cn_target, e) > 0) {
				rc = mnt_fs_set_target(fs, p);
				free(p);
			} else
				rc = -ENOMEM;

			if (rc < 0)
				break;
		}

		if (!rc)
			rc = update_table(upd, tb);
		free(cn_target);
	}

done:
	if (lc)
		mnt_unlock_file(lc);

	mnt_unref_table(tb);
	return rc;
}

static int update_modify_options(struct libmnt_update *upd, struct libmnt_lock *lc)
{
	struct libmnt_table *tb = NULL;
	int rc = 0;
	struct libmnt_fs *fs;

	assert(upd);
	assert(upd->fs);

	DBG(UPDATE, ul_debugobj(upd, "%s: modify options", upd->filename));

	fs = upd->fs;

	if (lc)
		rc = mnt_lock_file(lc);
	if (rc)
		return -MNT_ERR_LOCK;

	tb = __mnt_new_table_from_file(upd->filename, MNT_FMT_UTAB, 1);
	if (tb) {
		struct libmnt_fs *cur = mnt_table_find_target(tb,
					mnt_fs_get_target(fs),
					MNT_ITER_BACKWARD);
		if (cur) {
			rc = mnt_fs_set_attributes(cur,	mnt_fs_get_attributes(fs));
			if (!rc)
				rc = mnt_fs_set_options(cur, mnt_fs_get_options(fs));
			if (!rc)
				rc = update_table(upd, tb);
		} else
			rc = add_file_entry(tb, upd);	/* not found, add new */
	}

	if (lc)
		mnt_unlock_file(lc);

	mnt_unref_table(tb);
	return rc;
}

/**
 * mnt_update_table:
 * @upd: update
 * @lc: lock or NULL
 *
 * High-level API to update /etc/mtab (or private /run/mount/utab file).
 *
 * The @lc lock is optional and will be created if necessary. Note that
 * an automatically created lock blocks all signals.
 *
 * See also mnt_lock_block_signals() and mnt_context_get_lock().
 *
 * Returns: 0 on success, negative number on error.
 */
int mnt_update_table(struct libmnt_update *upd, struct libmnt_lock *lc)
{
	struct libmnt_lock *lc0 = lc;
	int rc = -EINVAL;

	if (!upd || !upd->filename)
		return -EINVAL;
	if (!upd->ready)
		return 0;

	DBG(UPDATE, ul_debugobj(upd, "%s: update tab", upd->filename));
	if (upd->fs) {
		DBG(UPDATE, mnt_fs_print_debug(upd->fs, stderr));
	}
	if (!lc) {
		lc = mnt_new_lock(upd->filename, 0);
		if (lc)
			mnt_lock_block_signals(lc, TRUE);
	}

	if (!upd->fs && upd->target)
		rc = update_remove_entry(upd, lc);	/* umount */
	else if (upd->mountflags & MS_MOVE)
		rc = update_modify_target(upd, lc);	/* move */
	else if (upd->mountflags & MS_REMOUNT)
		rc = update_modify_options(upd, lc);	/* remount */
	else if (upd->fs)
		rc = update_add_entry(upd, lc);	/* mount */

	upd->ready = FALSE;
	DBG(UPDATE, ul_debugobj(upd, "%s: update tab: done [rc=%d]",
				upd->filename, rc));
	if (lc != lc0)
		 mnt_free_lock(lc);
	return rc;
}

int mnt_update_already_done(struct libmnt_update *upd, struct libmnt_lock *lc)
{
	struct libmnt_table *tb = NULL;
	struct libmnt_lock *lc0 = lc;
	int rc = 0;

	if (!upd || !upd->filename || (!upd->fs && !upd->target))
		return -EINVAL;

	DBG(UPDATE, ul_debugobj(upd, "%s: checking for previous update", upd->filename));

	if (!lc) {
		lc = mnt_new_lock(upd->filename, 0);
		if (lc)
			mnt_lock_block_signals(lc, TRUE);
	}
	if (lc) {
		rc = mnt_lock_file(lc);
		if (rc) {
			rc = -MNT_ERR_LOCK;
			goto done;
		}
	}

	tb = __mnt_new_table_from_file(upd->filename, MNT_FMT_UTAB, 1);
	if (lc)
		mnt_unlock_file(lc);
	if (!tb)
		goto done;

	if (upd->fs) {
		/* mount */
		const char *tgt = mnt_fs_get_target(upd->fs);
		const char *src = mnt_fs_get_bindsrc(upd->fs) ?
					mnt_fs_get_bindsrc(upd->fs) :
					mnt_fs_get_source(upd->fs);

		if (mnt_table_find_pair(tb, src, tgt, MNT_ITER_BACKWARD)) {
			DBG(UPDATE, ul_debugobj(upd, "%s: found %s %s",
						upd->filename, src, tgt));
			rc = 1;
		}
	} else if (upd->target) {
		/* umount */
		if (!mnt_table_find_target(tb, upd->target, MNT_ITER_BACKWARD)) {
			DBG(UPDATE, ul_debugobj(upd, "%s: not-found (umounted) %s",
						upd->filename, upd->target));
			rc = 1;
		}
	}

	mnt_unref_table(tb);
done:
	if (lc && lc != lc0)
		mnt_free_lock(lc);
	DBG(UPDATE, ul_debugobj(upd, "%s: previous update check done [rc=%d]",
				upd->filename, rc));
	return rc;
}


#ifdef TEST_PROGRAM

static int update(const char *target, struct libmnt_fs *fs, unsigned long mountflags)
{
	int rc;
	struct libmnt_update *upd;

	DBG(UPDATE, ul_debug("update test"));

	upd = mnt_new_update();
	if (!upd)
		return -ENOMEM;

	rc = mnt_update_set_fs(upd, mountflags, target, fs);
	if (rc == 1) {
		/* update is unnecessary */
		rc = 0;
		goto done;
	}
	if (rc) {
		fprintf(stderr, "failed to set FS\n");
		goto done;
	}

	/* [... mount(2) call should be here...]  */

	rc = mnt_update_table(upd, NULL);
done:
	mnt_free_update(upd);
	return rc;
}

static int test_add(struct libmnt_test *ts, int argc, char *argv[])
{
	struct libmnt_fs *fs = mnt_new_fs();
	int rc;

	if (argc < 5 || !fs)
		return -1;
	mnt_fs_set_source(fs, argv[1]);
	mnt_fs_set_target(fs, argv[2]);
	mnt_fs_set_fstype(fs, argv[3]);
	mnt_fs_set_options(fs, argv[4]);

	rc = update(NULL, fs, 0);
	mnt_unref_fs(fs);
	return rc;
}

static int test_remove(struct libmnt_test *ts, int argc, char *argv[])
{
	if (argc < 2)
		return -1;
	return update(argv[1], NULL, 0);
}

static int test_move(struct libmnt_test *ts, int argc, char *argv[])
{
	struct libmnt_fs *fs = mnt_new_fs();
	int rc;

	if (argc < 3)
		return -1;
	mnt_fs_set_source(fs, argv[1]);
	mnt_fs_set_target(fs, argv[2]);

	rc = update(NULL, fs, MS_MOVE);

	mnt_unref_fs(fs);
	return rc;
}

static int test_remount(struct libmnt_test *ts, int argc, char *argv[])
{
	struct libmnt_fs *fs = mnt_new_fs();
	int rc;

	if (argc < 3)
		return -1;
	mnt_fs_set_target(fs, argv[1]);
	mnt_fs_set_options(fs, argv[2]);

	rc = update(NULL, fs, MS_REMOUNT);
	mnt_unref_fs(fs);
	return rc;
}

static int test_replace(struct libmnt_test *ts, int argc, char *argv[])
{
	struct libmnt_fs *fs = mnt_new_fs();
	struct libmnt_table *tb = mnt_new_table();
	int rc;

	if (argc < 3)
		return -1;

	mnt_table_enable_comments(tb, TRUE);
	mnt_table_parse_fstab(tb, NULL);

	mnt_fs_set_source(fs, argv[1]);
	mnt_fs_set_target(fs, argv[2]);
	mnt_fs_append_comment(fs, "# this is new filesystem\n");

	mnt_table_add_fs(tb, fs);
	mnt_unref_fs(fs);

	rc = mnt_table_replace_file(tb, mnt_get_fstab_path());
	mnt_unref_table(tb);
	return rc;
}

int main(int argc, char *argv[])
{
	struct libmnt_test tss[] = {
	{ "--add",    test_add,     "<src> <target> <type> <options>  add a line to mtab" },
	{ "--remove", test_remove,  "<target>                      MS_REMOUNT mtab change" },
	{ "--move",   test_move,    "<old_target>  <target>        MS_MOVE mtab change" },
	{ "--remount",test_remount, "<target>  <options>           MS_REMOUNT mtab change" },
	{ "--replace",test_replace, "<src> <target>                Add a line to LIBMOUNT_FSTAB and replace the original file" },
	{ NULL }
	};

	return mnt_run_test(tss, argc, argv);
}

#endif /* TEST_PROGRAM */
