// SPDX-License-Identifier: CDDL-1.0
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or https://opensource.org/licenses/CDDL-1.0.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

/*
 * Copyright (c) 2005, 2010, Oracle and/or its affiliates. All rights reserved.
 */

#include <ctype.h>
#include <dirent.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/efi_partition.h>

#ifdef HAVE_LIBUDEV
#include <libudev.h>
#endif

#include <libzutil.h>

/*
 * Append partition suffix to an otherwise fully qualified device path.
 * This is used to generate the name the full path as its stored in
 * ZPOOL_CONFIG_PATH for whole disk devices.  On success the new length
 * of 'path' will be returned on error a negative value is returned.
 */
int
zfs_append_partition(char *path, size_t max_len)
{
	int len = strlen(path);

	if ((strncmp(path, UDISK_ROOT, strlen(UDISK_ROOT)) == 0) ||
	    (strncmp(path, ZVOL_ROOT, strlen(ZVOL_ROOT)) == 0)) {
		if (len + 6 >= max_len)
			return (-1);

		(void) strcat(path, "-part1");
		len += 6;
	} else {
		if (len + 2 >= max_len)
			return (-1);

		if (isdigit(path[len-1])) {
			(void) strcat(path, "p1");
			len += 2;
		} else {
			(void) strcat(path, "1");
			len += 1;
		}
	}

	return (len);
}

/*
 * Remove partition suffix from a vdev path.  Partition suffixes may take three
 * forms: "-partX", "pX", or "X", where X is a string of digits.  The second
 * case only occurs when the suffix is preceded by a digit, i.e. "md0p0" The
 * third case only occurs when preceded by a string matching the regular
 * expression "^([hsv]|xv)d[a-z]+", i.e. a scsi, ide, virtio or xen disk.
 *
 * caller must free the returned string
 */
char *
zfs_strip_partition(const char *path)
{
	char *tmp = strdup(path);
	char *part = NULL, *d = NULL;
	if (!tmp)
		return (NULL);

	if ((part = strstr(tmp, "-part")) && part != tmp) {
		d = part + 5;
	} else if ((part = strrchr(tmp, 'p')) &&
	    part > tmp + 1 && isdigit(*(part-1))) {
		d = part + 1;
	} else if ((tmp[0] == 'h' || tmp[0] == 's' || tmp[0] == 'v') &&
	    tmp[1] == 'd') {
		for (d = &tmp[2]; isalpha(*d); part = ++d) { }
	} else if (strncmp("xvd", tmp, 3) == 0) {
		for (d = &tmp[3]; isalpha(*d); part = ++d) { }
	}
	if (part && d && *d != '\0') {
		for (; isdigit(*d); d++) { }
		if (*d == '\0')
			*part = '\0';
	}

	return (tmp);
}

/*
 * Same as zfs_strip_partition, but allows "/dev/" to be in the pathname
 *
 * path:	/dev/sda1
 * returns:	/dev/sda
 *
 * Returned string must be freed.
 */
static char *
zfs_strip_partition_path(const char *path)
{
	char *newpath = strdup(path);
	char *sd_offset;
	char *new_sd;

	if (!newpath)
		return (NULL);

	/* Point to "sda1" part of "/dev/sda1" */
	sd_offset = strrchr(newpath, '/') + 1;

	/* Get our new name "sda" */
	new_sd = zfs_strip_partition(sd_offset);
	if (!new_sd) {
		free(newpath);
		return (NULL);
	}

	/* Paste the "sda" where "sda1" was */
	strlcpy(sd_offset, new_sd, strlen(sd_offset) + 1);

	/* Free temporary "sda" */
	free(new_sd);

	return (newpath);
}

/*
 * Strip the unwanted portion of a device path.
 */
const char *
zfs_strip_path(const char *path)
{
	size_t spath_count;
	const char *const *spaths = zpool_default_search_paths(&spath_count);

	for (size_t i = 0; i < spath_count; ++i)
		if (strncmp(path, spaths[i], strlen(spaths[i])) == 0 &&
		    path[strlen(spaths[i])] == '/')
			return (path + strlen(spaths[i]) + 1);

	return (path);
}

