/*
 * This file is part of Clifm
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 * SPDX-FileCopyrightText: 2016-2026 L. Abramovich <leo.clifm@outlook.com>
*/

/* media.c -- functions to manage local filesystems */

#ifndef NO_MEDIA_FUNC

#include "helpers.h"

#include <unistd.h>
#include <string.h>
#include <errno.h>

#if defined(__linux__) || defined(__CYGWIN__)
# include <mntent.h>
# define HAVE_PROC_MOUNTS
# define DISK_LABELS_PATH "/dev/disk/by-label"
# ifndef _PATH_MOUNTED
#  define _PATH_MOUNTED "/etc/mtab"
# endif
#elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) \
|| defined(__DragonFly__)
# include <sys/mount.h>
# include <sys/sysctl.h>
#elif defined(__APPLE__)
# include <sys/param.h>
# include <sys/ucred.h>
# include <sys/mount.h>
#elif defined(__sun)
# include <sys/mnttab.h> /* getmntent() */
# ifndef MNTTAB
#  define MNTTAB "/etc/mnttab"
# endif /* MNTTAB */
#endif /* __linux__ || __CYGWIN__ */

#include "aux.h"
#include "colors.h" /* get_dir_color() */
#include "history.h"
#include "jump.h"
#include "listing.h"
#include "misc.h"
#include "navigation.h"
#include "readline.h"
#include "spawn.h"

/* Information about devices */
struct mnt_t {
	char *mnt;   /* Mountpoint */
	char *dev;   /* Device name (e.g.: /dev/sda1) */
	char *label; /* Device label */
};

static struct mnt_t *media = NULL;
static size_t mp_n = 0;

