/*
    devname.c : conversion functions for block device names to
        and from major/minor numbers
        Most of this code is taken from the kernel
        init/ and kinit source.
    Copyright (C) 2006, Aaron Griffin <aaronmgriffin@gmail.com>

    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 <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <ctype.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <alloca.h>
#include <inttypes.h>
#include <stddef.h>
#include <sys/types.h>
#include <sys/sysmacros.h>
#include <dirent.h>

#include "devname.h"

/* Find dev_t for e.g. "hda,NULL" or "hdb,2" */
dev_t
try_name(char *name, int part)
{
	char path[BUF_SZ];
	char buf[BUF_SZ];
	int range;
	unsigned int major, minor;
	dev_t res;
	char *s;
	int len;
	int fd;

	/* read device number from /sys/block/.../dev */
	snprintf(path, sizeof(path), "/sys/block/%s/dev", name);
	fd = open(path, 0, 0);
	if (fd < 0)
		goto fail;
	len = read(fd, buf, BUF_SZ);
	close(fd);

	if (len <= 0 || len == BUF_SZ || buf[len - 1] != '\n')
		goto fail;
	buf[len - 1] = '\0';
	major = strtoul(buf, &s, 10);
	if (*s != ':')
		goto fail;
	minor = strtoul(s+1, &s, 10);
	if (*s)
		goto fail;
	res = makedev(major, minor);

	/* if it's there and we are not looking for a partition - that's it */
	if (!part)
		return res;

	/* otherwise read range from .../range */
	snprintf(path, sizeof(path), "/sys/block/%s/range", name);
	fd = open(path, 0, 0);
	if (fd < 0)
		goto fail;
	len = read(fd, buf, 32);
	close(fd);
	if (len <= 0 || len == 32 || buf[len - 1] != '\n')
		goto fail;
	buf[len - 1] = '\0';
	range = strtoul(buf, &s, 10);
	if (*s)
		goto fail;

	/* if partition is within range - we got it */
	if (part < range) {
		return res + part;
	}

fail:
	return (dev_t) 0;
}

/*
 *	Convert a name into device number.  We accept the following variants:
 *
 *	1) device number in hexadecimal	represents itself
 *	2) /dev/nfs represents Root_NFS
 *	3) /dev/<disk_name> represents the device number of disk
 *	4) /dev/<disk_name><decimal> represents the device number
 *         of partition - device number of disk plus the partition number
 *	5) /dev/<disk_name>p<decimal> - same as the above, that form is
 *	   used when disk name of partitioned disk ends on a digit.
 *	6) an actual block device node in the initramfs filesystem
 *
 *	If name doesn't have fall into the categories above, we return 0.
 *	Driverfs is used to check if something is a disk name - it has
 *	all known disks under bus/block/devices.  If the disk name
 *	contains slashes, name of driverfs node has them replaced with
 *	dots.  try_name() does the actual checks, assuming that driverfs
 *	is mounted on rootfs /sys.
 */

dev_t
name_to_dev_t(const char *name)
{
	char *p;
	dev_t res = 0;
	char *s;
	int part;
	struct stat st;
	int len;
	const char *devname;

	if ( name[0] == '/' )
		devname = name;
	else {
		char *dname = alloca(strlen(name) + 6);
		sprintf(dname, "/dev/%s", name);
		devname = dname;
	}

	if (!stat(devname, &st) && S_ISBLK(st.st_mode))
		return st.st_rdev;

	if (strncmp(name, "/dev/", 5)) {
		res = (dev_t) strtoul(name, &p, 16);
		if (*p)
			return 0;
		return res;
	}
	name += 5;
	if (strcmp(name, "nfs") == 0)
		return Root_NFS;

	len = strlen(name);
	s = alloca(len+1);
	memcpy(s, name, len+1);

	for (p = s; *p; p++)
		if (*p == '/')
			*p = '.';
	res = try_name(s, 0);
	if (res)
		return res;

	while (p > s && isdigit(p[-1]))
		p--;
	if (p == s || !*p || *p == '0')
		goto fail;
	part = strtoul(p, NULL, 10);
	*p = '\0';
	res = try_name(s, part);
	if (res)
		return res;

	if (p < s + 2 || !isdigit(p[-2]) || p[-1] != 'p')
		goto fail;
	p[-1] = '\0';
	res = try_name(s, part);
	return res;

 fail:
	return (dev_t)0;
}

/*
 * Print the name of a block device.
 */
int
scansysdir(char *namebuf, char *sysdir, dev_t dev)
{
	char *dirtailptr = strchr(sysdir, '\0');
	DIR *dir;
	int done = 0;
	struct dirent *de;
	char *systail;
	FILE *sysdev;
	unsigned long ma, mi;
	char *ep;
	ssize_t rd;

	if (!(dir = opendir(sysdir)))
		return 0;

	*dirtailptr++ = '/';

	while (!done && (de = readdir(dir))) {
		/* Assume if we see a dot-name in sysfs it's special */
		if (de->d_name[0] == '.')
			continue;

		if (de->d_type != DT_UNKNOWN && de->d_type != DT_DIR)
			continue;

		if (strlen(de->d_name) >= (size_t)(TMPBUF_SZ-64)-(dirtailptr-sysdir))
			continue; /* Badness... */

		strcpy(dirtailptr, de->d_name);
		systail = strchr(sysdir, '\0');

		strcpy(systail, "/dev");
		if (!(sysdev = fopen(sysdir, "r")))
			continue;

		/* Abusing the namebuf as temporary storage here. */
		rd = fread(namebuf, 1, TMPBUF_SZ, sysdev);
		namebuf[rd] = '\0'; /* Just in case... */

		fclose(sysdev);

		ma = strtoul(namebuf, &ep, 10);
		if (ma != (unsigned long)major(dev) || *ep != ':')
			continue;

		mi = strtoul(ep+1, &ep, 10);
		if (*ep != '\n')
			continue;

		if (mi == (unsigned long)minor(dev)) {
			/* Found it! */
			strcpy(namebuf, de->d_name);
			done = 1;
		} else {
			/* we have a major number match, scan for partitions */
			*systail = '\0';
			done = scansysdir(namebuf, sysdir, dev);
		}
	}

	closedir(dir);
	return done;
}