/*
 * Read the contents of a sysfs file into an allocated buffer and remove the
 * last newline.
 *
 * This is useful for reading sysfs files that return a single string.  Return
 * an allocated string pointer on success, NULL otherwise.  Returned buffer
 * must be freed by the user.
 */
static char *
zfs_read_sysfs_file(char *filepath)
{
	char buf[4096];	/* all sysfs files report 4k size */
	char *str = NULL;

	FILE *fp = fopen(filepath, "r");
	if (fp == NULL) {
		return (NULL);
	}
	if (fgets(buf, sizeof (buf), fp) == buf) {
		/* success */

		/* Remove the last newline (if any) */
		size_t len = strlen(buf);
		if (buf[len - 1] == '\n') {
			buf[len - 1] = '\0';
		}
		str = strdup(buf);
	}

	fclose(fp);

	return (str);
}

/*
 * Given a dev name like "nvme0n1", return the full PCI slot sysfs path to
 * the drive (in /sys/bus/pci/slots).
 *
 * For example:
 *     dev:            "nvme0n1"
 *     returns:        "/sys/bus/pci/slots/0"
 *
 * 'dev' must be an NVMe device.
 *
 * Returned string must be freed.  Returns NULL on error or no sysfs path.
 */
static char *
zfs_get_pci_slots_sys_path(const char *dev_name)
{
	DIR *dp = NULL;
	struct dirent *ep;
	char *address1 = NULL;
	char *address2 = NULL;
	char *path = NULL;
	char buf[MAXPATHLEN];
	char *tmp;

	/* If they preface 'dev' with a path (like "/dev") then strip it off */
	tmp = strrchr(dev_name, '/');
	if (tmp != NULL)
		dev_name = tmp + 1;    /* +1 since we want the chr after '/' */

	if (strncmp("nvme", dev_name, 4) != 0)
		return (NULL);

	(void) snprintf(buf, sizeof (buf), "/sys/block/%s/device/address",
	    dev_name);

	address1 = zfs_read_sysfs_file(buf);
	if (!address1)
		return (NULL);

	/*
	 * /sys/block/nvme0n1/device/address format will
	 * be "0000:01:00.0" while /sys/bus/pci/slots/0/address will be
	 * "0000:01:00".  Just NULL terminate at the '.' so they match.
	 */
	tmp = strrchr(address1, '.');
	if (tmp != NULL)
		*tmp = '\0';

	dp = opendir("/sys/bus/pci/slots/");
	if (dp == NULL) {
		free(address1);
		return (NULL);
	}

	/*
	 * Look through all the /sys/bus/pci/slots/ subdirs
	 */
	while ((ep = readdir(dp))) {
		/*
		 * We only care about directory names that are a single number.
		 * Sometimes there's other directories like
		 * "/sys/bus/pci/slots/0-3/" in there - skip those.
		 */
		if (!zfs_isnumber(ep->d_name))
			continue;

		(void) snprintf(buf, sizeof (buf),
		    "/sys/bus/pci/slots/%s/address", ep->d_name);

		address2 = zfs_read_sysfs_file(buf);
		if (!address2)
			continue;

		if (strcmp(address1, address2) == 0) {
			/* Addresses match, we're all done */
			free(address2);
			if (asprintf(&path, "/sys/bus/pci/slots/%s",
			    ep->d_name) == -1) {
				continue;
			}
			break;
		}
		free(address2);
	}

	closedir(dp);
	free(address1);

	return (path);
}

/*
 * Given a dev name like "sda", return the full enclosure sysfs path to
 * the disk.  You can also pass in the name with "/dev" prepended
 * to it (like /dev/sda).  This works for both JBODs and NVMe PCI devices.
 *
 * For example, disk "sda" in enclosure slot 1:
 *     dev_name:       "sda"
 *     returns:        "/sys/class/enclosure/1:0:3:0/Slot 1"
 *
 * Or:
 *
 *      dev_name:   "nvme0n1"
 *      returns:    "/sys/bus/pci/slots/0"
 *
 * 'dev' must be a non-devicemapper device.
 *
 * Returned string must be freed.  Returns NULL on error.
 */
