/*
 * 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 <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "lib/util_path.h"
#include "lib/util_udev.h"

#include "attrib.h"
#include "ccwgroup.h"
#include "device.h"
#include "internal.h"
#include "misc.h"
#include "path.h"
#include "setting.h"
#include "udev.h"
#include "udev_ccwgroup.h"

static char *get_rule_path_by_devid(const char *type,
				    struct ccwgroup_devid *devid, bool autoconf)
{
	char *ccw_id, *path;

	ccw_id = ccw_devid_to_str(&devid->devid[0]);
	path = path_get_udev_rule(type, ccw_id, autoconf);
	free(ccw_id);

	return path;
}

static char *get_rule_path(const char *type, const char *id, bool autoconf)
{
	struct ccwgroup_devid devid;

	if (ccwgroup_parse_devid(&devid, id, err_ignore) != EXIT_OK)
		return NULL;

	return get_rule_path_by_devid(type, &devid, autoconf);
}

/* Check if a udev rule for the specified CCWGROUP device exists. */
bool udev_ccwgroup_exists(const char *type, const char *id, bool autoconf)
{
	char *path;
	bool rc;

	path = get_rule_path(type, id, autoconf);
	if (!path)
		return false;
	rc = util_path_is_reg_file(path);
	free(path);

	return rc;
}

static void add_setting_from_entry(struct setting_list *list,
				   struct util_udev_entry_node *entry,
				   struct attrib **attribs)
{
	char *copy, *name, *end;
	struct attrib *a;

	/* ENV{zdev_var}="1" */
	if (starts_with(entry->key, "ENV{zdev_") &&
	    strcmp(entry->op, "=") == 0) {
		udev_add_internal_from_entry(list, entry, attribs);
		return;
	}
	/* ATTR{[ccwgroup/0.0.f5f0]online}=1 */
	if (strncmp(entry->key, "ATTR{[ccwgroup/", 10) != 0 ||
	    strcmp(entry->op, "=") != 0)
		return;
	copy = misc_strdup(entry->key);
	name = copy;

	/* Find attribute name start. */
	name = strchr(entry->key, ']');
	end = strrchr(entry->key, '}');
	if (!name || !end)
		goto out;
	*end = 0;
	name++;

	a = attrib_find(attribs, name);
	setting_list_apply_actual(list, a, name, entry->value);

out:
	free(copy);
}

/* Extract CCWGROUP device settings from a CCWGROUP device udev rule file. */
static void udev_file_get_settings(struct util_udev_file *file,
				   struct attrib **attribs,
				   struct setting_list *list)
{
	struct util_udev_line_node *line;
	struct util_udev_entry_node *entry;

	util_list_iterate(&file->lines, line) {
		entry = util_list_start(&line->entries);
		if (!entry)
			continue;
		add_setting_from_entry(list, entry, attribs);
	}
}

/* Determine full CCWGROUP from data in udev rule file. */
static void expand_id(struct device *dev, struct util_udev_file *file)
{
	struct ccwgroup_devid devid;
	struct ccwgroup_devid *devid_ptr = dev->devid;
	struct util_udev_line_node *line;
	struct util_udev_entry_node *entry;
	char *id;
	int i;

	if (devid_ptr->num != 1)
		return;

	util_list_iterate(&file->lines, line) {
		entry = util_list_start(&line->entries);
		if (!entry)
			continue;
		if (!starts_with(entry->key, "ATTR{[drivers/ccwgroup:") ||
		    !ends_with(entry->key, "]group}"))
			continue;
		/* Extract CCWGROUP ID from comma-separated list of CCW
		 * device IDs. */
		id = misc_strdup(entry->value);
		for (i = 0; id[i]; i++) {
			if (id[i] == ',')
				id[i] = ':';
		}
		if (ccwgroup_parse_devid(&devid, id, err_ignore) == EXIT_OK)
			*devid_ptr = devid;
		free(dev->id);
		dev->id = ccwgroup_devid_to_str(&devid);
		free(id);
		break;
	}
}

/* Read the persistent configuration of a CCWGROUP device from a udev rule. */
exit_code_t udev_ccwgroup_read_device(struct device *dev, bool autoconf)
{
	struct subtype *st = dev->subtype;
	struct device_state *state = autoconf ? &dev->autoconf :
						&dev->persistent;
	struct util_udev_file *file = NULL;
	exit_code_t rc;
	char *path;

	path = get_rule_path_by_devid(st->name, dev->devid, autoconf);
	rc = (exit_code_t) util_udev_read_file(path, &file);
	if (rc)
		goto out;
	if (udev_file_is_empty(file)) {
		warn_once("Warning: Invalid udev rule: %s\n", path);
		state->exists = 0;
	} else {
		udev_file_get_settings(file, st->dev_attribs, state->settings);
		expand_id(dev, file);
		state->exists = 1;
	}
	util_udev_free_file(file);

out:
	free(path);

	return rc;
}

