/*
 * 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 "attrib.h"
#include "ccw.h"
#include "device.h"
#include "misc.h"
#include "path.h"
#include "setting.h"
#include "udev.h"

int udev_need_settle = 0;
int udev_no_settle;

/* Create a newly allocated udev entry. */
static struct udev_entry_node *udev_entry_node_new(const char *key,
						   const char *op,
						   const char *value)
{
	struct udev_entry_node *entry;

	entry = misc_malloc(sizeof(struct udev_entry_node));
	entry->key = misc_strdup(key);
	entry->op = misc_strdup(op);
	entry->value = misc_strdup(value);

	return entry;
}

/* Release resources associated with udev entry. */
static void udev_entry_node_free(struct udev_entry_node *entry)
{
	if (!entry)
		return;
	free(entry->key);
	free(entry->op);
	free(entry->value);
	free(entry);
}

/* Create a newly allocated udev line. */
static struct udev_line_node *udev_line_node_new(void)
{
	struct udev_line_node *line;

	line = misc_malloc(sizeof(struct udev_line_node));
	util_list_init(&line->entries, struct udev_entry_node, node);

	return line;
}

/* Release resources associated with udev line. */
static void udev_line_node_free(struct udev_line_node *line)
{
	struct udev_entry_node *e, *n;

	if (!line)
		return;
	util_list_iterate_safe(&line->entries, e, n) {
		util_list_remove(&line->entries, e);
		udev_entry_node_free(e);
	}
	free(line->line);
	free(line);
}

/* Create a newly allocated udev file. */
static struct udev_file *udev_file_new(void)
{
	struct udev_file *file;

	file = misc_malloc(sizeof(struct udev_file));
	util_list_init(&file->lines, struct udev_line_node, node);

	return file;
}

/* Release resources associated with udev file. */
void udev_free_file(struct udev_file *file)
{
	struct udev_line_node *l, *n;

	if (!file)
		return;
	util_list_iterate_safe(&file->lines, l, n) {
		util_list_remove(&file->lines, l);
		udev_line_node_free(l);
	}
	free(file);
}

/* Used for debugging. */
void udev_file_print(struct udev_file *file)
{
	struct udev_line_node *l;
	struct udev_entry_node *e;

	printf("udev_file at %p\n", (void *) file);
	if (!file)
		return;
	util_list_iterate(&file->lines, l) {
		printf("  udev_line_node at %p\n", (void *) l);
		printf("    line='%s'\n", l->line);
		util_list_iterate(&l->entries, e) {
			printf("    udev_entry at %p\n", (void *) e);
			printf("      '%s' '%s' '%s'\n", e->key, e->op,
			       e->value);
		}
	}
}

static void skip_whitespace(const char **s_ptr)
{
	const char *s = *s_ptr;

	while (*s && isspace(*s))
		s++;

	*s_ptr = s;
}

static char *parse_key(const char **s_ptr)
{
	const char *s, *e;
	char *key;

	s = *s_ptr;
	/* Parse \w+(\{[^\}]*\})? */
	e = s;
	while (*e && (isalnum(*e) || *e == '_'))
		e++;
	if (*e == '{') {
		while (*e && *e != '}')
			e++;
		if (*e == '}')
			e++;
	}

	if (e == s)
		return NULL;

	/* s points to key start, e to character after key end. */
	key = misc_malloc(e - s + 1);
	memcpy(key, s, e - s);

	*s_ptr = e;

	return key;
}

static char *parse_op(const char **s_ptr)
{
	const char *ops[] = { "==", "!=", "=", "+=", ":=", NULL };
	const char *entry;
	size_t len;
	int i;

	entry = *s_ptr;
	for (i = 0; ops[i]; i++) {
		len = strlen(ops[i]);
		if (strncmp(entry, ops[i], len) == 0) {
			*s_ptr += len;
			return misc_strdup(ops[i]);
		}
	}

	return NULL;
}

static char *parse_value(const char **s_ptr)
{
	const char *s, *e;
	char *value;

	/* Parse: ^\s*(.*)\s*$ */
	s = *s_ptr;
	skip_whitespace(&s);
	e = s;
	while (*e)
		e++;
	e--;
	while (e > s && isspace(*e))
		e--;
	e++;

	*s_ptr = e;

	/* Remove quotes. */
	if ((*s == '"' && *(e - 1) == '"') ||
	    (*s == '\'' && *(e - 1) == '\'')) {
		s++;
		e--;
	}

	/* s points to value start, e to character after value end. */
	value = misc_malloc(e - s + 1);
	memcpy(value, s, e - s);

	return value;
}