char *
zfs_get_enclosure_sysfs_path(const char *dev_name)
{
	DIR *dp = NULL;
	struct dirent *ep;
	char buf[MAXPATHLEN];
	char *tmp1 = NULL;
	char *tmp2 = NULL;
	char *tmp3 = NULL;
	char *path = NULL;
	size_t size;
	int tmpsize;

	if (dev_name == NULL)
		return (NULL);

	/* If they preface 'dev' with a path (like "/dev") then strip it off */
	tmp1 = strrchr(dev_name, '/');
	if (tmp1 != NULL)
		dev_name = tmp1 + 1;    /* +1 since we want the chr after '/' */

	tmpsize = asprintf(&tmp1, "/sys/block/%s/device", dev_name);
	if (tmpsize == -1 || tmp1 == NULL) {
		tmp1 = NULL;
		goto end;
	}

	dp = opendir(tmp1);
	if (dp == NULL)
		goto end;

	/*
	 * Look though all sysfs entries in /sys/block/<dev>/device for
	 * the enclosure symlink.
	 */
	while ((ep = readdir(dp))) {
		/* Ignore everything that's not our enclosure_device link */
		if (strstr(ep->d_name, "enclosure_device") == NULL)
			continue;

		if (tmp2 != NULL)
			free(tmp2);
		if (asprintf(&tmp2, "%s/%s", tmp1, ep->d_name) == -1) {
			tmp2 = NULL;
			break;
		}

		size = readlink(tmp2, buf, sizeof (buf));

		/* Did readlink fail or crop the link name? */
		if (size == -1 || size >= sizeof (buf))
			break;

		/*
		 * We got a valid link.  readlink() doesn't terminate strings
		 * so we have to do it.
		 */
		buf[size] = '\0';

		/*
		 * Our link will look like:
		 *
		 * "../../../../port-11:1:2/..STUFF../enclosure/1:0:3:0/SLOT 1"
		 *
		 * We want to grab the "enclosure/1:0:3:0/SLOT 1" part
		 */
		tmp3 = strstr(buf, "enclosure");
		if (tmp3 == NULL)
			break;

		if (path != NULL)
			free(path);
		if (asprintf(&path, "/sys/class/%s", tmp3) == -1) {
			/* If asprintf() fails, 'path' is undefined */
			path = NULL;
			break;
		}
	}

end:
	free(tmp2);
	free(tmp1);

	if (dp != NULL)
		closedir(dp);

	if (!path) {
		/*
		 * This particular disk isn't in a JBOD.  It could be an NVMe
		 * drive. If so, look up the NVMe device's path in
		 * /sys/bus/pci/slots/. Within that directory is a 'attention'
		 * file which controls the NVMe fault LED.
		 */
		path = zfs_get_pci_slots_sys_path(dev_name);
	}

	return (path);
}

/*
 * Allocate and return the underlying device name for a device mapper device.
 *
 * For example, dm_name = "/dev/dm-0" could return "/dev/sda". Symlinks to a
 * DM device (like /dev/disk/by-vdev/A0) are also allowed.
 *
 * If the DM device has multiple underlying devices (like with multipath
 * DM devices), then favor underlying devices that have a symlink back to their
 * back to their enclosure device in sysfs.  This will be useful for the
 * zedlet scripts that toggle the fault LED.
 *
 * Returns an underlying device name, or NULL on error or no match.  If dm_name
 * is not a DM device then return NULL.
 *
 * NOTE: The returned name string must be *freed*.
 */