/* Return an ID suitable for use as udev label. */
static char *get_label_id(const char *prefix, const char *type,
			  const char *dev_id)
{
	char *id;
	int i;

	id = misc_asprintf("%s_%s_%s", prefix, type, dev_id);
	for (i = 0; id[i]; i++) {
		if (isalnum(id[i]) || id[i] == '_' || id[i] == '.')
			continue;
		id[i] = '_';
	}

	return id;
}

/* Write the persistent configuration of a CCWGROUP device to a udev rule. */
exit_code_t udev_ccwgroup_write_device(struct device *dev, bool autoconf)
{
	struct subtype *st = dev->subtype;
	struct device_state *state = autoconf ? &dev->autoconf :
						&dev->persistent;
	struct ccwgroup_subtype_data *data = st->data;
	const char *type = st->name, *drv = data->ccwgroupdrv, *id = dev->id;
	struct ccwgroup_devid devid;
	char *path, *cfg_label = NULL, *group_label = NULL, *end_label = NULL,
	     *ccw_id, *chan_id;
	struct util_list *list;
	struct ptrlist_node *p;
	struct setting *s;
	struct strlist_node *str;
	exit_code_t rc = EXIT_OK;
	FILE *fd;
	unsigned int i;

	if (!state->exists)
		return udev_ccwgroup_remove_rule(type, id, autoconf);

	if (ccwgroup_parse_devid(&devid, id, err_ignore) != EXIT_OK)
		return EXIT_INVALID_ID;

	ccw_id = ccw_devid_to_str(&devid.devid[0]);
	path = get_rule_path(type, ccw_id, autoconf);

	group_label = get_label_id("group", type, ccw_id);
	cfg_label = get_label_id("cfg", type, ccw_id);
	end_label = get_label_id("end", type, ccw_id);

	/* Apply attributes in correct order. */
	list = setting_list_get_sorted(state->settings);

	debug("Writing %s udev rule file %s\n", type, path);
	if (!util_path_exists(path)) {
		rc = path_create(path);
		if (rc)
			goto out;
	}

	fd = misc_fopen(path, "w");
	if (!fd) {
		error("Could not write to file %s: %s\n", path,
		      strerror(errno));
		rc = EXIT_RUNTIME_ERROR;
		goto out;
	}

	/* Write udev rule prolog. */
	fprintf(fd, "# Generated by chzdev\n");

	/* Triggers. */
	fprintf(fd, "ACTION!=\"add|change\", GOTO=\"%s\"\n", end_label);
	fprintf(fd, "SUBSYSTEM==\"drivers\", "
		"KERNEL==\"%s\", GOTO=\"%s\"\n", drv, group_label);
	for (i = 0; i < devid.num; i++) {
		chan_id = ccw_devid_to_str(&devid.devid[i]);
		fprintf(fd, "SUBSYSTEM==\"ccw\", "
			"KERNEL==\"%s\", DRIVER==\"%s\", "
			"GOTO=\"%s\"\n", chan_id, drv, group_label);
		free(chan_id);
	}
	fprintf(fd, "ACTION==\"add\", SUBSYSTEM==\"ccwgroup\", "
		"KERNEL==\"%s\", DRIVER==\"%s\", GOTO=\"%s\"\n", ccw_id,
		drv, cfg_label);
	fprintf(fd, "GOTO=\"%s\"\n\n", end_label);

	/* Group. */
	fprintf(fd, "LABEL=\"%s\"\n", group_label);
	fprintf(fd, "TEST==\"[ccwgroup/%s]\", GOTO=\"%s\"\n", ccw_id,
		end_label);
	for (i = 0; i < devid.num; i++) {
		chan_id = ccw_devid_to_str(&devid.devid[i]);
		fprintf(fd, "TEST!=\"[ccw/%s]\", GOTO=\"%s\"\n", chan_id,
			end_label);
		free(chan_id);
	}

	/* Parallel execution of the ccwgroup devices udev rule can lead to
	 * udevadm warnings. To prevent this, serialize the execution.
	 */
	fprintf(fd, "\n# Note: Serialize add events on different devices to"
		   " change event on single\n# base device to prevent duplicate"
		   " concurrent grouping attempts\n");

	/* The new change uevent should be generated to the base-device only */
	chan_id = ccw_devid_to_str(&devid.devid[0]);
	fprintf(fd, "ACTION==\"add\", ATTR{[ccw/%s]uevent}=\"change\"\n", chan_id);
	fprintf(fd, "ACTION==\"change\", ATTR{[drivers/ccwgroup:%s]group}=\"", drv);
	free(chan_id);
	for (i = 0; i < devid.num; i++) {
		chan_id = ccw_devid_to_str(&devid.devid[i]);
		fprintf(fd, "%s%s", i > 0 ? "," : "", chan_id);
		free(chan_id);
	}
	fprintf(fd, "\"\n");
	fprintf(fd, "GOTO=\"%s\"\n\n", end_label);

	/* Configure. */
	fprintf(fd, "LABEL=\"%s\"\n", cfg_label);
	util_list_iterate(list, p) {
		s = p->ptr;
		if (s->removed)
			continue;
		if (s->values) {
			util_list_iterate(s->values, str) {
				fprintf(fd, "ATTR{[ccwgroup/%s]%s}=\"%s\"\n",
					ccw_id, s->name, str->str);
			}
		} else if ((s->attrib && s->attrib->internal) ||
			   internal_by_name(s->name)) {
			fprintf(fd, "ENV{zdev_%s}=\"%s\"\n",
				internal_get_name(s->name), s->value);
		} else {
			fprintf(fd, "ATTR{[ccwgroup/%s]%s}=\"%s\"\n", ccw_id,
				s->name, s->value);
		}
	}

	/* Write udev rule epilog. */
	fprintf(fd, "\n");
	fprintf(fd, "LABEL=\"%s\"\n", end_label);

	if (misc_fclose(fd))
		warn("Could not close file %s: %s\n", path, strerror(errno));

out:
	ptrlist_free(list, 0);
	free(end_label);
	free(cfg_label);
	free(group_label);
	free(path);
	free(ccw_id);

	return rc;
}

