/* 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: tabdiff
 * @title: Compare changes in mount tables
 * @short_description: compare changes in the list of the mounted filesystems
 */
#include "mountP.h"

struct tabdiff_entry {
	int	oper;			/* MNT_TABDIFF_* flags; */

	struct libmnt_fs *old_fs;	/* pointer to the old FS */
	struct libmnt_fs *new_fs;	/* pointer to the new FS */

	struct list_head changes;
};

struct libmnt_tabdiff {
	int nchanges;			/* number of changes */

	struct list_head changes;	/* list with modified entries */
	struct list_head unused;	/* list with unused entries */
};

/**
 * mnt_new_tabdiff:
 *
 * Allocates a new table diff struct.
 *
 * Returns: new diff handler or NULL.
 */
struct libmnt_tabdiff *mnt_new_tabdiff(void)
{
	struct libmnt_tabdiff *df = calloc(1, sizeof(*df));

	if (!df)
		return NULL;

	DBG(DIFF, ul_debugobj(df, "alloc"));

	INIT_LIST_HEAD(&df->changes);
	INIT_LIST_HEAD(&df->unused);
	return df;
}

static void free_tabdiff_entry(struct tabdiff_entry *de)
{
	if (!de)
		return;
	list_del(&de->changes);
	mnt_unref_fs(de->new_fs);
	mnt_unref_fs(de->old_fs);
	free(de);
}

/**
 * mnt_free_tabdiff:
 * @df: tab diff
 *
 * Deallocates tab diff struct and all entries.
 */
void mnt_free_tabdiff(struct libmnt_tabdiff *df)
{
	if (!df)
		return;

	DBG(DIFF, ul_debugobj(df, "free"));

	while (!list_empty(&df->changes)) {
		struct tabdiff_entry *de = list_entry(df->changes.next,
			                  struct tabdiff_entry, changes);
		free_tabdiff_entry(de);
	}

	free(df);
}

/**
 * mnt_tabdiff_next_change:
 * @df: tabdiff pointer
 * @itr: iterator
 * @old_fs: returns the old entry or NULL if new entry added
 * @new_fs: returns the new entry or NULL if old entry removed
 * @oper: MNT_TABDIFF_{MOVE,UMOUNT,REMOUNT,MOUNT} flags
 *
 * The options @old_fs, @new_fs and @oper are optional.
 *
 * Returns: 0 on success, negative number in case of error or 1 at the end of list.
 */
int mnt_tabdiff_next_change(struct libmnt_tabdiff *df, struct libmnt_iter *itr,
		struct libmnt_fs **old_fs, struct libmnt_fs **new_fs, int *oper)
{
	int rc = 1;
	struct tabdiff_entry *de = NULL;

	if (!df || !itr)
		return -EINVAL;

	if (!itr->head)
		MNT_ITER_INIT(itr, &df->changes);
	if (itr->p != itr->head) {
		de = MNT_ITER_GET_ENTRY(itr, struct tabdiff_entry, changes);
		MNT_ITER_ITERATE(itr);
		rc = 0;
	}

	if (old_fs)
		*old_fs = de ? de->old_fs : NULL;
	if (new_fs)
		*new_fs = de ? de->new_fs : NULL;
	if (oper)
		*oper = de ? de->oper : 0;

	return rc;
}

static int tabdiff_reset(struct libmnt_tabdiff *df)
{
	assert(df);

	DBG(DIFF, ul_debugobj(df, "resetting"));

	/* zeroize all entries and move them to the list of unused
	 */
	while (!list_empty(&df->changes)) {
		struct tabdiff_entry *de = list_entry(df->changes.next,
			                  struct tabdiff_entry, changes);

		list_del_init(&de->changes);
		list_add_tail(&de->changes, &df->unused);

		mnt_unref_fs(de->new_fs);
		mnt_unref_fs(de->old_fs);

		de->new_fs = de->old_fs = NULL;
		de->oper = 0;
	}

	df->nchanges = 0;
	return 0;
}

static int tabdiff_add_entry(struct libmnt_tabdiff *df, struct libmnt_fs *old,
			     struct libmnt_fs *new, int oper)
{
	struct tabdiff_entry *de;

	assert(df);

	DBG(DIFF, ul_debugobj(df, "add change on %s",
				mnt_fs_get_target(new ? new : old)));

	if (!list_empty(&df->unused)) {
		de = list_entry(df->unused.next, struct tabdiff_entry, changes);
		list_del(&de->changes);
	} else {
		de = calloc(1, sizeof(*de));
		if (!de)
			return -ENOMEM;
	}

	INIT_LIST_HEAD(&de->changes);

	mnt_ref_fs(new);
	mnt_ref_fs(old);

	mnt_unref_fs(de->new_fs);
	mnt_unref_fs(de->old_fs);

	de->old_fs = old;
	de->new_fs = new;
	de->oper = oper;

	list_add_tail(&de->changes, &df->changes);
	df->nchanges++;
	return 0;
}

static struct tabdiff_entry *tabdiff_get_mount(struct libmnt_tabdiff *df,
					       const char *src,
					       int id)
{
	struct list_head *p;

	assert(df);

	list_for_each(p, &df->changes) {
		struct tabdiff_entry *de;

		de = list_entry(p, struct tabdiff_entry, changes);

		if (de->oper == MNT_TABDIFF_MOUNT && de->new_fs &&
		    mnt_fs_get_id(de->new_fs) == id) {

			const char *s = mnt_fs_get_source(de->new_fs);

			if (s == NULL && src == NULL)
				return de;
			if (s && src && strcmp(s, src) == 0)
				return de;
		}
	}
	return NULL;
}