#ifdef HAVE_PROC_MOUNTS
static char **
get_block_devices(void)
{
	struct dirent **blockdev = NULL;
	int block_n = scandir("/dev", &blockdev, NULL, alphasort);
	if (block_n == - 1)
		return NULL;

	char **bd = NULL;
	size_t n = 0;

	for (int i = 0; i < block_n; i++) {
# ifndef _DIRENT_HAVE_D_TYPE
		char bpath[PATH_MAX + 1];
		snprintf(bpath, sizeof(bpath), "/dev/%s", blockdev[i]->d_name);
		struct stat a;
		if (stat(bpath, &a) == -1) {
			free(blockdev[i]);
			continue;
		}
		if (!S_ISBLK(a.st_mode)) {
# else
		if (blockdev[i]->d_type != DT_BLK) {
# endif /* !_DIRENT_HAVE_D_TYPE */
			free(blockdev[i]);
			continue;
		}

		const char *name = blockdev[i]->d_name;

		/* Skip /dev/ram and /dev/loop devices */
		if ((*name == 'l' && strncmp(name, "loop", 4) == 0)
		|| (*name == 'r' && strncmp(name, "ram", 3) == 0)) {
			free(blockdev[i]);
			continue;
		}

		/* Get only partition names, normally ending with a number */
		const size_t blen = strlen(name);
		if (blen > 0 && (name[blen - 1] < '1' || name[blen - 1] > '9')) {
			free(blockdev[i]);
			continue;
		}

		bd = xnrealloc(bd, n + 2, sizeof(char *));
		bd[n] = xnmalloc(blen + 6, sizeof(char *));
		snprintf(bd[n], blen + 6, "/dev/%s", name);
		n++;
		bd[n] = NULL;
		free(blockdev[i]);
	}

	free(blockdev);
	return bd;
}

static int
cd_out_of_mountpoint(char *mountpoint)
{
	if (!mountpoint || !*mountpoint)
		return FUNC_FAILURE;

	char *slash = strrchr(mountpoint, '/');
	if (!slash)
		return FUNC_FAILURE;

	*slash = '\0';
	const int ret = cd_function(mountpoint, CD_PRINT_ERROR);
	*slash = '/';

	return ret;
}

static int
unmount_dev(const size_t n)
{
	if (xargs.mount_cmd == UNSET) {
		xerror(_("%s: No mount application found. Install either "
			"udevil or udisks2.\n"), PROGRAM_NAME);
		return FUNC_FAILURE;
	}

	char *mnt = media[n].mnt;
	int exit_status = FUNC_SUCCESS;

	/* Get out of mountpoint before unmounting */
	const size_t mlen = strlen(mnt);
	if (strncmp(mnt, workspaces[cur_ws].path, mlen) == 0)
		(void)cd_out_of_mountpoint(mnt);

	const char *cmd[] = {xargs.mount_cmd == MNT_UDISKS2 ? "udisksctl"
			: "udevil", "unmount", "-b", media[n].dev, NULL};
	if (launch_execv(cmd, FOREGROUND, E_NOFLAG) != FUNC_SUCCESS)
		exit_status = FUNC_FAILURE;

	if (exit_status != FUNC_FAILURE && xargs.mount_cmd == MNT_UDEVIL)
		printf(_("%s: Unmounted %s\n"), PROGRAM_NAME, media[n].dev);

	return exit_status;
}

static char *
get_dev_label(void)
{
	const size_t n = mp_n;
	struct dirent **labels = NULL;
	const int ln = scandir(DISK_LABELS_PATH, &labels, NULL, alphasort);
	if (ln == - 1)
		return NULL;

	char *label = NULL;

	for (int i = 0; i < ln; i++) {
		if (label) {
			free(labels[i]);
			continue;
		}

		char *name = labels[i]->d_name;
		char lpath[PATH_MAX + 1];
		snprintf(lpath, sizeof(lpath), "%s/%s", DISK_LABELS_PATH, name);
		char *rpath = xrealpath(lpath, NULL);
		if (!rpath) {
			free(labels[i]);
			continue;
		}

		const int ret = strcmp(rpath, media[n].dev);
		free(rpath);
		if (ret == 0) {
			/* Device label is encoded using hex. Let's decode it */
			char *p = strchr(name, '\\');
			if (p && *(p + 1) == 'x') {
				char pp = 0;
				pp = (char)(from_hex(*(p + 2)) << 4 | from_hex(*(p + 3)));
				*p = pp;
				xstrsncpy(p + 1, p + 4, strlen(p + 4) + 1);
			}
			label = savestring(name, strlen(name));
		}

		free(labels[i]);
	}

	free(labels);
	return label;
}

static void
list_unmounted_devs(void)
{
	const size_t mp_n_bk = mp_n;
	char **unm_devs = get_block_devices();
	if (!unm_devs)
		return;

	printf(_("\n%sUnmounted devices%s\n\n"), BOLD, df_c);

	for (int i = 0; unm_devs[i]; i++) {
		int skip = 0;
		/* Skip already mounted devices */
		for (size_t j = 0; j < mp_n_bk; j++) {
			if (strcmp(media[j].dev, unm_devs[i]) == 0)
				skip = 1;
		}

		if (skip == 1) {
			free(unm_devs[i]);
			continue;
		}

		media = xnrealloc(media, mp_n + 2, sizeof(struct mnt_t));
		media[mp_n].dev = savestring(unm_devs[i], strlen(unm_devs[i]));
		media[mp_n].mnt = NULL;

		media[mp_n].label = get_dev_label();

		if (media[mp_n].label)
			printf("%s%zu %s%s [%s%s%s]\n", el_c, mp_n + 1, df_c,
				media[mp_n].dev, mi_c, media[mp_n].label, df_c);
		else
			printf("%s%zu %s%s\n", el_c, mp_n + 1, df_c, media[mp_n].dev);

		mp_n++;
		free(unm_devs[i]);
	}
	free(unm_devs);

	media[mp_n].dev = NULL;
	media[mp_n].mnt = NULL;
	media[mp_n].label = NULL;
}

static int
list_mounted_devs(const int mode)
{
	struct mntent *ent;

	FILE *fp = setmntent(_PATH_MOUNTED, "r");
	if (!fp) {
		xerror("mp: setmntent: %s: %s\n", _PATH_MOUNTED, strerror(errno));
		return FUNC_FAILURE;
	}

	while ((ent = getmntent(fp)) != NULL) {
		if (strncmp(ent->mnt_fsname, "/dev/", 5) != 0)
			continue;

		struct stat a;
		if (stat(ent->mnt_dir, &a) == -1 || !S_ISDIR(a.st_mode))
			continue;

		const char *dir_color = get_dir_color(ent->mnt_dir, &a, -1);

		if (mode == MEDIA_LIST) {
			printf("%s%zu%s %s%s%s [%s]\n", el_c, mp_n + 1,
				df_c, dir_color, ent->mnt_dir, df_c, ent->mnt_fsname);
		} else {
			printf("%s%zu%s %s [%s%s%s]\n", el_c, mp_n + 1,
				df_c, ent->mnt_fsname, dir_color, ent->mnt_dir, df_c);
		}

		media = xnrealloc(media, mp_n + 2, sizeof(struct mnt_t));
		media[mp_n].mnt = savestring(ent->mnt_dir, strlen(ent->mnt_dir));
		media[mp_n].dev = savestring(ent->mnt_fsname, strlen(ent->mnt_fsname));
		media[mp_n++].label = NULL;
	}

	endmntent(fp);

	media[mp_n].dev = NULL;
	media[mp_n].mnt = NULL;
	media[mp_n].label = NULL;

	return FUNC_SUCCESS;
}

/* Mount device and store mountpoint (at media[N]). */
static int
mount_dev(const size_t n)
{
	if (xargs.mount_cmd == UNSET) {
		xerror(_("%s: No mount application found. Install either "
			"udevil or udisks2.\n"), PROGRAM_NAME);
		return FUNC_FAILURE;
	}

	char file[PATH_MAX + 1];
	snprintf(file, sizeof(file), "%s/%s", xargs.stealth_mode == 1
		? P_tmpdir : tmp_dir, TMP_FILENAME);

	int fd = mkstemp(file);
	if (fd == -1)
		return FUNC_FAILURE;

	int stdout_bk = dup(STDOUT_FILENO); /* Save original stdout */
	if (stdout_bk == -1) {
		unlinkat(XAT_FDCWD, file, 0);
		close(fd);
		return FUNC_FAILURE;
	}

	dup2(fd, STDOUT_FILENO); /* Redirect stdout to the desired file */
	close(fd);

	if (xargs.mount_cmd == MNT_UDISKS2) {
		const char *cmd[] = {"udisksctl", "mount", "-b", media[n].dev, NULL};
		launch_execv(cmd, FOREGROUND, E_NOFLAG);
	} else {
		const char *cmd[] = {"udevil", "mount", media[n].dev, NULL};
		launch_execv(cmd, FOREGROUND, E_NOFLAG);
	}

	dup2(stdout_bk, STDOUT_FILENO); /* Restore original stdout */
	close(stdout_bk);

	FILE *fp = open_fread(file, &fd);
	if (!fp) {
		unlinkat(XAT_FDCWD, file, 0);
		return FUNC_FAILURE;
	}

	char out_line[PATH_MAX + 1]; *out_line = '\0';
	if (fgets(out_line, (int)sizeof(out_line), fp) == NULL) {
		/* Error is printed by the mount command itself */
		unlinkat(XAT_FDCWD, file, 0);
		fclose(fp);
		return FUNC_FAILURE;
	}

	unlinkat(XAT_FDCWD, file, 0);
	fclose(fp);

	/* Recover the mountpoint used by the mounting command. */
	char *p = strstr(out_line, " at ");
	if (!p || !p[1] || !p[2] || !p[3] || p[4] != '/') {
		xerror(_("%s: Error retrieving mountpoint\n"), PROGRAM_NAME);
		return FUNC_FAILURE;
	}
	p += 4;

	/* Using strnlen() here avoids a Redhat hardened compilation warning. */
	size_t plen = strnlen(p, sizeof(out_line) - 4);
	if (plen > 0 && p[plen - 1] == '\n') {
		plen--;
		p[plen] = '\0';
	}

	media[n].mnt = savestring(p, plen);

	return FUNC_SUCCESS;
}
#endif /* HAVE_PROC_MOUNTS */

static void
free_media(void)
{
	for (size_t i = mp_n; i-- > 0;) {
		free(media[i].mnt);
		free(media[i].dev);
		free(media[i].label);
	}
	free(media);
	media = NULL;
}

/* Get device information via external application */
static int
print_dev_info(const size_t n)
{
	if (!media[n].dev || xargs.mount_cmd == UNSET)
		return FUNC_FAILURE;

	int exit_status = FUNC_SUCCESS;
	if (xargs.mount_cmd == MNT_UDEVIL) {
		const char *cmd[] = {"udevil", "info", media[n].dev, NULL};
		if (launch_execv(cmd, FOREGROUND, E_NOFLAG) != FUNC_SUCCESS)
			exit_status = FUNC_FAILURE;
	} else {
		const char *cmd[] = {"udisksctl", "info", "-b", media[n].dev, NULL};
		if (launch_execv(cmd, FOREGROUND, E_NOFLAG) != FUNC_SUCCESS)
			exit_status = FUNC_FAILURE;
	}

	return exit_status;
}

#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) \
|| defined(__DragonFly__) || defined(__APPLE__)
static size_t
# ifdef __NetBSD__
list_mountpoints_bsd(struct statvfs *fslist)
# else
list_mountpoints_bsd(struct statfs *fslist)
# endif /* __NetBSD__ */
{
	size_t j = 0;

	for (size_t i = 0; i < mp_n; i++) {
		/* Do not list all mountpoints, but only those corresponding
		 * to a block device (/dev) */
		if (strncmp(fslist[i].f_mntfromname, "/dev/", 5) != 0)
			continue;

		struct stat a;
		if (stat(fslist[i].f_mntonname, &a) == -1 || !S_ISDIR(a.st_mode))
			continue;

		const char *dir_color = get_dir_color(fslist[i].f_mntonname, &a, -1);
		printf("%s%zu%s %s%s%s (%s)\n", el_c, j + 1, df_c, dir_color,
			fslist[i].f_mntonname, df_c, fslist[i].f_mntfromname);

		/* Store the mountpoint in the mounpoints struct */
		media = xnrealloc(media, j + 2, sizeof(struct mnt_t));
		media[j].mnt = savestring(fslist[i].f_mntonname,
			strlen(fslist[i].f_mntonname));
		media[j].label = NULL;
		media[j++].dev = NULL;
	}

	media[j].dev = NULL;
	media[j].mnt = NULL;
	media[j].label = NULL;

	return j;
}
#endif /* BSD || APPLE */

static size_t
get_mnt_input(const int mode, int *info)
{
	size_t n = (size_t)-1;

	puts(_("Enter 'q' to quit"));
	if (xargs.mount_cmd != UNSET)
		puts(_("Enter 'iELN' for device information. E.g.: i4"));

	char *input = NULL;
	while (!input) {
#ifdef HAVE_PROC_MOUNTS
		if (mode == MEDIA_LIST)
			input = rl_no_hist(_("Select a mountpoint (by ELN): "), 0);
		else
			input = rl_no_hist(_("Select a mountpoint/device (by ELN): "), 0);
#else
		UNUSED(mode);
		input = rl_no_hist(_("Select a mountpoint (by ELN): "), 0);
#endif /* HAVE_PROC_MOUNTS */
		if (!input || !*input) {
			free(input);
			input = NULL;
			continue;
		}

		if (*input == 'q' && !input[1]) {
			if (conf.autols == 1)
				reload_dirlist();
			free(input);
			return (size_t)-1;
		}

		const char *p = input;
		if (*p == 'i') {
			*info = 1;
			++p;
		}

		const int atoi_num = xatoi(p);
		if (atoi_num <= 0 || (size_t)atoi_num > mp_n) {
			xerror(_("%s: %s: Invalid ELN\n"), PROGRAM_NAME, input);
			free(input);
			input = NULL;
			continue;
		}

		n = (size_t)atoi_num - 1;
	}

	free(input);
	return n;
}

static int
print_mnt_info(const size_t n)
{
	const int exit_status = print_dev_info(n);

	if (exit_status == FUNC_SUCCESS)
		press_any_key_to_continue(1);

	free_media();
	return exit_status;
}

#ifdef __sun
/* Get and list available mountpoints (dirs only).
 * Returns the number of mounpoints found. */
static size_t
xgetmntinfo_sun(void)
{
	FILE *fp = fopen(MNTTAB, "r");
	if (!fp)
		return 0;

	size_t n = 0;
	struct mnttab ent;

	while (getmntent(fp, &ent) != -1) {
		char *mp = ent.mnt_mountp;
		if (!mp || !*mp)
			continue;

		struct stat a;
		if (stat(mp, &a) == -1 || !S_ISDIR(a.st_mode))
			continue;

		const char *dir_color = get_dir_color(mp, &a, -1);
		printf("%s%zu%s %s%s%s [%s]\n", el_c, n + 1, df_c,
			dir_color, mp, df_c, ent.mnt_special);

		media = xnrealloc(media, n + 2, sizeof(struct mnt_t));
		media[n].mnt = savestring(mp, strlen(mp));
		media[n].label = NULL;
		media[n++].dev = NULL;
	}

	fclose(fp);

	media[n].mnt = NULL;
	media[n].label = NULL;
	media[n].dev = NULL;

	return n;
}
#endif /* __sun */

/* If MODE is MEDIA_MOUNT (used by the 'media' command) list mounted and
 * unmounted devices allowing the user to mount or unmount any of them.
 * If MODE is rather MEDIA_LIST (used by the 'mp' command), just list
 * available mountpoints and allow the user to cd into the selected one. */
int
media_menu(const int mode)
{
#if defined(__HAIKU__)
	xerror(_("%s: Function not available on this platform\n"),
		mode == MEDIA_LIST ? _("mountpoints") : _("media"));
	return FUNC_FAILURE;
#endif /* __HAIKU__ */

#ifndef HAVE_PROC_MOUNTS
	if (mode == MEDIA_MOUNT) {
		xerror(_("media: Function not available on this platform\n"));
		return FUNC_FAILURE;
	}
#endif /* HAVE_PROC_MOUNTS */

	if (mode == MEDIA_MOUNT && xargs.mount_cmd == UNSET) {
		xerror(_("media: No mount application found. Install either "
			"udevil or udisks2.\n"));
		return FUNC_FAILURE;
	}

	if (conf.clear_screen > 0)
		CLEAR;

#ifdef HAVE_PROC_MOUNTS
	printf("%s%s%s\n\n", BOLD, mode == MEDIA_LIST ? _("Mountpoints")
			: _("Mounted devices"), df_c);
#else
	printf(_("%sMountpoints%s\n\n"), BOLD, df_c);
#endif /* HAVE_PROC_MOUNTS */

	media = xnmalloc(1, sizeof(struct mnt_t));
	mp_n = 0;

#ifdef HAVE_PROC_MOUNTS
	if (list_mounted_devs(mode) == FUNC_FAILURE) {
		free(media);
		media = NULL;
		return FUNC_FAILURE;
	}

	if (mode == MEDIA_MOUNT)
		list_unmounted_devs();

#elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__APPLE__) \
|| defined(__DragonFly__)
	struct statfs *fslist;
	mp_n = (size_t)getmntinfo(&fslist, MNT_NOWAIT);
#elif defined(__NetBSD__)
	struct statvfs *fslist;
	mp_n = (size_t)getmntinfo(&fslist, MNT_NOWAIT);
#elif defined(__sun)
	mp_n = xgetmntinfo_sun();
#endif /* HAVE_PROC_MOUNTS */

	/* This should never happen: There should always be a mountpoint,
	 * at least "/" */
	/* cppcheck-suppress knownConditionTrueFalse */
	if (mp_n == 0) {
#ifdef HAVE_PROC_MOUNTS
		printf(_("%s: There are no available %s\n"), mode == MEDIA_LIST ? "mp"
			: "media", mode == MEDIA_LIST ? _("mountpoints") : _("devices"));
#else
		fputs(_("mp: There are no available mountpoints\n"), stdout);
#endif /* HAVE_PROC_MOUNTS */
		return FUNC_SUCCESS;
	}

#if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) \
|| defined(__APPLE__) || defined(__DragonFly__)
	mp_n = list_mountpoints_bsd(fslist);
#endif /* BSD || APPLE */

	putchar('\n');
	int info = 0;
	const size_t n = get_mnt_input(mode, &info);

	int exit_status = FUNC_SUCCESS;
	if (n == (size_t)-1)
		goto EXIT;

	if (info == 1) {
		exit_status = print_mnt_info(n);
		media_menu(mode);
		return exit_status;
	}

#ifdef HAVE_PROC_MOUNTS
	if (mode == MEDIA_MOUNT) {
		if (!media[n].mnt) {
			/* The device is unmounted: mount it */
			if (mount_dev(n) == FUNC_FAILURE) {
				exit_status = FUNC_FAILURE;
				goto EXIT;
			}
		} else {
			/* The device is mounted: unmount it */
			int ret = unmount_dev(n);
			if (ret == FUNC_FAILURE)
				exit_status = FUNC_FAILURE;
			goto EXIT;
		}
	}
#endif /* HAVE_PROC_MOUNTS */

	if (xchdir(media[n].mnt, SET_TITLE) != FUNC_SUCCESS) {
		xerror("%s: '%s': %s\n", PROGRAM_NAME, media[n].mnt, strerror(errno));
		exit_status = FUNC_FAILURE;
		goto EXIT;
	}

	free(workspaces[cur_ws].path);
	workspaces[cur_ws].path = savestring(media[n].mnt, strlen(media[n].mnt));

	if (conf.autols == 1)
		reload_dirlist();

	add_to_dirhist(workspaces[cur_ws].path);
	add_to_jumpdb(workspaces[cur_ws].path);

EXIT:
	free_media();
	return exit_status;
}

#else
void *_skip_me_media;
#endif /* !NO_MEDIA_FUNC */
