/*
 * zdev - Modify and display the persistent configuration of devices
 *
 * Copyright IBM Corp. 2016, 2017
 *
 * s390-tools is free software; you can redistribute it and/or modify
 * it under the terms of the MIT license. See LICENSE for details.
 */

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "blkinfo.h"
#include "devnode.h"
#include "misc.h"

#define LSBLK_CMDLINE	"lsblk -P -o NAME,MAJ:MIN,FSTYPE,UUID,MOUNTPOINT,PKNAME 2>/dev/null"
#define LSBLK_CMDLINE2	"lsblk -P -o NAME,MAJ:MIN,FSTYPE,UUID,MOUNTPOINTS,PKNAME 2>/dev/null"

struct blkinfo {
	struct devnode *devnode;
	char *fstype;
	char *uuid;
	char *mountpoint;
	char *parent;
};

static struct util_list *cached_blkinfos;

static struct blkinfo *blkinfo_new(const char *name, const char *majmin,
				   const char *fstype, const char *uuid,
				   const char *mountpoint, const char *parent)
{
	struct blkinfo *blkinfo;
	unsigned int major, minor;

	blkinfo = misc_malloc(sizeof(struct blkinfo));
	if (name && majmin) {
		if (sscanf(majmin, "%u:%u", &major, &minor) == 2) {
			blkinfo->devnode = devnode_new(BLOCKDEV, major, minor,
						       name);
		}
	}
	if (fstype && *fstype)
		blkinfo->fstype = misc_strdup(fstype);
	if (uuid && *uuid)
		blkinfo->uuid = misc_strdup(uuid);
	if (mountpoint && *mountpoint)
		blkinfo->mountpoint = misc_strdup(mountpoint);
	if (parent && *parent)
		blkinfo->parent = misc_strdup(parent);

	return blkinfo;
}

static void blkinfo_free(struct blkinfo *blkinfo)
{
	if (!blkinfo)
		return;
	free(blkinfo->devnode);
	free(blkinfo->fstype);
	free(blkinfo->uuid);
	free(blkinfo->mountpoint);
	free(blkinfo->parent);
	free(blkinfo);
}

/* Used for debugging. */
void blkinfo_print(struct blkinfo *blkinfo, int level)
{
	printf("%*sblkinfo at %p\n", level, "", (void *) blkinfo);
	level += 2;
	if (blkinfo->devnode)
		devnode_print(blkinfo->devnode, level);
	if (blkinfo->fstype)
		printf("%*sfstype=%s\n", level, "", blkinfo->fstype);
	if (blkinfo->uuid)
		printf("%*suuid=%s\n", level, "", blkinfo->uuid);
	if (blkinfo->mountpoint)
		printf("%*smountpoint=%s\n", level, "", blkinfo->mountpoint);
	if (blkinfo->parent)
		printf("%*sparent=%s\n", level, "", blkinfo->parent);
}

/* Convert each occurrence of '\xnn' in @str to character with hex code <nn>. */
static void hex_unescape(char *str)
{
	unsigned int c;

	while ((str = strstr(str, "\\x"))) {
		if (isxdigit(str[2]) && isxdigit(str[3]) &&
		    sscanf(str + 2, "%2x", &c) == 1) {
			str[0] = (char)c;

			/* Move remainder of str including nul behind <c>. */
			memmove(str + /* <c> */ 1,
				str + /* '\xnn' */ 4,
				strlen(str + 4) + /* <nul> */ 1);
		}

		str++;
	}
}

static char *isolate_keyword(char **line_ptr, const char *keyword)
{
	char *start, *end;

	start = strstr(*line_ptr, keyword);
	if (!start)
		return NULL;
	start += strlen(keyword);
	end = start;
	while (*end && *end != '"')
		end++;
	if (*end) {
		*end = 0;
		*line_ptr = end + 1;
	} else
		*line_ptr = end;

	return start;
}

static void add_blkinfos_from_line(struct util_list *blkinfos,
				   char *line)
{
	char *name, *majmin, *fstype, *uuid, *mountpoint, *mountpoints, *parent;
	struct blkinfo *blkinfo;

	name		= isolate_keyword(&line, "NAME=\"");
	majmin		= isolate_keyword(&line, "MAJ:MIN=\"");
	if (!majmin)
		majmin  = isolate_keyword(&line, "MAJ_MIN=\"");
	fstype		= isolate_keyword(&line, "FSTYPE=\"");
	uuid		= isolate_keyword(&line, "UUID=\"");
	mountpoint	= isolate_keyword(&line, "MOUNTPOINT=\"");
	mountpoints	= isolate_keyword(&line, "MOUNTPOINTS=\"");
	parent		= isolate_keyword(&line, "PKNAME=\"");

	if (!mountpoints) {
		/* Handle old lsblk output format. */
		blkinfo = blkinfo_new(name, majmin, fstype, uuid, mountpoint,
				      parent);
		ptrlist_add(blkinfos, blkinfo);
		return;
	}

	/* Restore newline mount point separator encoded as hex. */
	hex_unescape(mountpoints);

	/* Represent each mount point as a separate blkinfo to support
	 * resolution of multi-mount point file systems like btrfs
	 * subvolumes. */
	while ((mountpoint = strsep(&mountpoints, "\n"))) {
		blkinfo = blkinfo_new(name, majmin, fstype, uuid, mountpoint,
				      parent);
		ptrlist_add(blkinfos, blkinfo);
	}
}

