/*
 * Copyright (C) 2004-2007 Red Hat, Inc. All rights reserved.
 *
 * This file is part of LVM2.
 *
 * This copyrighted material is made available to anyone wishing to use,
 * modify, copy, or redistribute it subject to the terms and conditions
 * of the GNU Lesser General Public License v.2.1.
 *
 * You should have received a copy of the GNU Lesser 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 "lib/misc/lib.h"
#include "lib/filters/filter.h"

#ifdef __linux__

#include <sys/sysmacros.h>
#include <dirent.h>

static int _locate_sysfs_blocks(const char *sysfs_dir, char *path, size_t len,
				unsigned *sysfs_depth)
{
	struct stat info;
	unsigned i;
	static const struct dir_class {
		const char path[32];
		int depth;
	} classes[] = {
		/*
		 * unified classification directory for all kernel subsystems
		 *
		 * /sys/subsystem/block/devices
		 * |-- sda -> ../../../devices/pci0000:00/0000:00:1f.2/host0/target0:0:0/0:0:0:0/block/sda
		 * |-- sda1 -> ../../../devices/pci0000:00/0000:00:1f.2/host0/target0:0:0/0:0:0:0/block/sda/sda1
		 *  `-- sr0 -> ../../../devices/pci0000:00/0000:00:1f.2/host1/target1:0:0/1:0:0:0/block/sr0
		 *
		 */
		{ "subsystem/block/devices", 0 },

		/*
		 * block subsystem as a class
		 *
		 * /sys/class/block
		 * |-- sda -> ../../devices/pci0000:00/0000:00:1f.2/host0/target0:0:0/0:0:0:0/block/sda
		 * |-- sda1 -> ../../devices/pci0000:00/0000:00:1f.2/host0/target0:0:0/0:0:0:0/block/sda/sda1
		 *  `-- sr0 -> ../../devices/pci0000:00/0000:00:1f.2/host1/target1:0:0/1:0:0:0/block/sr0
		 *
		 */
		{ "class/block", 0 },

		/*
		 * old block subsystem layout with nested directories
		 *
		 * /sys/block/
		 * |-- sda
		 * |   |-- capability
		 * |   |-- dev
		 * ...
		 * |   |-- sda1
		 * |   |   |-- dev
		 * ...
		 * |
		 * `-- sr0
		 *     |-- capability
		 *     |-- dev
		 * ...
		 *
		 */

		{ "block", 1 }
	};

	for (i = 0; i < DM_ARRAY_SIZE(classes); ++i)
		if ((dm_snprintf(path, len, "%s%s", sysfs_dir, classes[i].path) >= 0) &&
		    (stat(path, &info) == 0)) {
			*sysfs_depth = classes[i].depth;
			return 1;
		}

	return 0;
}

/*----------------------------------------------------------------
 * We need to store a set of dev_t.
 *--------------------------------------------------------------*/
struct entry {
	struct entry *next;
	dev_t dev;
};

#define SET_BUCKETS 64
struct dev_set {
	struct dm_pool *mem;
	const char *sys_block;
	unsigned sysfs_depth;
	int initialised;
	struct entry *slots[SET_BUCKETS];
};

static struct dev_set *_dev_set_create(struct dm_pool *mem,
				       const char *sys_block,
				       unsigned sysfs_depth)
{
	struct dev_set *ds;

	if (!(ds = dm_pool_zalloc(mem, sizeof(*ds))))
		return NULL;

	ds->mem = mem;
	if (!(ds->sys_block = dm_pool_strdup(mem, sys_block)))
		return NULL;

	ds->sysfs_depth = sysfs_depth;
	ds->initialised = 0;

	return ds;
}

static unsigned _hash_dev(dev_t dev)
{
	return (major(dev) ^ minor(dev)) & (SET_BUCKETS - 1);
}

/*
 * Doesn't check that the set already contains dev.
 */
static int _set_insert(struct dev_set *ds, dev_t dev)
{
	struct entry *e;
	unsigned h = _hash_dev(dev);

	if (!(e = dm_pool_alloc(ds->mem, sizeof(*e))))
		return 0;

	e->next = ds->slots[h];
	e->dev = dev;
	ds->slots[h] = e;

	return 1;
}

static int _set_lookup(struct dev_set *ds, dev_t dev)
{
	unsigned h = _hash_dev(dev);
	struct entry *e;

	for (e = ds->slots[h]; e; e = e->next)
		if (e->dev == dev)
			return 1;

	return 0;
}

/*----------------------------------------------------------------
 * filter methods
 *--------------------------------------------------------------*/