static char *read_full_id(const char *path)
{
	char *text, *start, *end, *id = NULL;

	text = misc_read_text_file(path, 0, err_ignore);
	if (!text)
		return NULL;
	start = strstr(text, "ATTR{[drivers/ccwgroup:");
	if (!start)
		goto out;
	start = strchr(start, '"');
	if (!start)
		goto out;
	start++;
	end = strchr(start, '"');
	if (!end)
		goto out;
	*end = 0;
	id = misc_strdup(start);

	start = id;
	while ((start = strchr(start, ',')))
		*start = ':';

out:
	free(text);

	if (!id)
		warn_once("Warning: Invalid udev rule: %s\n", path);

	return id;
}

struct get_ids_cb_data {
	char *prefix;
	struct util_list *ids;
};

static exit_code_t get_ids_cb(const char *path, const char *filename,
			      void *data)
{
	struct get_ids_cb_data *cb_data = data;
	char *id;

	if (!starts_with(filename, cb_data->prefix))
		return EXIT_OK;
	id = read_full_id(path);
	if (!id)
		return EXIT_OK;
	strlist_add(cb_data->ids, id);
	free(id);

	return EXIT_OK;
}

/* Add the IDs for all devices of the specified subtype name for which a
 * udev rule exists to strlist LIST. */
void udev_ccwgroup_add_device_ids(const char *type, struct util_list *list,
				  bool autoconf)
{
	struct get_ids_cb_data cb_data;
	char *path;

	path = path_get_udev_rules(autoconf);
	cb_data.prefix = misc_asprintf("%s-%s-", UDEV_PREFIX, type);
	cb_data.ids = list;

	if (util_path_is_dir(path))
		path_for_each(path, get_ids_cb, &cb_data);

	free(cb_data.prefix);
	free(path);
}

/* Remove UDEV rule for CCWGROUP device. */
exit_code_t udev_ccwgroup_remove_rule(const char *type, const char *id,
				      bool autoconf)
{
	char *partial_id, *path;
	exit_code_t rc = EXIT_OK;

	partial_id = ccwgroup_get_partial_id(id);
	if (!partial_id)
		return EXIT_INVALID_ID;

	path = path_get_udev_rule(type, partial_id, autoconf);
	if (util_path_is_reg_file(path))
		rc = remove_file(path);
	free(path);
	free(partial_id);

	return rc;
}