/**
 * mnt_diff_tables:
 * @df: diff handler
 * @old_tab: old table
 * @new_tab: new table
 *
 * Compares @old_tab and @new_tab, the result is stored in @df and accessible by
 * mnt_tabdiff_next_change().
 *
 * Returns: number of changes, negative number in case of error.
 */
int mnt_diff_tables(struct libmnt_tabdiff *df, struct libmnt_table *old_tab,
		    struct libmnt_table *new_tab)
{
	struct libmnt_fs *fs;
	struct libmnt_iter itr;
	int no, nn;

	if (!df || !old_tab || !new_tab)
		return -EINVAL;

	tabdiff_reset(df);

	no = mnt_table_get_nents(old_tab);
	nn = mnt_table_get_nents(new_tab);

	if (!no && !nn)			/* both tables are empty */
		return 0;

	DBG(DIFF, ul_debugobj(df, "analyze new (%d entries), "
				          "old (%d entries)",
				nn, no));

	mnt_reset_iter(&itr, MNT_ITER_FORWARD);

	/* all mounted or umounted */
	if (!no && nn) {
		while(mnt_table_next_fs(new_tab, &itr, &fs) == 0)
			tabdiff_add_entry(df, NULL, fs, MNT_TABDIFF_MOUNT);
		goto done;

	} else if (no && !nn) {
		while(mnt_table_next_fs(old_tab, &itr, &fs) == 0)
			tabdiff_add_entry(df, fs, NULL, MNT_TABDIFF_UMOUNT);
		goto done;
	}

	/* search newly mounted or modified */
	while(mnt_table_next_fs(new_tab, &itr, &fs) == 0) {
		struct libmnt_fs *o_fs;
		const char *src = mnt_fs_get_source(fs),
			   *tgt = mnt_fs_get_target(fs);

		o_fs = mnt_table_find_pair(old_tab, src, tgt, MNT_ITER_FORWARD);
		if (!o_fs)
			/* 'fs' is not in the old table -- so newly mounted */
			tabdiff_add_entry(df, NULL, fs, MNT_TABDIFF_MOUNT);
		else {
			/* is modified? */
			const char *v1 = mnt_fs_get_vfs_options(o_fs),
				   *v2 = mnt_fs_get_vfs_options(fs),
				   *f1 = mnt_fs_get_fs_options(o_fs),
				   *f2 = mnt_fs_get_fs_options(fs);

			if ((v1 && v2 && strcmp(v1, v2) != 0) || (f1 && f2 && strcmp(f1, f2) != 0))
				tabdiff_add_entry(df, o_fs, fs, MNT_TABDIFF_REMOUNT);
		}
	}

	/* search umounted or moved */
	mnt_reset_iter(&itr, MNT_ITER_FORWARD);
	while(mnt_table_next_fs(old_tab, &itr, &fs) == 0) {
		const char *src = mnt_fs_get_source(fs),
			   *tgt = mnt_fs_get_target(fs);

		if (!mnt_table_find_pair(new_tab, src, tgt, MNT_ITER_FORWARD)) {
			struct tabdiff_entry *de;

			de = tabdiff_get_mount(df, src,	mnt_fs_get_id(fs));
			if (de) {
				mnt_ref_fs(fs);
				mnt_unref_fs(de->old_fs);
				de->oper = MNT_TABDIFF_MOVE;
				de->old_fs = fs;
			} else
				tabdiff_add_entry(df, fs, NULL, MNT_TABDIFF_UMOUNT);
		}
	}
done:
	DBG(DIFF, ul_debugobj(df, "%d changes detected", df->nchanges));
	return df->nchanges;
}

#ifdef TEST_PROGRAM

static int test_diff(struct libmnt_test *ts __attribute__((unused)),
		     int argc, char *argv[])
{
	struct libmnt_table *tb_old, *tb_new;
	struct libmnt_tabdiff *diff;
	struct libmnt_iter *itr;
	struct libmnt_fs *old, *new;
	int rc = -1, change;

	if (argc != 3)
		return -1;

	tb_old = mnt_new_table_from_file(argv[1]);
	tb_new = mnt_new_table_from_file(argv[2]);
	diff = mnt_new_tabdiff();
	itr = mnt_new_iter(MNT_ITER_FORWARD);

	if (!tb_old || !tb_new || !diff || !itr) {
		warnx("failed to allocate resources");
		goto done;
	}

	rc = mnt_diff_tables(diff, tb_old, tb_new);
	if (rc < 0)
		goto done;

	while(mnt_tabdiff_next_change(diff, itr, &old, &new, &change) == 0) {

		printf("%s on %s: ", mnt_fs_get_source(new ? new : old),
				     mnt_fs_get_target(new ? new : old));

		switch(change) {
		case MNT_TABDIFF_MOVE:
			printf("MOVED to %s\n", mnt_fs_get_target(new));
			break;
		case MNT_TABDIFF_UMOUNT:
			printf("UMOUNTED\n");
			break;
		case MNT_TABDIFF_REMOUNT:
			printf("REMOUNTED from '%s' to '%s'\n",
					mnt_fs_get_options(old),
					mnt_fs_get_options(new));
			break;
		case MNT_TABDIFF_MOUNT:
			printf("MOUNTED\n");
			break;
		default:
			printf("unknown change!\n");
		}
	}

	rc = 0;
done:
	mnt_unref_table(tb_old);
	mnt_unref_table(tb_new);
	mnt_free_tabdiff(diff);
	mnt_free_iter(itr);
	return rc;
}

int main(int argc, char *argv[])
{
	struct libmnt_test tss[] = {
		{ "--diff", test_diff, "<old> <new> prints change" },
		{ NULL }
	};

	return mnt_run_test(tss, argc, argv);
}

#endif /* TEST_PROGRAM */