static int _parse_dev(const char *file, FILE *fp, dev_t *result)
{
	unsigned major, minor;
	char buffer[64];

	if (!fgets(buffer, sizeof(buffer), fp)) {
		log_error("Empty sysfs device file: %s", file);
		return 0;
	}

	if (sscanf(buffer, "%u:%u", &major, &minor) != 2) {
		log_error("Incorrect format for sysfs device file: %s.", file);
		return 0;
	}

	*result = makedev(major, minor);
	return 1;
}

static int _read_dev(const char *file, dev_t *result)
{
	int r;
	FILE *fp;

	if (!(fp = fopen(file, "r"))) {
		log_sys_error("fopen", file);
		return 0;
	}

	r = _parse_dev(file, fp, result);

	if (fclose(fp))
		log_sys_error("fclose", file);

	return r;
}

/*
 * Recurse through sysfs directories, inserting any devs found.
 */
static int _read_devs(struct dev_set *ds, const char *dir, unsigned sysfs_depth)
{
	struct dirent *d;
	DIR *dr;
	struct stat info;
	char path[PATH_MAX];
	char file[PATH_MAX];
	dev_t dev = { 0 };
	int r = 1;

	if (!(dr = opendir(dir))) {
		log_sys_error("opendir", dir);
		return 0;
	}

	while ((d = readdir(dr))) {
		if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
			continue;

		if (dm_snprintf(path, sizeof(path), "%s/%s", dir,
				 d->d_name) < 0) {
			log_error("sysfs path name too long: %s in %s",
				  d->d_name, dir);
			continue;
		}

		/* devices have a "dev" file */
		if (dm_snprintf(file, sizeof(file), "%s/dev", path) < 0) {
			log_error("sysfs path name too long: %s in %s",
				  d->d_name, dir);
			continue;
		}

		if (!stat(file, &info)) {
			/* recurse if we found a device and expect subdirs */
			if (sysfs_depth)
				_read_devs(ds, path, sysfs_depth - 1);

			/* add the device we have found */
			if (_read_dev(file, &dev))
				_set_insert(ds, dev);
		}
	}

	if (closedir(dr))
		log_sys_error("closedir", dir);

	return r;
}

static int _init_devs(struct dev_set *ds)
{
	if (!_read_devs(ds, ds->sys_block, ds->sysfs_depth)) {
		ds->initialised = -1;
		return 0;
	}

	ds->initialised = 1;

	return 1;
}


static int _accept_p(struct cmd_context *cmd, struct dev_filter *f, struct device *dev, const char *use_filter_name)
{
	struct dev_set *ds = (struct dev_set *) f->private;

	dev->filtered_flags &= ~DEV_FILTERED_SYSFS;

	if (!ds->initialised)
		_init_devs(ds);

	/* Pass through if initialisation failed */
	if (ds->initialised != 1)
		return 1;

	if (!_set_lookup(ds, dev->dev)) {
		log_debug_devs("%s: Skipping (sysfs)", dev_name(dev));
		dev->filtered_flags |= DEV_FILTERED_SYSFS;
		return 0;
	}

	return 1;
}

static void _destroy(struct dev_filter *f)
{
	struct dev_set *ds = (struct dev_set *) f->private;

	if (f->use_count)
		log_error(INTERNAL_ERROR "Destroying sysfs filter while in use %u times.", f->use_count);

	dm_pool_destroy(ds->mem);
}

struct dev_filter *sysfs_filter_create(void)
{
	const char *sysfs_dir = dm_sysfs_dir();
	char sys_block[PATH_MAX];
	unsigned sysfs_depth;
	struct dm_pool *mem;
	struct dev_set *ds;
	struct dev_filter *f;

	if (!*sysfs_dir) {
		log_verbose("No proc filesystem found: skipping sysfs filter");
		return NULL;
	}

	if (!_locate_sysfs_blocks(sysfs_dir, sys_block, sizeof(sys_block), &sysfs_depth))
		return NULL;

	if (!(mem = dm_pool_create("sysfs", 256))) {
		log_error("sysfs pool creation failed");
		return NULL;
	}

	if (!(ds = _dev_set_create(mem, sys_block, sysfs_depth))) {
		log_error("sysfs dev_set creation failed");
		goto bad;
	}

	if (!(f = dm_pool_zalloc(mem, sizeof(*f))))
		goto_bad;

	f->passes_filter = _accept_p;
	f->destroy = _destroy;
	f->use_count = 0;
	f->private = ds;
	f->name = "sysfs";

	log_debug_devs("Sysfs filter initialised.");

	return f;

 bad:
	dm_pool_destroy(mem);
	return NULL;
}

#else

struct dev_filter *sysfs_filter_create(const char *sysfs_dir __attribute__((unused)))
{
	return NULL;
}

#endif
