/*
 * Linux block devices helpers
 *
 * Copyright (C) 2018-2022 Red Hat, Inc. All rights reserved.
 * Copyright (C) 2018-2022 Ondrej Kozina
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include "cryptsetup.h"
#include <dirent.h>
#ifdef HAVE_SYS_SYSMACROS_H
# include <sys/sysmacros.h>     /* for major, minor */
#endif
#include <uuid/uuid.h>

#define UUID_LEN 37 /* 36 + \0, libuuid ... */

static int dm_prepare_uuid(const char *type, const char *uuid, char *buf, size_t buflen)
{
	char *ptr, uuid2[UUID_LEN] = {0};
	uuid_t uu;
	unsigned i = 0;

	/* Remove '-' chars */
	if (uuid) {
		if (uuid_parse(uuid, uu) < 0) {
			log_dbg("Requested UUID %s has invalid format.", uuid);
			return 0;
		}

		for (ptr = uuid2, i = 0; i < UUID_LEN; i++)
			if (uuid[i] != '-') {
				*ptr = uuid[i];
				ptr++;
			}
	}

	if (snprintf(buf, buflen, DM_UUID_PREFIX "%s%s%s%s",
	    type ?: "", type ? "-" : "",
	    uuid2[0] ? uuid2 : "", uuid2[0] ? "-" : "") < 0)
		return 0;

	return 1;
}

/* return number of holders in general, if matched dm_uuid prefix it's returned via dm_name */
/* negative value is error */
static int lookup_holder_dm_name(const char *dm_uuid, dev_t devno, char **r_dm_name)
{
	struct dirent *entry;
	char dm_subpath[PATH_MAX], data_dev_dir[PATH_MAX], uuid[DM_UUID_LEN], dm_name[PATH_MAX] = {};
	ssize_t s;
	struct stat st;
	int dmfd, fd, len, r = 0; /* not found */
	DIR *dir;

	if (!r_dm_name)
		return -EINVAL;

	len = snprintf(data_dev_dir, PATH_MAX, "/sys/dev/block/%u:%u/holders", major(devno), minor(devno));
	if (len < 0 || len >= PATH_MAX)
		return -EINVAL;

	if (!(dir = opendir(data_dev_dir)))
		/* map ENOTDIR to ENOENT we'll handle both errors same */
		return errno == ENOTDIR ? -ENOENT : -errno;

	while (r != 1 && (entry = readdir(dir))) {
		if (entry->d_name[0] == '.' ||
		    !strncmp(entry->d_name, "..", 2))
			continue;

		/* there's a holder */
		r++;

		/* we already have a dm_name, just count remaining holders */
		if (*dm_name != '\0')
			continue;

		len = snprintf(dm_subpath, PATH_MAX, "%s/%s", entry->d_name, "dm");
		if (len < 0 || len >= PATH_MAX) {
			r = -EINVAL;
			break;
		}

		/* looking for dm-X/dm directory, symlinks are fine */
		dmfd = openat(dirfd(dir), dm_subpath, O_DIRECTORY | O_RDONLY);
		if (dmfd < 0)
			continue;

		fd = openat(dmfd, "uuid", O_RDONLY);
		if (fd < 0) {
			close(dmfd);
			continue;
		}

		if (fstat(fd, &st) || !S_ISREG(st.st_mode)) {
			close(fd);
			close(dmfd);
			continue;
		}

		/* reads binary data */
		s = read_buffer(fd, uuid, sizeof(uuid) - 1);
		close(fd);
		uuid[s > 0 ? s : 0] = '\0';
		if (!strncmp(uuid, dm_uuid, strlen(dm_uuid)))
			log_dbg("Found candidate device %s", entry->d_name);
		else {
			close(dmfd);
			continue;
		}

		fd = openat(dmfd, "name", O_RDONLY);
		if (fd < 0) {
			close(dmfd);
			continue;
		}

		if (fstat(fd, &st) || !S_ISREG(st.st_mode)) {
			close(fd);
			close(dmfd);
			continue;
		}

		/* reads binary data */
		s = read_buffer(fd, dm_name, sizeof(dm_name));
		close(fd);
		close(dmfd);
		if (s > 1) {
			dm_name[s-1] = '\0';
			log_dbg("Found dm device %s", dm_name);
			if (!(*r_dm_name = strdup(dm_name)))
				return -ENOMEM;
		}
	}

	closedir(dir);

	return r;
}

int tools_lookup_crypt_device(struct crypt_device *cd, const char *type,
		const char *data_device_path, char **r_name)
{
	char *c;
	struct stat st;
	char dev_uuid[DM_UUID_LEN + DM_BY_ID_PREFIX_LEN] = DM_BY_ID_PREFIX;

	if (!dm_prepare_uuid(type, crypt_get_uuid(cd), dev_uuid + DM_BY_ID_PREFIX_LEN, DM_UUID_LEN))
		return -EINVAL;

	c = strrchr(dev_uuid, '-');
	if (!c)
		return -EINVAL;

	/* cut of dm name */
	*c = '\0';

	log_dbg("Looking for any dm device with prefix: %s", dev_uuid);

	if (stat(data_device_path, &st) < 0)
		return -ENODEV;

	if (!S_ISBLK(st.st_mode))
		return -ENOTBLK;

	return lookup_holder_dm_name(dev_uuid + DM_BY_ID_PREFIX_LEN, st.st_rdev, r_name);
}