static char *
dm_get_underlying_path(const char *dm_name)
{
	DIR *dp = NULL;
	struct dirent *ep;
	char *realp;
	char *tmp = NULL;
	char *path = NULL;
	char *dev_str;
	char *first_path = NULL;
	char *enclosure_path;

	if (dm_name == NULL)
		return (NULL);

	/* dm name may be a symlink (like /dev/disk/by-vdev/A0) */
	realp = realpath(dm_name, NULL);
	if (realp == NULL)
		return (NULL);

	/*
	 * If they preface 'dev' with a path (like "/dev") then strip it off.
	 * We just want the 'dm-N' part.
	 */
	tmp = strrchr(realp, '/');
	if (tmp != NULL)
		dev_str = tmp + 1;    /* +1 since we want the chr after '/' */
	else
		dev_str = tmp;

	if (asprintf(&tmp, "/sys/block/%s/slaves/", dev_str) == -1) {
		tmp = NULL;
		goto end;
	}

	dp = opendir(tmp);
	if (dp == NULL)
		goto end;

	/*
	 * A device-mapper device can have multiple paths to it (multipath).
	 * Favor paths that have a symlink back to their enclosure device.
	 * We have to do this since some enclosures may only provide a symlink
	 * back for one underlying path to a disk and not the other.
	 *
	 * If no paths have links back to their enclosure, then just return the
	 * first path.
	 */
	while ((ep = readdir(dp))) {
		if (ep->d_type != DT_DIR) {	/* skip "." and ".." dirs */
			if (!first_path)
				first_path = strdup(ep->d_name);

			enclosure_path =
			    zfs_get_enclosure_sysfs_path(ep->d_name);

			if (!enclosure_path)
				continue;

			if (asprintf(&path, "/dev/%s", ep->d_name) == -1)
				path = NULL;
			free(enclosure_path);
			break;
		}
	}

end:
	if (dp != NULL)
		closedir(dp);
	free(tmp);
	free(realp);

	if (!path && first_path) {
		/*
		 * None of the underlying paths had a link back to their
		 * enclosure devices.  Throw up out hands and return the first
		 * underlying path.
		 */
		if (asprintf(&path, "/dev/%s", first_path) == -1)
			path = NULL;
	}

	free(first_path);
	return (path);
}

/*
 * Return B_TRUE if device is a device mapper or multipath device.
 * Return B_FALSE if not.
 */
boolean_t
zfs_dev_is_dm(const char *dev_name)
{

	char *tmp;
	tmp = dm_get_underlying_path(dev_name);
	if (tmp == NULL)
		return (B_FALSE);

	free(tmp);
	return (B_TRUE);
}

/*
 * By "whole disk" we mean an entire physical disk (something we can
 * label, toggle the write cache on, etc.) as opposed to the full
 * capacity of a pseudo-device such as lofi or did.  We act as if we
 * are labeling the disk, which should be a pretty good test of whether
 * it's a viable device or not.  Returns B_TRUE if it is and B_FALSE if
 * it isn't.
 */
boolean_t
zfs_dev_is_whole_disk(const char *dev_name)
{
	struct dk_gpt *label = NULL;
	int fd;

	if ((fd = open(dev_name, O_RDONLY | O_DIRECT | O_CLOEXEC)) < 0)
		return (B_FALSE);

	if (efi_alloc_and_init(fd, EFI_NUMPAR, &label) != 0) {
		(void) close(fd);
		return (B_FALSE);
	}

	efi_free(label);
	(void) close(fd);

	return (B_TRUE);
}