static bool parse_udev_entry(struct udev_line_node *line, const char *entry)
{
	char *key = NULL, *op = NULL, *value = NULL;
	struct udev_entry_node *e;
	bool rc = false;

	/* Parse: ^\s*(\w+)\s*(==|!=|=|\+=|:=)\s*"?([^"]*)"\s*$ */

	/* Parse key. */
	skip_whitespace(&entry);
	key = parse_key(&entry);
	if (!key)
		goto out;

	/* Parse operator. */
	skip_whitespace(&entry);
	op = parse_op(&entry);
	if (!op)
		goto out;

	/* Parse value. */
	skip_whitespace(&entry);
	value = parse_value(&entry);
	if (!value)
		goto out;
	skip_whitespace(&entry);

	/* Check for unrecognized characters at end of entry. */
	if (*entry != 0)
		goto out;

	/* Add entry to list. */
	e = udev_entry_node_new(key, op, value);
	util_list_add_tail(&line->entries, e);
	rc = true;

out:
	free(key);
	free(op);
	free(value);

	return rc;
}

static void replace_unquoted(char *s, char from, char to)
{
	char quoted = 0;

	for (; *s; s++) {
		if (quoted) {
			/* Skip until quote end is found. */
			if (*s == quoted)
				quoted = 0;
			continue;
		}
		if (*s == '"' || *s == '\'') {
			quoted = *s;
			continue;
		}
		if (*s == from)
			*s = to;
	}
}

static bool parse_udev_line(struct udev_file *file, const char *line)
{
	char *copy, *curr, *next;
	struct udev_line_node *l;
	int i;
	bool result = true;

	l = udev_line_node_new();
	l->line = misc_strdup(line);

	/* Check for empty lines and comment lines. */
	for (i = 0; line[i] && isspace(line[i]); i++);
	if (line[i] == 0 || line[i] == '#')
		goto ok;

	/* Parse each comma-separated entry. */
	copy = misc_strdup(line);

	/* A hack to differentiate between quoted and unquoted commas. */
	replace_unquoted(copy, ',', 1);

	next = copy;
	while ((curr = strsep(&next, "\1"))) {
		if (!parse_udev_entry(l, curr)) {
			result = false;
			break;
		}
	}
	free(copy);

ok:
	if (result)
		util_list_add_tail(&file->lines, l);
	else
		udev_line_node_free(l);

	return result;
}

/* Read the contents of a udev rule file. */
exit_code_t udev_read_file(const char *path, struct udev_file **file_ptr)
{
	char *text, *curr, *next;
	struct udev_file *file;
	int once = 0;

	text = misc_read_text_file(path, 0, err_print);
	if (!text)
		return EXIT_RUNTIME_ERROR;
	file = udev_file_new();

	/* Iterate over each line. */
	next = text;
	while ((curr = strsep(&next, "\n"))) {
		if (parse_udev_line(file, curr))
			continue;
		if (!once) {
			once = 1;
			verb("Unrecognized udev rule format in %s:\n", path);
		}
		verb("%s\n", curr);
	}

	free(text);
	*file_ptr = file;

	return EXIT_OK;
}

/* Check if a udev file does not contain any statements. */
bool udev_file_is_empty(struct udev_file *file)
{
	struct udev_line_node *l;

	if (!file)
		return true;
	util_list_iterate(&file->lines, l) {
		if (l->line[0])
			return false;
	}

	return true;
}

static bool get_ids_cb(const char *filename, void *data)
{
	char *prefix = data;

	if (strncmp(filename, prefix, strlen(prefix)) != 0)
		return false;
	if (!ends_with(filename, UDEV_SUFFIX))
		return false;

	return true;
}

/* Add the IDs for all devices of the specified subtype name for which a
 * udev rule exists to strlist LIST. */
void udev_get_device_ids(const char *type, struct util_list *list,
			 bool autoconf)
{
	char *path, *prefix;
	struct util_list *files;
	struct strlist_node *s;
	size_t plen, len;

	prefix = misc_asprintf("%s-%s-", UDEV_PREFIX, type);
	plen = strlen(prefix);
	path = path_get_udev_rules(autoconf);
	files = strlist_new();
	if (!misc_read_dir(path, files, get_ids_cb, prefix))
		goto out;

	util_list_iterate(files, s) {
		/* 41-dasd-eckd-0.0.1234.rules */
		len = strlen(s->str);
		s->str[len - sizeof(UDEV_SUFFIX) + 1] = 0;
		strlist_add(list, &s->str[plen]);
	}

out:
	strlist_free(files);
	free(path);
	free(prefix);
}

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

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

	return rc;
}

/* Wait for all current udev events to finish. */
void udev_settle(void)
{
	if (udev_no_settle)
		return;
	misc_system(err_ignore, "%s settle", PATH_UDEVADM);
}

/* Extract internal attribute settings from @entry and add to @list.
 * Associate corresponding attribute if found in @attribs. */
void udev_add_internal_from_entry(struct setting_list *list,
				  struct udev_entry_node *entry,
				  struct attrib **attribs)
{
	char *copy, *name, *end, *u;
	struct attrib *a;

	/* ENV{zdev_var}="1" */
	copy = misc_strdup(entry->key);

	/* Find attribute name start. */
	name = strchr(copy, '{');
	end = strrchr(copy, '}');
	if (!name || !end)
		goto out;
	*end = 0;
	name++;

	/* zdev_ => zdev: */
	u = strchr(name, '_');
	if (u)
		*u = ':';

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

out:
	free(copy);
}