static void report_partition(const char *value, const char *device, bool batch_mode)
{
	if (batch_mode)
		log_dbg("Device %s already contains a '%s' partition signature.", device, value);
	else
		log_std(_("WARNING: Device %s already contains a '%s' partition signature.\n"), device, value);
}

static void report_superblock(const char *value, const char *device, bool batch_mode)
{
	if (batch_mode)
		log_dbg("Device %s already contains a '%s' superblock signature.", device, value);
	else
		log_std(_("WARNING: Device %s already contains a '%s' superblock signature.\n"), device, value);
}

int tools_detect_signatures(const char *device, tools_probe_filter_info filter,
		size_t *count,bool batch_mode)
{
	int r;
	size_t tmp_count;
	struct blkid_handle *h;
	blk_probe_status pr;

	if (!count)
		count = &tmp_count;

	*count = 0;

	if (!blk_supported()) {
		log_dbg("Blkid support disabled.");
		return 0;
	}

	if ((r = blk_init_by_path(&h, device))) {
		log_err(_("Failed to initialize device signature probes."));
		return -EINVAL;
	}

	switch (filter) {
	case PRB_FILTER_LUKS:
		if (blk_superblocks_filter_luks(h)) {
			r = -EINVAL;
			goto out;
		}
		/* fall-through */
	case PRB_FILTER_NONE:
		blk_set_chains_for_full_print(h);
		break;
	case PRB_ONLY_LUKS:
		blk_set_chains_for_fast_detection(h);
		if (blk_superblocks_only_luks(h)) {
			r = -EINVAL;
			goto out;
		}
	}

	while ((pr = blk_probe(h)) < PRB_EMPTY) {
		if (blk_is_partition(h))
			report_partition(blk_get_partition_type(h), device, batch_mode);
		else if (blk_is_superblock(h))
			report_superblock(blk_get_superblock_type(h), device, batch_mode);
		else {
			log_dbg("Internal tools_detect_signatures() error.");
			r = -EINVAL;
			goto out;
		}
		(*count)++;
	}

	if (pr == PRB_FAIL)
		r = -EINVAL;
out:
	blk_free(h);
	return r;
}

int tools_wipe_all_signatures(const char *path, bool exclusive, bool only_luks)
{
	int fd, flags, r;
	blk_probe_status pr;
	struct stat st;
	struct blkid_handle *h = NULL;

	if (!blk_supported()) {
		log_dbg("Blkid support disabled.");
		return 0;
	}

	if (stat(path, &st)) {
		log_err(_("Failed to stat device %s."), path);
		return -EINVAL;
	}

	flags = O_RDWR;
	if (S_ISBLK(st.st_mode) && exclusive)
		flags |= O_EXCL;

	/* better than opening regular file with O_EXCL (undefined) */
	/* coverity[toctou] */
	fd = open(path, flags); /* lgtm[cpp/toctou-race-condition] */
	if (fd < 0) {
		if (errno == EBUSY)
			log_err(_("Cannot exclusively open %s, device in use."), path);
		else
			log_err(_("Failed to open file %s in read/write mode."), path);
		return -EINVAL;
	}

	if ((r = blk_init_by_fd(&h, fd))) {
		log_err(_("Failed to initialize device signature probes."));
		r = -EINVAL;
		goto out;
	}

	blk_set_chains_for_wipes(h);
	if (only_luks && (r = blk_superblocks_only_luks(h))) {
		r = -EINVAL;
		goto out;
	}

	while ((pr = blk_probe(h)) < PRB_EMPTY) {
		if (blk_is_partition(h))
			log_verbose(_("Existing '%s' partition signature on device %s will be wiped."),
				    blk_get_partition_type(h), path);
		if (blk_is_superblock(h))
			log_verbose(_("Existing '%s' superblock signature on device %s will be wiped."),
				    blk_get_superblock_type(h), path);
		if (blk_do_wipe(h) || fsync(fd)) {
			log_err(_("Failed to wipe device signature."));
			r = -EINVAL;
			goto out;
		}
	}

	if (pr != PRB_EMPTY) {
		log_err(_("Failed to probe device %s for a signature."), path);
		r = -EINVAL;
	}
out:
	close(fd);
	blk_free(h);
	return r;
}

int tools_superblock_block_size(const char *device, char *sb_name, size_t sb_name_len, unsigned *r_block_size)
{
	struct blkid_handle *h;
	const char *name;
	int r = 0;

	if (!r_block_size || !sb_name || sb_name_len < 1)
		return -EINVAL;

	if (!blk_supported()) {
		log_dbg("Blkid support disabled.");
		return 0;
	}

	if ((r = blk_init_by_path(&h, device))) {
		log_err(_("Failed to initialize device signature probes."));
		return -EINVAL;
	}

	blk_set_chains_for_superblocks(h);

	switch (blk_probe(h)) {
	case PRB_OK:
		*r_block_size = blk_get_block_size(h);
		if (!*r_block_size) /* same as not-found */
			break;

		if (!(name = blk_get_superblock_type(h))) {
			r = -EINVAL;
			break;
		}

		/* we don't mind truncating */
		strncpy(sb_name, name, sb_name_len - 1);
		sb_name[sb_name_len-1] = '\0';

		log_dbg("Detected superblock %s on device %s (block size: %u).", sb_name, device, *r_block_size);
		r = 1;
		/* fall-through */
	case PRB_EMPTY:
		break;
	default:
		r = -EINVAL;
	}

	blk_free(h);

	return r;
}

bool tools_blkid_supported(void)
{
	return blk_supported() != 0;
}