static struct util_list *blkinfos_read(void)
{
	char *output, *curr, *next;
	struct util_list *blkinfos;

	if (cached_blkinfos)
		return cached_blkinfos;

	output = misc_read_cmd_output(LSBLK_CMDLINE2, 0, 1);
	if (output && !*output) {
		/* No output might indicate no support for new lsblk command-
		 * line format - fall back to old format. */
		free(output);
		output = misc_read_cmd_output(LSBLK_CMDLINE, 0, 1);
	}
	if (!output)
		return NULL;

	blkinfos = ptrlist_new();

	/* Iterate over each line. */
	next = output;
	while ((curr = strsep(&next, "\n"))) {
		add_blkinfos_from_line(blkinfos, curr);
	}

	free(output);

	cached_blkinfos = blkinfos;

	return blkinfos;
}

static void blkinfos_free(struct util_list *blkinfos)
{
	struct ptrlist_node *p, *n;

	if (!blkinfos)
		return;

	util_list_iterate_safe(blkinfos, p, n) {
		util_list_remove(blkinfos, p);
		blkinfo_free(p->ptr);
		free(p);
	}
	free(blkinfos);
}

/* Used for debugging. */
void blkinfos_print(struct util_list *blkinfos, int level)
{
	struct ptrlist_node *p;

	printf("%*sblkinfos at %p\n", level, "", (void *) blkinfos);
	if (!blkinfos)
		return;
	level += 2;
	util_list_iterate(blkinfos, p)
		blkinfo_print(p->ptr, level);
}

/* Find a blkinfo for the specified devnode or NULL of none was found. */
static struct blkinfo *blkinfo_get_by_devnode(struct devnode *devnode)
{
	struct util_list *blkinfos;
	struct ptrlist_node *p;
	struct blkinfo *b;

	blkinfos = blkinfos_read();
	if (!blkinfos)
		return NULL;

	/* Get UUID for the specified devnode. */
	util_list_iterate(blkinfos, p) {
		b = p->ptr;
		if (b->devnode && devnode_cmp(b->devnode, devnode) == 0)
			return b;
	}

	return NULL;
}

/* Find a blkinfo for the specified device name or NULL if none was found. */
static struct blkinfo *blkinfo_get_by_name(const char *name)
{
	struct util_list *blkinfos;
	struct ptrlist_node *p;
	struct blkinfo *b;

	blkinfos = blkinfos_read();
	if (!blkinfos)
		return NULL;

	/* Get UUID for the specified devnode. */
	util_list_iterate(blkinfos, p) {
		b = p->ptr;
		if (b->devnode && strcmp(b->devnode->name, name) == 0)
			return b;
	}

	return NULL;
}

/* Find a blkinfo for the specified major+minor or NULL if none was found. */
static struct blkinfo *blkinfo_get_by_majmin(unsigned int major,
					     unsigned int minor)
{
	struct util_list *blkinfos;
	struct ptrlist_node *p;
	struct blkinfo *b;

	blkinfos = blkinfos_read();
	if (!blkinfos)
		return NULL;

	/* Get UUID for the specified devnode. */
	util_list_iterate(blkinfos, p) {
		b = p->ptr;
		if (b->devnode && b->devnode->major == major &&
		    b->devnode->minor == minor)
			return b;
	}

	return NULL;
}

/* Return a newly allocated ptrlist of devnodes of block devices which are
 * ancestors of the specified block device or NULL if no ancestors are found.
 * An ancestor is a block device which is the parent of another block device
 * according to lsblk data. */
struct util_list *blkinfo_get_ancestor_devnodes(struct devnode *devnode)
{
	struct util_list *blkinfos;
	struct util_list *todos, *result;
	struct util_list *done;
	struct devnode *curr, *p_devnode;
	struct blkinfo *blkinfo, *parent;
	struct ptrlist_node *p_curr, *p;
	int num_parents;

	blkinfos = blkinfos_read();
	if (!blkinfos)
		return NULL;

	todos = ptrlist_new();
	result = ptrlist_new();
	done = strlist_new();