/*
 * Lookup the underlying device for a device name
 *
 * Often you'll have a symlink to a device, a partition device,
 * or a multipath device, and want to look up the underlying device.
 * This function returns the underlying device name.  If the device
 * name is already the underlying device, then just return the same
 * name.  If the device is a DM device with multiple underlying devices
 * then return the first one.
 *
 * For example:
 *
 * 1. /dev/disk/by-id/ata-QEMU_HARDDISK_QM00001 -> ../../sda
 * dev_name:	/dev/disk/by-id/ata-QEMU_HARDDISK_QM00001
 * returns:	/dev/sda
 *
 * 2. /dev/mapper/mpatha (made up of /dev/sda and /dev/sdb)
 * dev_name:	/dev/mapper/mpatha
 * returns:	/dev/sda (first device)
 *
 * 3. /dev/sda (already the underlying device)
 * dev_name:	/dev/sda
 * returns:	/dev/sda
 *
 * 4. /dev/dm-3 (mapped to /dev/sda)
 * dev_name:	/dev/dm-3
 * returns:	/dev/sda
 *
 * 5. /dev/disk/by-id/scsi-0QEMU_drive-scsi0-0-0-0-part9 -> ../../sdb9
 * dev_name:	/dev/disk/by-id/scsi-0QEMU_drive-scsi0-0-0-0-part9
 * returns:	/dev/sdb
 *
 * 6. /dev/disk/by-uuid/5df030cf-3cd9-46e4-8e99-3ccb462a4e9a -> ../dev/sda2
 * dev_name:	/dev/disk/by-uuid/5df030cf-3cd9-46e4-8e99-3ccb462a4e9a
 * returns:	/dev/sda
 *
 * Returns underlying device name, or NULL on error or no match.
 *
 * NOTE: The returned name string must be *freed*.
 */
char *
zfs_get_underlying_path(const char *dev_name)
{
	char *name = NULL;
	char *tmp;

	if (dev_name == NULL)
		return (NULL);

	tmp = dm_get_underlying_path(dev_name);

	/* dev_name not a DM device, so just un-symlinkize it */
	if (tmp == NULL)
		tmp = realpath(dev_name, NULL);

	if (tmp != NULL) {
		name = zfs_strip_partition_path(tmp);
		free(tmp);
	}

	return (name);
}


#ifdef HAVE_LIBUDEV

/*
 * A disk is considered a multipath whole disk when:
 *	DEVNAME key value has "dm-"
 *	DM_UUID key exists and starts with 'mpath-'
 *	ID_PART_TABLE_TYPE key does not exist or is not gpt
 *	ID_FS_LABEL key does not exist (disk isn't labeled)
 */
static boolean_t
is_mpath_udev_sane(struct udev_device *dev)
{
	const char *devname, *type, *uuid, *label;

	devname = udev_device_get_property_value(dev, "DEVNAME");
	type = udev_device_get_property_value(dev, "ID_PART_TABLE_TYPE");
	uuid = udev_device_get_property_value(dev, "DM_UUID");
	label = udev_device_get_property_value(dev, "ID_FS_LABEL");

	if ((devname != NULL && strncmp(devname, "/dev/dm-", 8) == 0) &&
	    ((type == NULL) || (strcmp(type, "gpt") != 0)) &&
	    ((uuid != NULL) && (strncmp(uuid, "mpath-", 6) == 0)) &&
	    (label == NULL)) {
		return (B_TRUE);
	}

	return (B_FALSE);
}

/*
 * Check if a disk is a multipath "blank" disk:
 *
 * 1. The disk has udev values that suggest it's a multipath disk
 * 2. The disk is not currently labeled with a filesystem of any type
 * 3. There are no partitions on the disk
 */
boolean_t
is_mpath_whole_disk(const char *path)
{
	struct udev *udev;
	struct udev_device *dev = NULL;
	char nodepath[MAXPATHLEN];
	char *sysname;

	if (realpath(path, nodepath) == NULL)
		return (B_FALSE);
	sysname = strrchr(nodepath, '/') + 1;
	if (strncmp(sysname, "dm-", 3) != 0)
		return (B_FALSE);
	if ((udev = udev_new()) == NULL)
		return (B_FALSE);
	if ((dev = udev_device_new_from_subsystem_sysname(udev, "block",
	    sysname)) == NULL) {
		udev_device_unref(dev);
		return (B_FALSE);
	}

	/* Sanity check some udev values */
	boolean_t is_sane = is_mpath_udev_sane(dev);
	udev_device_unref(dev);

	return (is_sane);
}

#else /* HAVE_LIBUDEV */

boolean_t
is_mpath_whole_disk(const char *path)
{
	(void) path;
	return (B_FALSE);
}

#endif /* HAVE_LIBUDEV */