	/* Repeat the resolution process until only devices with no parent
	 * remain. */
	ptrlist_add(todos, devnode_copy(devnode));
	while ((p_curr = util_list_start(todos))) {
		util_list_remove(todos, p_curr);
		curr = p_curr->ptr;
		free(p_curr);

		/* Check for parents. */
		num_parents = 0;
		util_list_iterate(blkinfos, p) {
			blkinfo = p->ptr;
			if (!blkinfo->devnode || !blkinfo->parent)
				continue;
			if (devnode_cmp(blkinfo->devnode, curr) != 0)
				continue;
			parent = blkinfo_get_by_name(blkinfo->parent);
			if (parent && parent->devnode)
				goto add;

			/* Try to resolve name to major:minor. */
			p_devnode = devnode_from_devfile(NULL, blkinfo->parent,
							 BLOCKDEV);
			if (!p_devnode)
				continue;
			parent = blkinfo_get_by_majmin(p_devnode->major,
						       p_devnode->minor);
			free(p_devnode);
			if (!parent || !parent->devnode)
				continue;

add:
			ptrlist_add(todos, devnode_copy(parent->devnode));
			num_parents++;
		}

		if (num_parents == 0 && devnode_cmp(curr, devnode) != 0 &&
		    !strlist_find(done, curr->name)) {
			/* Add device with no parent to results (except for
			 * the initially specified device). */
			ptrlist_add(result, curr);
			strlist_add(done, curr->name);
		} else
			free(curr);
	}
	ptrlist_free(todos, 0);
	strlist_free(done);

	/* Check if any ancestors were found. */
	if (util_list_is_empty(result)) {
		ptrlist_free(result, 0);
		return NULL;
	}

	return result;
}

/* Return a newly allocated ptrlist of devnodes of block devices which have
 * the same file system UUID and type or NULL if no such devices are found. */
struct util_list *blkinfo_get_same_uuid_devnodes(struct devnode *devnode)
{
	struct util_list *blkinfos;
	struct util_list *devnodes;
	struct ptrlist_node *p;
	struct blkinfo *b;
	char *uuid;

	blkinfos = blkinfos_read();
	if (!blkinfos)
		return NULL;

	b = blkinfo_get_by_devnode(devnode);
	if (!b)
		return NULL;

	uuid = b->uuid;
	if (!uuid)
		return NULL;

	/* Get all devnodes of devices with the same UUID. */
	devnodes = ptrlist_new();
	util_list_iterate(blkinfos, p) {
		b = p->ptr;
		if (!b->devnode || !b->uuid)
			continue;
		if (strcmp(b->uuid, uuid) != 0)
			continue;
		if (devnode_cmp(b->devnode, devnode) == 0)
			continue;
		ptrlist_add(devnodes, devnode_copy(b->devnode));
	}

	if (util_list_is_empty(devnodes)) {
		strlist_free(devnodes);
		devnodes = NULL;
	}

	return devnodes;
}

/* Check mountpoints in blkinfo for an entry that provides path. */
struct devnode *blkinfo_get_devnode_by_path(const char *path)
{
	struct util_list *blkinfos;
	struct ptrlist_node *p;
	struct blkinfo *b, *match = NULL;
	size_t blen, mlen = 0;

	blkinfos = blkinfos_read();
	if (!blkinfos)
		return NULL;

	util_list_iterate(blkinfos, p) {
		b = p->ptr;
		if (!b->devnode || !b->mountpoint)
			continue;
		blen = strlen(b->mountpoint);

		/* path:       /path/to/file
		 * mountpoint: /path */
		if (strncmp(path, b->mountpoint, blen) != 0)
			continue;
		if (path[blen] && path[blen] != '/')
			continue;

		/* Ensure longest match. */
		if (!match || blen > mlen) {
			match = b;
			mlen = blen;
		}
	}

	return match ? devnode_copy(match->devnode) : NULL;
}

/* Add list of mountpoints for mounted file systems to strlist @list. */
void blkinfo_add_mountpoints(struct util_list *list)
{
	struct util_list *blkinfos;
	struct ptrlist_node *p;
	struct blkinfo *b;

	blkinfos = blkinfos_read();
	if (!blkinfos)
		return;

	util_list_iterate(blkinfos, p) {
		b = p->ptr;
		if (!b->devnode || !b->mountpoint || *b->mountpoint != '/')
			continue;
		strlist_add_unique(list, b->mountpoint);
	}
}

/* Add list of newly allocated devnodes for active swap devices to ptrlist
 * @list. */
void blkinfo_add_swap_devnodes(struct util_list *list)
{
	struct util_list *blkinfos;
	struct ptrlist_node *p;
	struct blkinfo *b;

	blkinfos = blkinfos_read();
	if (!blkinfos)
		return;

	util_list_iterate(blkinfos, p) {
		b = p->ptr;
		if (!b->devnode || !b->mountpoint ||
		    strcmp(b->mountpoint, "[SWAP]") != 0)
			continue;
		ptrlist_add(list, devnode_copy(b->devnode));
	}
}

void blkinfo_exit(void)
{
	blkinfos_free(cached_blkinfos);
}
